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,873 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SqliteTraceStore:SQLite 持久化存储。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
1. 管理 SQLite schema(trace_sessions / trace_spans)
|
|
6
|
+
2. 提供写入接口(save_session / save_spans)
|
|
7
|
+
3. 提供查询接口(query_sessions / get_span_tree)
|
|
8
|
+
|
|
9
|
+
设计要点:
|
|
10
|
+
- WAL 模式:并发读写安全
|
|
11
|
+
- 索引优化:session_id / parent_span_id / span_type
|
|
12
|
+
- 批量写入:减少 I/O
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sqlite3
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import List, Optional, Dict, Any
|
|
21
|
+
from contextlib import contextmanager
|
|
22
|
+
|
|
23
|
+
from langchain_agentx.workspace import resolve_agent_workspace_config
|
|
24
|
+
|
|
25
|
+
from .models import TraceSession, TraceSpan
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SqliteTraceStore:
|
|
29
|
+
"""SQLite trace 存储"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, db_path: str = None):
|
|
32
|
+
"""
|
|
33
|
+
初始化 SqliteTraceStore。
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
db_path: SQLite 数据库路径(默认:<workspace_root>/<agent_home>/data/db/traces.db)
|
|
37
|
+
"""
|
|
38
|
+
if db_path is None:
|
|
39
|
+
workspace_cfg = resolve_agent_workspace_config()
|
|
40
|
+
db_file = workspace_cfg.traces_db_path
|
|
41
|
+
db_file.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
db_path = str(db_file)
|
|
43
|
+
|
|
44
|
+
self.db_path = db_path
|
|
45
|
+
self._init_db()
|
|
46
|
+
|
|
47
|
+
def _init_db(self) -> None:
|
|
48
|
+
"""初始化数据库 schema(非破坏性,不清空历史数据)。"""
|
|
49
|
+
with self._get_connection() as conn:
|
|
50
|
+
current_journal_mode = str(
|
|
51
|
+
conn.execute("PRAGMA journal_mode").fetchone()[0]
|
|
52
|
+
).lower()
|
|
53
|
+
if current_journal_mode != "wal":
|
|
54
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
55
|
+
|
|
56
|
+
# 创建 trace_sessions 表
|
|
57
|
+
conn.execute("""
|
|
58
|
+
CREATE TABLE IF NOT EXISTS trace_sessions (
|
|
59
|
+
session_id TEXT PRIMARY KEY,
|
|
60
|
+
conversation_session_id TEXT NOT NULL,
|
|
61
|
+
parent_session_id TEXT,
|
|
62
|
+
parent_tool_call_id TEXT,
|
|
63
|
+
workflow_id TEXT,
|
|
64
|
+
container_type TEXT NOT NULL DEFAULT 'agent_session',
|
|
65
|
+
unit_type TEXT NOT NULL DEFAULT 'main_loop',
|
|
66
|
+
origin TEXT NOT NULL DEFAULT 'user',
|
|
67
|
+
visibility TEXT NOT NULL DEFAULT 'default',
|
|
68
|
+
user_query TEXT,
|
|
69
|
+
graph_name TEXT,
|
|
70
|
+
start_time REAL NOT NULL,
|
|
71
|
+
end_time REAL,
|
|
72
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
73
|
+
finish_reason TEXT,
|
|
74
|
+
exit_code TEXT,
|
|
75
|
+
terminal_reason TEXT,
|
|
76
|
+
loop_count INTEGER DEFAULT 0,
|
|
77
|
+
total_input_tokens INTEGER DEFAULT 0,
|
|
78
|
+
total_output_tokens INTEGER DEFAULT 0,
|
|
79
|
+
total_latency_ms REAL DEFAULT 0.0,
|
|
80
|
+
metadata_json TEXT
|
|
81
|
+
)
|
|
82
|
+
""")
|
|
83
|
+
self._ensure_session_columns(conn)
|
|
84
|
+
|
|
85
|
+
# 创建 trace_spans 表
|
|
86
|
+
conn.execute("""
|
|
87
|
+
CREATE TABLE IF NOT EXISTS trace_spans (
|
|
88
|
+
span_id TEXT PRIMARY KEY,
|
|
89
|
+
session_id TEXT NOT NULL,
|
|
90
|
+
parent_span_id TEXT,
|
|
91
|
+
span_type TEXT NOT NULL,
|
|
92
|
+
name TEXT NOT NULL,
|
|
93
|
+
start_time REAL NOT NULL,
|
|
94
|
+
end_time REAL,
|
|
95
|
+
latency_ms REAL,
|
|
96
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
97
|
+
input_json TEXT,
|
|
98
|
+
output_json TEXT,
|
|
99
|
+
error TEXT,
|
|
100
|
+
metadata_json TEXT,
|
|
101
|
+
FOREIGN KEY (session_id) REFERENCES trace_sessions(session_id)
|
|
102
|
+
)
|
|
103
|
+
""")
|
|
104
|
+
|
|
105
|
+
# 创建索引
|
|
106
|
+
conn.execute("""
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_spans_session
|
|
108
|
+
ON trace_spans(session_id)
|
|
109
|
+
""")
|
|
110
|
+
|
|
111
|
+
conn.execute("""
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_spans_parent
|
|
113
|
+
ON trace_spans(parent_span_id)
|
|
114
|
+
""")
|
|
115
|
+
|
|
116
|
+
conn.execute("""
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_spans_type
|
|
118
|
+
ON trace_spans(span_type)
|
|
119
|
+
""")
|
|
120
|
+
|
|
121
|
+
conn.execute("""
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_time
|
|
123
|
+
ON trace_sessions(start_time DESC)
|
|
124
|
+
""")
|
|
125
|
+
|
|
126
|
+
conn.execute("""
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status
|
|
128
|
+
ON trace_sessions(status)
|
|
129
|
+
""")
|
|
130
|
+
conn.execute("""
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_conversation
|
|
132
|
+
ON trace_sessions(conversation_session_id)
|
|
133
|
+
""")
|
|
134
|
+
conn.execute("""
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_parent
|
|
136
|
+
ON trace_sessions(parent_session_id)
|
|
137
|
+
""")
|
|
138
|
+
conn.execute("""
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_visibility
|
|
140
|
+
ON trace_sessions(visibility)
|
|
141
|
+
""")
|
|
142
|
+
conn.execute("""
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_container_type
|
|
144
|
+
ON trace_sessions(container_type)
|
|
145
|
+
""")
|
|
146
|
+
conn.execute("""
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_workflow
|
|
148
|
+
ON trace_sessions(workflow_id)
|
|
149
|
+
""")
|
|
150
|
+
conn.execute("""
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_unit_type
|
|
152
|
+
ON trace_sessions(unit_type)
|
|
153
|
+
""")
|
|
154
|
+
self._init_views(conn)
|
|
155
|
+
|
|
156
|
+
conn.commit()
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _init_views(conn: sqlite3.Connection) -> None:
|
|
160
|
+
conn.execute("""
|
|
161
|
+
CREATE VIEW IF NOT EXISTS v_session_summary AS
|
|
162
|
+
SELECT
|
|
163
|
+
s.session_id,
|
|
164
|
+
s.graph_name,
|
|
165
|
+
s.user_query,
|
|
166
|
+
s.start_time,
|
|
167
|
+
s.end_time,
|
|
168
|
+
s.total_latency_ms,
|
|
169
|
+
s.total_input_tokens,
|
|
170
|
+
s.total_output_tokens,
|
|
171
|
+
s.finish_reason,
|
|
172
|
+
s.status,
|
|
173
|
+
s.container_type,
|
|
174
|
+
s.unit_type,
|
|
175
|
+
s.visibility,
|
|
176
|
+
(SELECT COUNT(*) FROM trace_spans sp
|
|
177
|
+
WHERE sp.session_id = s.session_id AND sp.span_type = 'loop_step') AS step_count,
|
|
178
|
+
(SELECT COUNT(*) FROM trace_spans sp
|
|
179
|
+
WHERE sp.session_id = s.session_id AND sp.span_type = 'tool_call') AS tool_count
|
|
180
|
+
FROM trace_sessions s
|
|
181
|
+
ORDER BY s.start_time DESC
|
|
182
|
+
""")
|
|
183
|
+
conn.execute("""
|
|
184
|
+
CREATE VIEW IF NOT EXISTS v_conversation_summary AS
|
|
185
|
+
SELECT
|
|
186
|
+
conversation_session_id AS conversation_id,
|
|
187
|
+
COUNT(*) AS run_count,
|
|
188
|
+
MIN(start_time) AS first_start_time,
|
|
189
|
+
MAX(COALESCE(end_time, start_time)) AS last_end_time,
|
|
190
|
+
SUM(total_input_tokens) AS total_input_tokens,
|
|
191
|
+
SUM(total_output_tokens) AS total_output_tokens,
|
|
192
|
+
SUM(total_latency_ms) AS total_latency_ms
|
|
193
|
+
FROM trace_sessions
|
|
194
|
+
WHERE conversation_session_id IS NOT NULL
|
|
195
|
+
AND conversation_session_id != ''
|
|
196
|
+
GROUP BY conversation_session_id
|
|
197
|
+
ORDER BY last_end_time DESC
|
|
198
|
+
""")
|
|
199
|
+
conn.execute("""
|
|
200
|
+
CREATE VIEW IF NOT EXISTS v_workflow_summary AS
|
|
201
|
+
SELECT
|
|
202
|
+
workflow_id,
|
|
203
|
+
COUNT(*) AS total_tasks,
|
|
204
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS succeeded,
|
|
205
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
206
|
+
SUM(CASE WHEN status = 'interrupted' THEN 1 ELSE 0 END) AS interrupted,
|
|
207
|
+
MIN(start_time) AS workflow_start,
|
|
208
|
+
MAX(COALESCE(end_time, start_time)) AS workflow_end,
|
|
209
|
+
(MAX(COALESCE(end_time, start_time)) - MIN(start_time)) * 1000 AS total_latency_ms,
|
|
210
|
+
SUM(total_input_tokens) AS total_input_tokens,
|
|
211
|
+
SUM(total_output_tokens) AS total_output_tokens
|
|
212
|
+
FROM trace_sessions
|
|
213
|
+
WHERE workflow_id IS NOT NULL
|
|
214
|
+
AND workflow_id != ''
|
|
215
|
+
GROUP BY workflow_id
|
|
216
|
+
ORDER BY workflow_start DESC
|
|
217
|
+
""")
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _ensure_session_columns(conn: sqlite3.Connection) -> None:
|
|
221
|
+
"""为历史数据库补齐新增列(幂等)。"""
|
|
222
|
+
existing = {
|
|
223
|
+
str(row[1])
|
|
224
|
+
for row in conn.execute("PRAGMA table_info(trace_sessions)").fetchall()
|
|
225
|
+
}
|
|
226
|
+
required_columns = {
|
|
227
|
+
"container_type": "TEXT NOT NULL DEFAULT 'agent_session'",
|
|
228
|
+
"unit_type": "TEXT NOT NULL DEFAULT 'main_loop'",
|
|
229
|
+
"origin": "TEXT NOT NULL DEFAULT 'user'",
|
|
230
|
+
"visibility": "TEXT NOT NULL DEFAULT 'default'",
|
|
231
|
+
"workflow_id": "TEXT",
|
|
232
|
+
}
|
|
233
|
+
for column, column_type in required_columns.items():
|
|
234
|
+
if column in existing:
|
|
235
|
+
continue
|
|
236
|
+
conn.execute(f"ALTER TABLE trace_sessions ADD COLUMN {column} {column_type}")
|
|
237
|
+
|
|
238
|
+
@contextmanager
|
|
239
|
+
def _get_connection(self):
|
|
240
|
+
"""获取数据库连接(上下文管理器)
|
|
241
|
+
|
|
242
|
+
Note:
|
|
243
|
+
timeout=30.0 用于并发写入场景(WAL 模式下写锁竞争)。
|
|
244
|
+
详见 docs/architecture/observability-sqlite-concurrency.md
|
|
245
|
+
"""
|
|
246
|
+
conn = sqlite3.connect(self.db_path, timeout=30.0, check_same_thread=False)
|
|
247
|
+
conn.row_factory = sqlite3.Row
|
|
248
|
+
try:
|
|
249
|
+
yield conn
|
|
250
|
+
finally:
|
|
251
|
+
conn.close()
|
|
252
|
+
|
|
253
|
+
def save_session(self, session: TraceSession) -> None:
|
|
254
|
+
with self._get_connection() as conn:
|
|
255
|
+
conn.execute("""
|
|
256
|
+
INSERT OR REPLACE INTO trace_sessions (
|
|
257
|
+
session_id, conversation_session_id, parent_session_id, parent_tool_call_id,
|
|
258
|
+
workflow_id,
|
|
259
|
+
container_type, unit_type, origin, visibility,
|
|
260
|
+
user_query, graph_name, start_time, end_time,
|
|
261
|
+
status, finish_reason, exit_code, terminal_reason, loop_count,
|
|
262
|
+
total_input_tokens, total_output_tokens, total_latency_ms, metadata_json
|
|
263
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
264
|
+
""", (
|
|
265
|
+
session.session_id,
|
|
266
|
+
session.conversation_session_id or session.session_id,
|
|
267
|
+
session.parent_session_id,
|
|
268
|
+
session.parent_tool_call_id,
|
|
269
|
+
session.workflow_id,
|
|
270
|
+
session.container_type,
|
|
271
|
+
session.unit_type,
|
|
272
|
+
session.origin,
|
|
273
|
+
session.visibility,
|
|
274
|
+
session.user_query,
|
|
275
|
+
session.graph_name,
|
|
276
|
+
session.start_time,
|
|
277
|
+
session.end_time,
|
|
278
|
+
session.status,
|
|
279
|
+
session.finish_reason,
|
|
280
|
+
session.exit_code,
|
|
281
|
+
session.terminal_reason,
|
|
282
|
+
session.loop_count,
|
|
283
|
+
session.total_input_tokens,
|
|
284
|
+
session.total_output_tokens,
|
|
285
|
+
session.total_latency_ms,
|
|
286
|
+
json.dumps(session.metadata, default=str) if session.metadata else None,
|
|
287
|
+
))
|
|
288
|
+
conn.commit()
|
|
289
|
+
|
|
290
|
+
def save_spans(self, spans: List[TraceSpan]) -> None:
|
|
291
|
+
"""
|
|
292
|
+
批量保存 spans。
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
spans: TraceSpan 列表
|
|
296
|
+
"""
|
|
297
|
+
if not spans:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
with self._get_connection() as conn:
|
|
301
|
+
conn.executemany("""
|
|
302
|
+
INSERT OR REPLACE INTO trace_spans (
|
|
303
|
+
span_id, session_id, parent_span_id, span_type, name,
|
|
304
|
+
start_time, end_time, latency_ms, status,
|
|
305
|
+
input_json, output_json, error, metadata_json
|
|
306
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
307
|
+
""", [
|
|
308
|
+
(
|
|
309
|
+
span.span_id,
|
|
310
|
+
span.session_id,
|
|
311
|
+
span.parent_span_id,
|
|
312
|
+
span.span_type.value if hasattr(span.span_type, 'value') else span.span_type,
|
|
313
|
+
span.name,
|
|
314
|
+
span.start_time,
|
|
315
|
+
span.end_time,
|
|
316
|
+
span.latency_ms,
|
|
317
|
+
span.status.value if hasattr(span.status, 'value') else span.status,
|
|
318
|
+
json.dumps(span.input_data, default=str) if span.input_data else None,
|
|
319
|
+
json.dumps(span.output_data, default=str) if span.output_data else None,
|
|
320
|
+
span.error,
|
|
321
|
+
json.dumps(span.metadata, default=str) if span.metadata else None,
|
|
322
|
+
)
|
|
323
|
+
for span in spans
|
|
324
|
+
])
|
|
325
|
+
conn.commit()
|
|
326
|
+
|
|
327
|
+
def _row_to_session(self, row) -> TraceSession:
|
|
328
|
+
return TraceSession(
|
|
329
|
+
session_id=row["session_id"],
|
|
330
|
+
conversation_session_id=row["conversation_session_id"],
|
|
331
|
+
parent_session_id=row["parent_session_id"],
|
|
332
|
+
parent_tool_call_id=row["parent_tool_call_id"],
|
|
333
|
+
workflow_id=row["workflow_id"] if "workflow_id" in row.keys() else None,
|
|
334
|
+
container_type=row["container_type"] if "container_type" in row.keys() else "agent_session",
|
|
335
|
+
unit_type=row["unit_type"],
|
|
336
|
+
origin=row["origin"],
|
|
337
|
+
visibility=row["visibility"],
|
|
338
|
+
user_query=row["user_query"],
|
|
339
|
+
graph_name=row["graph_name"],
|
|
340
|
+
start_time=row["start_time"],
|
|
341
|
+
end_time=row["end_time"],
|
|
342
|
+
status=row["status"],
|
|
343
|
+
finish_reason=row["finish_reason"],
|
|
344
|
+
exit_code=row["exit_code"],
|
|
345
|
+
terminal_reason=row["terminal_reason"],
|
|
346
|
+
loop_count=row["loop_count"],
|
|
347
|
+
total_input_tokens=row["total_input_tokens"],
|
|
348
|
+
total_output_tokens=row["total_output_tokens"],
|
|
349
|
+
total_latency_ms=row["total_latency_ms"],
|
|
350
|
+
metadata=json.loads(row["metadata_json"]) if row["metadata_json"] else {},
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def get_session(self, session_id: str) -> Optional[TraceSession]:
|
|
354
|
+
"""
|
|
355
|
+
获取 session。
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
session_id: session 唯一标识
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
TraceSession 或 None
|
|
362
|
+
"""
|
|
363
|
+
with self._get_connection() as conn:
|
|
364
|
+
row = conn.execute("""
|
|
365
|
+
SELECT * FROM trace_sessions WHERE session_id = ?
|
|
366
|
+
""", (session_id,)).fetchone()
|
|
367
|
+
|
|
368
|
+
if not row:
|
|
369
|
+
return None
|
|
370
|
+
return self._row_to_session(row)
|
|
371
|
+
|
|
372
|
+
def query_sessions(
|
|
373
|
+
self,
|
|
374
|
+
limit: int = 20,
|
|
375
|
+
offset: int = 0,
|
|
376
|
+
status: Optional[str] = None,
|
|
377
|
+
graph_name: Optional[str] = None,
|
|
378
|
+
visibility: Optional[str] = None,
|
|
379
|
+
workflow_id: Optional[str] = None,
|
|
380
|
+
**kwargs
|
|
381
|
+
) -> List[TraceSession]:
|
|
382
|
+
"""
|
|
383
|
+
查询 session 列表。
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
limit: 返回条数
|
|
387
|
+
offset: 偏移量
|
|
388
|
+
status: 过滤状态
|
|
389
|
+
graph_name: 过滤 graph 名称
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
TraceSession 列表
|
|
393
|
+
"""
|
|
394
|
+
query = "SELECT * FROM trace_sessions WHERE 1=1"
|
|
395
|
+
params = []
|
|
396
|
+
|
|
397
|
+
if status:
|
|
398
|
+
query += " AND status = ?"
|
|
399
|
+
params.append(status)
|
|
400
|
+
|
|
401
|
+
if graph_name:
|
|
402
|
+
query += " AND graph_name = ?"
|
|
403
|
+
params.append(graph_name)
|
|
404
|
+
|
|
405
|
+
if visibility:
|
|
406
|
+
query += " AND visibility = ?"
|
|
407
|
+
params.append(visibility)
|
|
408
|
+
|
|
409
|
+
if workflow_id:
|
|
410
|
+
query += " AND workflow_id = ?"
|
|
411
|
+
params.append(workflow_id)
|
|
412
|
+
|
|
413
|
+
query += " ORDER BY COALESCE(end_time, start_time) DESC LIMIT ? OFFSET ?"
|
|
414
|
+
params.extend([limit, offset])
|
|
415
|
+
|
|
416
|
+
with self._get_connection() as conn:
|
|
417
|
+
rows = conn.execute(query, params).fetchall()
|
|
418
|
+
return [self._row_to_session(row) for row in rows]
|
|
419
|
+
|
|
420
|
+
def query_workflow_summary(self, limit: int = 20) -> List[Dict[str, Any]]:
|
|
421
|
+
"""按 workflow_id 聚合查询摘要。"""
|
|
422
|
+
with self._get_connection() as conn:
|
|
423
|
+
rows = conn.execute(
|
|
424
|
+
"""
|
|
425
|
+
SELECT
|
|
426
|
+
workflow_id,
|
|
427
|
+
COUNT(*) AS total_tasks,
|
|
428
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS succeeded,
|
|
429
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
430
|
+
SUM(CASE WHEN status = 'interrupted' THEN 1 ELSE 0 END) AS interrupted,
|
|
431
|
+
MIN(start_time) AS workflow_start,
|
|
432
|
+
MAX(COALESCE(end_time, start_time)) AS workflow_end,
|
|
433
|
+
(MAX(COALESCE(end_time, start_time)) - MIN(start_time)) * 1000 AS total_latency_ms,
|
|
434
|
+
SUM(total_input_tokens) AS total_input_tokens,
|
|
435
|
+
SUM(total_output_tokens) AS total_output_tokens
|
|
436
|
+
FROM trace_sessions
|
|
437
|
+
WHERE workflow_id IS NOT NULL AND workflow_id != ''
|
|
438
|
+
GROUP BY workflow_id
|
|
439
|
+
ORDER BY workflow_start DESC
|
|
440
|
+
LIMIT ?
|
|
441
|
+
""",
|
|
442
|
+
(limit,),
|
|
443
|
+
).fetchall()
|
|
444
|
+
return [dict(row) for row in rows]
|
|
445
|
+
|
|
446
|
+
def get_workflow_summary(self, workflow_id: str) -> Optional[Dict[str, Any]]:
|
|
447
|
+
"""获取单个 workflow 的聚合摘要。"""
|
|
448
|
+
with self._get_connection() as conn:
|
|
449
|
+
row = conn.execute(
|
|
450
|
+
"""
|
|
451
|
+
SELECT
|
|
452
|
+
workflow_id,
|
|
453
|
+
COUNT(*) AS total_tasks,
|
|
454
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS succeeded,
|
|
455
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
456
|
+
SUM(CASE WHEN status = 'interrupted' THEN 1 ELSE 0 END) AS interrupted,
|
|
457
|
+
MIN(start_time) AS workflow_start,
|
|
458
|
+
MAX(COALESCE(end_time, start_time)) AS workflow_end,
|
|
459
|
+
(MAX(COALESCE(end_time, start_time)) - MIN(start_time)) * 1000 AS total_latency_ms,
|
|
460
|
+
SUM(total_input_tokens) AS total_input_tokens,
|
|
461
|
+
SUM(total_output_tokens) AS total_output_tokens
|
|
462
|
+
FROM trace_sessions
|
|
463
|
+
WHERE workflow_id = ?
|
|
464
|
+
GROUP BY workflow_id
|
|
465
|
+
""",
|
|
466
|
+
(workflow_id,),
|
|
467
|
+
).fetchone()
|
|
468
|
+
return dict(row) if row else None
|
|
469
|
+
|
|
470
|
+
def get_sessions_by_ids(self, session_ids: List[str]) -> Dict[str, TraceSession]:
|
|
471
|
+
"""按 session_id 批量读取 sessions。"""
|
|
472
|
+
if not session_ids:
|
|
473
|
+
return {}
|
|
474
|
+
placeholders = ",".join("?" for _ in session_ids)
|
|
475
|
+
query = f"SELECT * FROM trace_sessions WHERE session_id IN ({placeholders})"
|
|
476
|
+
with self._get_connection() as conn:
|
|
477
|
+
rows = conn.execute(query, session_ids).fetchall()
|
|
478
|
+
sessions = [self._row_to_session(row) for row in rows]
|
|
479
|
+
return {session.session_id: session for session in sessions}
|
|
480
|
+
|
|
481
|
+
def _conversation_id_of(self, session: TraceSession) -> str:
|
|
482
|
+
return session.conversation_session_id or session.session_id
|
|
483
|
+
|
|
484
|
+
def query_conversations(
|
|
485
|
+
self,
|
|
486
|
+
limit: int = 20,
|
|
487
|
+
status: Optional[str] = None,
|
|
488
|
+
include_hidden: bool = False,
|
|
489
|
+
main_loop_only: bool = False,
|
|
490
|
+
) -> List[Dict[str, Any]]:
|
|
491
|
+
"""按 conversation_session_id 聚合查询会话列表(SQL 聚合)。"""
|
|
492
|
+
where_clause = ""
|
|
493
|
+
params: List[Any] = []
|
|
494
|
+
if status:
|
|
495
|
+
where_clause = "WHERE s.status = ?"
|
|
496
|
+
params.append(status)
|
|
497
|
+
|
|
498
|
+
query = f"""
|
|
499
|
+
WITH grouped AS (
|
|
500
|
+
SELECT
|
|
501
|
+
COALESCE(s.conversation_session_id, s.session_id) AS conversation_id,
|
|
502
|
+
COUNT(*) AS run_count,
|
|
503
|
+
AVG(s.total_latency_ms) AS avg_latency_ms,
|
|
504
|
+
SUM(s.total_input_tokens) AS total_input_tokens,
|
|
505
|
+
SUM(s.total_output_tokens) AS total_output_tokens,
|
|
506
|
+
MAX(COALESCE(s.end_time, s.start_time)) AS latest_end_time
|
|
507
|
+
FROM trace_sessions s
|
|
508
|
+
{where_clause}
|
|
509
|
+
GROUP BY COALESCE(s.conversation_session_id, s.session_id)
|
|
510
|
+
)
|
|
511
|
+
SELECT
|
|
512
|
+
g.conversation_id AS session_id,
|
|
513
|
+
g.run_count,
|
|
514
|
+
g.avg_latency_ms,
|
|
515
|
+
g.total_input_tokens,
|
|
516
|
+
g.total_output_tokens,
|
|
517
|
+
g.latest_end_time,
|
|
518
|
+
(
|
|
519
|
+
SELECT s2.graph_name
|
|
520
|
+
FROM trace_sessions s2
|
|
521
|
+
WHERE COALESCE(s2.conversation_session_id, s2.session_id) = g.conversation_id
|
|
522
|
+
{f"AND s2.status = ?" if status else ""}
|
|
523
|
+
ORDER BY COALESCE(s2.end_time, s2.start_time) DESC
|
|
524
|
+
LIMIT 1
|
|
525
|
+
) AS graph_name,
|
|
526
|
+
(
|
|
527
|
+
SELECT s2.status
|
|
528
|
+
FROM trace_sessions s2
|
|
529
|
+
WHERE COALESCE(s2.conversation_session_id, s2.session_id) = g.conversation_id
|
|
530
|
+
{f"AND s2.status = ?" if status else ""}
|
|
531
|
+
ORDER BY COALESCE(s2.end_time, s2.start_time) DESC
|
|
532
|
+
LIMIT 1
|
|
533
|
+
) AS last_status
|
|
534
|
+
,
|
|
535
|
+
(
|
|
536
|
+
SELECT s2.visibility
|
|
537
|
+
FROM trace_sessions s2
|
|
538
|
+
WHERE COALESCE(s2.conversation_session_id, s2.session_id) = g.conversation_id
|
|
539
|
+
{f"AND s2.status = ?" if status else ""}
|
|
540
|
+
ORDER BY COALESCE(s2.end_time, s2.start_time) DESC
|
|
541
|
+
LIMIT 1
|
|
542
|
+
) AS last_visibility,
|
|
543
|
+
(
|
|
544
|
+
SELECT s2.user_query
|
|
545
|
+
FROM trace_sessions s2
|
|
546
|
+
WHERE COALESCE(s2.conversation_session_id, s2.session_id) = g.conversation_id
|
|
547
|
+
AND s2.unit_type = 'main_loop'
|
|
548
|
+
ORDER BY s2.start_time ASC
|
|
549
|
+
LIMIT 1
|
|
550
|
+
) AS topic_query,
|
|
551
|
+
(
|
|
552
|
+
SELECT 1
|
|
553
|
+
FROM trace_sessions s2
|
|
554
|
+
WHERE COALESCE(s2.conversation_session_id, s2.session_id) = g.conversation_id
|
|
555
|
+
AND s2.unit_type = 'main_loop'
|
|
556
|
+
LIMIT 1
|
|
557
|
+
) AS has_main_loop
|
|
558
|
+
FROM grouped g
|
|
559
|
+
ORDER BY g.latest_end_time DESC
|
|
560
|
+
LIMIT ?
|
|
561
|
+
"""
|
|
562
|
+
query_params = list(params)
|
|
563
|
+
if status:
|
|
564
|
+
query_params.append(status)
|
|
565
|
+
query_params.append(status)
|
|
566
|
+
query_params.append(status)
|
|
567
|
+
query_params.append(limit)
|
|
568
|
+
|
|
569
|
+
with self._get_connection() as conn:
|
|
570
|
+
rows = conn.execute(query, query_params).fetchall()
|
|
571
|
+
items = [
|
|
572
|
+
{
|
|
573
|
+
"session_id": row["session_id"],
|
|
574
|
+
"graph_name": row["graph_name"],
|
|
575
|
+
"run_count": row["run_count"],
|
|
576
|
+
"latest_end_time": row["latest_end_time"],
|
|
577
|
+
"last_status": row["last_status"],
|
|
578
|
+
"last_visibility": row["last_visibility"] or "default",
|
|
579
|
+
"avg_latency_ms": row["avg_latency_ms"] or 0.0,
|
|
580
|
+
"total_input_tokens": int(row["total_input_tokens"] or 0),
|
|
581
|
+
"total_output_tokens": int(row["total_output_tokens"] or 0),
|
|
582
|
+
"topic_query": row["topic_query"] or "",
|
|
583
|
+
"has_main_loop": bool(row["has_main_loop"]),
|
|
584
|
+
}
|
|
585
|
+
for row in rows
|
|
586
|
+
]
|
|
587
|
+
# 过滤顺序约定:
|
|
588
|
+
# 1) main_loop_only 先收敛“会话语义”(只保留含主 run 的 conversation)
|
|
589
|
+
# 2) include_hidden 再决定是否展示 hidden 可见性项
|
|
590
|
+
if main_loop_only:
|
|
591
|
+
items = [item for item in items if item["has_main_loop"]]
|
|
592
|
+
if include_hidden:
|
|
593
|
+
return items
|
|
594
|
+
return [item for item in items if item["last_visibility"] != "hidden"]
|
|
595
|
+
|
|
596
|
+
def get_runs_by_conversation(
|
|
597
|
+
self,
|
|
598
|
+
conversation_session_id: str,
|
|
599
|
+
limit: int = 50,
|
|
600
|
+
) -> List[TraceSession]:
|
|
601
|
+
"""获取某个会话下的 runs(每条 run_id 为 TraceSession.session_id)。"""
|
|
602
|
+
query = """
|
|
603
|
+
SELECT *
|
|
604
|
+
FROM trace_sessions
|
|
605
|
+
WHERE COALESCE(conversation_session_id, session_id) = ?
|
|
606
|
+
ORDER BY COALESCE(end_time, start_time) DESC
|
|
607
|
+
LIMIT ?
|
|
608
|
+
"""
|
|
609
|
+
with self._get_connection() as conn:
|
|
610
|
+
rows = conn.execute(query, (conversation_session_id, limit)).fetchall()
|
|
611
|
+
return [self._row_to_session(row) for row in rows]
|
|
612
|
+
|
|
613
|
+
def get_span_tree(self, session_id: str) -> List[TraceSpan]:
|
|
614
|
+
"""
|
|
615
|
+
获取 session 的所有 span。
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
session_id: session 唯一标识
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
TraceSpan 列表
|
|
622
|
+
"""
|
|
623
|
+
with self._get_connection() as conn:
|
|
624
|
+
rows = conn.execute("""
|
|
625
|
+
SELECT * FROM trace_spans
|
|
626
|
+
WHERE session_id = ?
|
|
627
|
+
ORDER BY start_time
|
|
628
|
+
""", (session_id,)).fetchall()
|
|
629
|
+
|
|
630
|
+
from .models import SpanType, SpanStatus
|
|
631
|
+
|
|
632
|
+
return [
|
|
633
|
+
TraceSpan(
|
|
634
|
+
span_id=row["span_id"],
|
|
635
|
+
session_id=row["session_id"],
|
|
636
|
+
parent_span_id=row["parent_span_id"],
|
|
637
|
+
span_type=SpanType(row["span_type"]),
|
|
638
|
+
name=row["name"],
|
|
639
|
+
start_time=row["start_time"],
|
|
640
|
+
end_time=row["end_time"],
|
|
641
|
+
latency_ms=row["latency_ms"],
|
|
642
|
+
status=SpanStatus(row["status"]),
|
|
643
|
+
input_data=json.loads(row["input_json"]) if row["input_json"] else None,
|
|
644
|
+
output_data=json.loads(row["output_json"]) if row["output_json"] else None,
|
|
645
|
+
error=row["error"],
|
|
646
|
+
metadata=json.loads(row["metadata_json"]) if row["metadata_json"] else {},
|
|
647
|
+
)
|
|
648
|
+
for row in rows
|
|
649
|
+
]
|
|
650
|
+
|
|
651
|
+
def get_span_trees(self, session_ids: List[str]) -> Dict[str, List[TraceSpan]]:
|
|
652
|
+
"""
|
|
653
|
+
批量获取多个 session 的 spans,减少 N 次往返查询。
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
session_ids: session_id 列表
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
Dict[session_id, List[TraceSpan]]
|
|
660
|
+
"""
|
|
661
|
+
result: Dict[str, List[TraceSpan]] = {sid: [] for sid in session_ids}
|
|
662
|
+
if not session_ids:
|
|
663
|
+
return result
|
|
664
|
+
|
|
665
|
+
placeholders = ",".join("?" for _ in session_ids)
|
|
666
|
+
query = (
|
|
667
|
+
"SELECT * FROM trace_spans "
|
|
668
|
+
f"WHERE session_id IN ({placeholders}) "
|
|
669
|
+
"ORDER BY session_id, start_time"
|
|
670
|
+
)
|
|
671
|
+
with self._get_connection() as conn:
|
|
672
|
+
rows = conn.execute(query, session_ids).fetchall()
|
|
673
|
+
from .models import SpanType, SpanStatus
|
|
674
|
+
|
|
675
|
+
for row in rows:
|
|
676
|
+
span = TraceSpan(
|
|
677
|
+
span_id=row["span_id"],
|
|
678
|
+
session_id=row["session_id"],
|
|
679
|
+
parent_span_id=row["parent_span_id"],
|
|
680
|
+
span_type=SpanType(row["span_type"]),
|
|
681
|
+
name=row["name"],
|
|
682
|
+
start_time=row["start_time"],
|
|
683
|
+
end_time=row["end_time"],
|
|
684
|
+
latency_ms=row["latency_ms"],
|
|
685
|
+
status=SpanStatus(row["status"]),
|
|
686
|
+
input_data=json.loads(row["input_json"]) if row["input_json"] else None,
|
|
687
|
+
output_data=json.loads(row["output_json"]) if row["output_json"] else None,
|
|
688
|
+
error=row["error"],
|
|
689
|
+
metadata=json.loads(row["metadata_json"]) if row["metadata_json"] else {},
|
|
690
|
+
)
|
|
691
|
+
if span.session_id not in result:
|
|
692
|
+
result[span.session_id] = []
|
|
693
|
+
result[span.session_id].append(span)
|
|
694
|
+
return result
|
|
695
|
+
|
|
696
|
+
def cleanup_old_sessions(self, days: int = 7) -> int:
|
|
697
|
+
"""
|
|
698
|
+
清理过期的 session。
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
days: 保留天数
|
|
702
|
+
|
|
703
|
+
Returns:
|
|
704
|
+
删除的 session 数量
|
|
705
|
+
"""
|
|
706
|
+
import time
|
|
707
|
+
cutoff_time = time.time() - (days * 24 * 3600)
|
|
708
|
+
|
|
709
|
+
with self._get_connection() as conn:
|
|
710
|
+
# 删除 spans
|
|
711
|
+
conn.execute("""
|
|
712
|
+
DELETE FROM trace_spans
|
|
713
|
+
WHERE session_id IN (
|
|
714
|
+
SELECT session_id FROM trace_sessions
|
|
715
|
+
WHERE start_time < ?
|
|
716
|
+
)
|
|
717
|
+
""", (cutoff_time,))
|
|
718
|
+
|
|
719
|
+
# 删除 sessions
|
|
720
|
+
cursor = conn.execute("""
|
|
721
|
+
DELETE FROM trace_sessions WHERE start_time < ?
|
|
722
|
+
""", (cutoff_time,))
|
|
723
|
+
|
|
724
|
+
deleted_count = cursor.rowcount
|
|
725
|
+
conn.commit()
|
|
726
|
+
|
|
727
|
+
return deleted_count
|
|
728
|
+
|
|
729
|
+
def cleanup_sessions_by_retention(
|
|
730
|
+
self,
|
|
731
|
+
*,
|
|
732
|
+
failed_days: int = 180,
|
|
733
|
+
normal_days: int = 30,
|
|
734
|
+
) -> int:
|
|
735
|
+
"""
|
|
736
|
+
按状态分层清理过期 session。
|
|
737
|
+
|
|
738
|
+
保留策略:
|
|
739
|
+
- failed/interrupted:failed_days
|
|
740
|
+
- 其他状态(completed/running 等):normal_days
|
|
741
|
+
"""
|
|
742
|
+
import time
|
|
743
|
+
|
|
744
|
+
now = time.time()
|
|
745
|
+
failed_cutoff = now - (int(failed_days) * 24 * 3600)
|
|
746
|
+
normal_cutoff = now - (int(normal_days) * 24 * 3600)
|
|
747
|
+
with self._get_connection() as conn:
|
|
748
|
+
# 先删 span(级联在 sqlite 默认未开启,显式清理)
|
|
749
|
+
conn.execute(
|
|
750
|
+
"""
|
|
751
|
+
DELETE FROM trace_spans
|
|
752
|
+
WHERE session_id IN (
|
|
753
|
+
SELECT session_id
|
|
754
|
+
FROM trace_sessions
|
|
755
|
+
WHERE (status IN ('failed', 'interrupted') AND start_time < ?)
|
|
756
|
+
OR (status NOT IN ('failed', 'interrupted') AND start_time < ?)
|
|
757
|
+
)
|
|
758
|
+
""",
|
|
759
|
+
(failed_cutoff, normal_cutoff),
|
|
760
|
+
)
|
|
761
|
+
cursor = conn.execute(
|
|
762
|
+
"""
|
|
763
|
+
DELETE FROM trace_sessions
|
|
764
|
+
WHERE (status IN ('failed', 'interrupted') AND start_time < ?)
|
|
765
|
+
OR (status NOT IN ('failed', 'interrupted') AND start_time < ?)
|
|
766
|
+
""",
|
|
767
|
+
(failed_cutoff, normal_cutoff),
|
|
768
|
+
)
|
|
769
|
+
deleted_count = int(cursor.rowcount or 0)
|
|
770
|
+
conn.commit()
|
|
771
|
+
return deleted_count
|
|
772
|
+
|
|
773
|
+
def mark_sessions_interrupted(
|
|
774
|
+
self,
|
|
775
|
+
*,
|
|
776
|
+
status_filter: str = "running",
|
|
777
|
+
start_before: float,
|
|
778
|
+
new_status: str = "interrupted",
|
|
779
|
+
terminal_reason: str = "process_crash_or_timeout",
|
|
780
|
+
) -> int:
|
|
781
|
+
"""将超时 running 会话标记为 interrupted。"""
|
|
782
|
+
with self._get_connection() as conn:
|
|
783
|
+
cursor = conn.execute(
|
|
784
|
+
"""
|
|
785
|
+
UPDATE trace_sessions
|
|
786
|
+
SET status = ?, terminal_reason = ?, end_time = ?
|
|
787
|
+
WHERE status = ? AND start_time < ?
|
|
788
|
+
""",
|
|
789
|
+
(new_status, terminal_reason, time.time(), status_filter, start_before),
|
|
790
|
+
)
|
|
791
|
+
conn.commit()
|
|
792
|
+
return cursor.rowcount
|
|
793
|
+
|
|
794
|
+
def recover_orphan_sessions(self, timeout_seconds: int = 3600) -> int:
|
|
795
|
+
"""恢复孤儿会话(服务启动时调用)。"""
|
|
796
|
+
cutoff = time.time() - timeout_seconds
|
|
797
|
+
return self.mark_sessions_interrupted(
|
|
798
|
+
status_filter="running",
|
|
799
|
+
start_before=cutoff,
|
|
800
|
+
new_status="interrupted",
|
|
801
|
+
terminal_reason="process_crash_or_timeout",
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
805
|
+
"""返回全局聚合统计,供 API/CLI 使用。"""
|
|
806
|
+
with self._get_connection() as conn:
|
|
807
|
+
row = conn.execute(
|
|
808
|
+
"""
|
|
809
|
+
SELECT
|
|
810
|
+
COUNT(*) AS total_runs,
|
|
811
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
|
812
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
813
|
+
SUM(CASE WHEN status = 'interrupted' THEN 1 ELSE 0 END) AS interrupted,
|
|
814
|
+
AVG(total_latency_ms) AS avg_latency_ms,
|
|
815
|
+
AVG(loop_count) AS avg_steps,
|
|
816
|
+
AVG(total_input_tokens) AS avg_tokens_in,
|
|
817
|
+
AVG(total_output_tokens) AS avg_tokens_out
|
|
818
|
+
FROM trace_sessions
|
|
819
|
+
"""
|
|
820
|
+
).fetchone()
|
|
821
|
+
tool_row = conn.execute(
|
|
822
|
+
"SELECT AVG(tool_count) AS avg_tool_calls FROM v_session_summary"
|
|
823
|
+
).fetchone()
|
|
824
|
+
llm_row = conn.execute(
|
|
825
|
+
"""
|
|
826
|
+
SELECT AVG(cnt) AS avg_llm_calls FROM (
|
|
827
|
+
SELECT session_id, COUNT(*) AS cnt
|
|
828
|
+
FROM trace_spans
|
|
829
|
+
WHERE span_type = 'llm_call'
|
|
830
|
+
GROUP BY session_id
|
|
831
|
+
)
|
|
832
|
+
"""
|
|
833
|
+
).fetchone()
|
|
834
|
+
deg_row = conn.execute(
|
|
835
|
+
"""
|
|
836
|
+
SELECT
|
|
837
|
+
COUNT(*) AS total_llm,
|
|
838
|
+
SUM(CASE WHEN metadata_json LIKE '%"detected": true%' THEN 1 ELSE 0 END) AS degraded
|
|
839
|
+
FROM trace_spans
|
|
840
|
+
WHERE span_type = 'llm_call'
|
|
841
|
+
"""
|
|
842
|
+
).fetchone()
|
|
843
|
+
total_runs = int((row["total_runs"] if row else 0) or 0)
|
|
844
|
+
total_llm = int((deg_row["total_llm"] if deg_row else 0) or 0)
|
|
845
|
+
degraded = int((deg_row["degraded"] if deg_row else 0) or 0)
|
|
846
|
+
return {
|
|
847
|
+
"total_sessions": total_runs,
|
|
848
|
+
"total_runs": total_runs,
|
|
849
|
+
"completed": int((row["completed"] if row else 0) or 0),
|
|
850
|
+
"failed": int((row["failed"] if row else 0) or 0),
|
|
851
|
+
"interrupted": int((row["interrupted"] if row else 0) or 0),
|
|
852
|
+
"avg_latency_ms": round(float((row["avg_latency_ms"] if row else 0.0) or 0.0), 1),
|
|
853
|
+
"avg_steps": round(float((row["avg_steps"] if row else 0.0) or 0.0), 1),
|
|
854
|
+
"avg_tokens_in": round(float((row["avg_tokens_in"] if row else 0.0) or 0.0), 0),
|
|
855
|
+
"avg_tokens_out": round(float((row["avg_tokens_out"] if row else 0.0) or 0.0), 0),
|
|
856
|
+
"avg_tool_calls": round(float((tool_row["avg_tool_calls"] if tool_row else 0.0) or 0.0), 1),
|
|
857
|
+
"avg_llm_calls": round(
|
|
858
|
+
float(0.0 if llm_row is None or llm_row["avg_llm_calls"] is None else llm_row["avg_llm_calls"]),
|
|
859
|
+
1,
|
|
860
|
+
),
|
|
861
|
+
"degradation_rate_percent": round((degraded / total_llm) * 100, 1) if total_llm else 0.0,
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
@classmethod
|
|
865
|
+
def from_env(cls) -> "SqliteTraceStore":
|
|
866
|
+
"""创建 SqliteTraceStore。
|
|
867
|
+
|
|
868
|
+
若设置 ``AGENTX_TRACE_DB_PATH``,使用该路径;否则 ``db_path`` 为 ``None``,
|
|
869
|
+
与无参 ``SqliteTraceStore()`` 相同,经 ``resolve_agent_workspace_config()`` 使用
|
|
870
|
+
``traces_db_path``(见 ``docs/design-docs/trace/OB-D09-trace-workspace-bootstrap.md`` §3)。
|
|
871
|
+
"""
|
|
872
|
+
db_path = os.getenv("AGENTX_TRACE_DB_PATH")
|
|
873
|
+
return cls(db_path=db_path)
|