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,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
memory/session/compact_bridge.py — Session Memory 与 compact 桥接
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
在 compact 前等待提取完成,并在可用时基于 session memory 优先重建精简上下文。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
由 loop compaction 编排层调用,作为 legacy compact 之前的优先尝试路径。
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
本阶段仅实现 Phase 3 的桥接核心能力,不接入具体 loop 节点。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from langchain_core.messages import AIMessage, BaseMessage, SystemMessage, ToolMessage
|
|
23
|
+
|
|
24
|
+
from langchain_agentx.memory.session.prompts import (
|
|
25
|
+
DEFAULT_SESSION_MEMORY_TEMPLATE,
|
|
26
|
+
MAX_SECTION_LENGTH,
|
|
27
|
+
)
|
|
28
|
+
from langchain_agentx.memory.session.session_memory import SessionMemoryExtractionState
|
|
29
|
+
|
|
30
|
+
EXTRACTION_WAIT_TIMEOUT_SECONDS = 15.0
|
|
31
|
+
EXTRACTION_STALE_THRESHOLD_SECONDS = 60.0
|
|
32
|
+
MAX_SECTION_CHARS = MAX_SECTION_LENGTH * 4
|
|
33
|
+
TRUNCATION_MARKER = "[... section truncated for length ...]"
|
|
34
|
+
SUMMARY_PREFIX = "This session is being continued from saved session memory."
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def wait_for_session_memory_extraction(
|
|
38
|
+
extraction_state: SessionMemoryExtractionState,
|
|
39
|
+
*,
|
|
40
|
+
timeout_seconds: float = EXTRACTION_WAIT_TIMEOUT_SECONDS,
|
|
41
|
+
stale_threshold_seconds: float = EXTRACTION_STALE_THRESHOLD_SECONDS,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""等待进行中的提取完成(带超时与 stale 保护)。"""
|
|
44
|
+
start = time.monotonic()
|
|
45
|
+
while extraction_state.extraction_started_at is not None:
|
|
46
|
+
extraction_age = start - extraction_state.extraction_started_at
|
|
47
|
+
if extraction_age >= stale_threshold_seconds:
|
|
48
|
+
return
|
|
49
|
+
if (time.monotonic() - start) >= timeout_seconds:
|
|
50
|
+
return
|
|
51
|
+
await asyncio.sleep(0.05)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_session_memory_empty(content: str, template: str = DEFAULT_SESSION_MEMORY_TEMPLATE) -> bool:
|
|
55
|
+
"""判断 session memory 是否仍为模板占位内容。"""
|
|
56
|
+
return content.strip() == template.strip()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _split_into_sections(content: str) -> list[str]:
|
|
60
|
+
sections: list[str] = []
|
|
61
|
+
current: list[str] = []
|
|
62
|
+
for line in content.splitlines():
|
|
63
|
+
if line.startswith("# ") and current:
|
|
64
|
+
sections.append("\n".join(current).strip())
|
|
65
|
+
current = [line]
|
|
66
|
+
continue
|
|
67
|
+
current.append(line)
|
|
68
|
+
if current:
|
|
69
|
+
sections.append("\n".join(current).strip())
|
|
70
|
+
return [section for section in sections if section]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _truncate_at_line_boundary(text: str, max_chars: int) -> str:
|
|
74
|
+
if len(text) <= max_chars:
|
|
75
|
+
return text
|
|
76
|
+
cut = text.rfind("\n", 0, max_chars)
|
|
77
|
+
if cut == -1:
|
|
78
|
+
return text[:max_chars]
|
|
79
|
+
return text[:cut]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def truncate_session_memory_for_compact(content: str) -> str:
|
|
83
|
+
"""按 section 逐段截断 session memory 内容。"""
|
|
84
|
+
sections = _split_into_sections(content)
|
|
85
|
+
if not sections:
|
|
86
|
+
return content
|
|
87
|
+
|
|
88
|
+
result: list[str] = []
|
|
89
|
+
for section in sections:
|
|
90
|
+
if len(section) <= MAX_SECTION_CHARS:
|
|
91
|
+
result.append(section)
|
|
92
|
+
continue
|
|
93
|
+
truncated = _truncate_at_line_boundary(section, MAX_SECTION_CHARS)
|
|
94
|
+
result.append(f"{truncated}\n{TRUNCATION_MARKER}")
|
|
95
|
+
return "\n\n".join(result)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _is_safe_cut_point(messages: list[BaseMessage], index: int) -> bool:
|
|
99
|
+
if index >= len(messages):
|
|
100
|
+
return True
|
|
101
|
+
current = messages[index]
|
|
102
|
+
if isinstance(current, ToolMessage):
|
|
103
|
+
return False
|
|
104
|
+
if isinstance(current, AIMessage):
|
|
105
|
+
# OpenAI 对话约束:summary 之后首条消息不能是 assistant。
|
|
106
|
+
return False
|
|
107
|
+
if index > 0:
|
|
108
|
+
previous = messages[index - 1]
|
|
109
|
+
if isinstance(previous, AIMessage) and bool(previous.tool_calls):
|
|
110
|
+
return False
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def adjust_index_to_preserve_api_invariants(
|
|
115
|
+
messages: list[BaseMessage],
|
|
116
|
+
target_index: int,
|
|
117
|
+
) -> int:
|
|
118
|
+
"""向前调整截断点,避免拆分 tool_use/tool_result 对。"""
|
|
119
|
+
index = max(0, min(target_index, len(messages)))
|
|
120
|
+
while index > 0:
|
|
121
|
+
if _is_safe_cut_point(messages, index):
|
|
122
|
+
return index
|
|
123
|
+
index -= 1
|
|
124
|
+
return 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _build_summary_message(session_memory_summary: str) -> SystemMessage:
|
|
128
|
+
return SystemMessage(
|
|
129
|
+
content=(
|
|
130
|
+
f"{SUMMARY_PREFIX}\n\n"
|
|
131
|
+
f"<session_memory_summary>\n{session_memory_summary}\n</session_memory_summary>\n"
|
|
132
|
+
"Recent messages are preserved verbatim."
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def try_session_memory_compaction(
|
|
138
|
+
*,
|
|
139
|
+
messages: list[BaseMessage],
|
|
140
|
+
session_memory_path: Path,
|
|
141
|
+
template: str = DEFAULT_SESSION_MEMORY_TEMPLATE,
|
|
142
|
+
) -> list[BaseMessage] | None:
|
|
143
|
+
"""优先使用 session memory 重建消息,失败返回 None。"""
|
|
144
|
+
try:
|
|
145
|
+
memory_content = session_memory_path.read_text(encoding="utf-8")
|
|
146
|
+
except OSError:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
if is_session_memory_empty(memory_content, template):
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
if len(messages) <= 2:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
truncated_summary = truncate_session_memory_for_compact(memory_content)
|
|
156
|
+
target_index = max(1, len(messages) // 2)
|
|
157
|
+
cut_index = adjust_index_to_preserve_api_invariants(messages, target_index)
|
|
158
|
+
if cut_index == 0:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
head_system: list[BaseMessage] = []
|
|
162
|
+
remaining: list[BaseMessage] = messages
|
|
163
|
+
if isinstance(messages[0], SystemMessage):
|
|
164
|
+
head_system = [messages[0]]
|
|
165
|
+
remaining = messages[1:]
|
|
166
|
+
cut_index = max(0, cut_index - 1)
|
|
167
|
+
tail = remaining[cut_index:]
|
|
168
|
+
if not tail:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
rebuilt = [*head_system, _build_summary_message(truncated_summary), *tail]
|
|
172
|
+
return rebuilt
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class SessionMemoryCompactBridge:
|
|
177
|
+
"""Session memory compact 协作者。"""
|
|
178
|
+
|
|
179
|
+
session_memory_path: Path
|
|
180
|
+
extraction_state: SessionMemoryExtractionState
|
|
181
|
+
template: str = DEFAULT_SESSION_MEMORY_TEMPLATE
|
|
182
|
+
consecutive_failures: int = 0
|
|
183
|
+
max_failures_before_open_circuit: int = 3
|
|
184
|
+
|
|
185
|
+
async def wait_for_extraction(self) -> None:
|
|
186
|
+
await wait_for_session_memory_extraction(self.extraction_state)
|
|
187
|
+
|
|
188
|
+
def is_empty(self) -> bool:
|
|
189
|
+
try:
|
|
190
|
+
content = self.session_memory_path.read_text(encoding="utf-8")
|
|
191
|
+
except OSError:
|
|
192
|
+
return True
|
|
193
|
+
return is_session_memory_empty(content, self.template)
|
|
194
|
+
|
|
195
|
+
def try_compact(self, messages: list[BaseMessage]) -> list[BaseMessage] | None:
|
|
196
|
+
if self.consecutive_failures >= self.max_failures_before_open_circuit:
|
|
197
|
+
return None
|
|
198
|
+
compacted = try_session_memory_compaction(
|
|
199
|
+
messages=messages,
|
|
200
|
+
session_memory_path=self.session_memory_path,
|
|
201
|
+
template=self.template,
|
|
202
|
+
)
|
|
203
|
+
if compacted is None:
|
|
204
|
+
self.consecutive_failures += 1
|
|
205
|
+
return None
|
|
206
|
+
self.consecutive_failures = 0
|
|
207
|
+
return compacted
|
|
208
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
memory/session/prompts.py — Session Memory 提示词构建
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
提供会话记忆模板、提取提示词与提示词组装函数,确保 Layer 3 提取行为稳定可测。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
由 SessionMemoryManager 在后台提取前调用,生成最终 update prompt。
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
本模块仅实现 Phase 1 的 prompt 与模板能力,不承担提取调度和 compact 集成。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
MAX_SECTION_LENGTH = 2000
|
|
20
|
+
SECTION_REMINDER_THRESHOLD_RATIO = 0.8
|
|
21
|
+
|
|
22
|
+
DEFAULT_SESSION_MEMORY_TEMPLATE = """# Session Title
|
|
23
|
+
_A short and distinctive 5-10 word descriptive title for the session. Super info dense, no filler_
|
|
24
|
+
|
|
25
|
+
# Current State
|
|
26
|
+
_What is actively being worked on right now? Pending tasks not yet completed. Immediate next steps._
|
|
27
|
+
|
|
28
|
+
# Task specification
|
|
29
|
+
_What did the user ask to build? Any design decisions or other explanatory context_
|
|
30
|
+
|
|
31
|
+
# Files and Functions
|
|
32
|
+
_What are the important files? In short, what do they contain and why are they relevant?_
|
|
33
|
+
|
|
34
|
+
# Workflow
|
|
35
|
+
_What bash commands are usually run and in what order? How to interpret their output if not obvious?_
|
|
36
|
+
|
|
37
|
+
# Errors & Corrections
|
|
38
|
+
_Errors encountered and how they were fixed. What did the user correct? What approaches failed and should not be tried again?_
|
|
39
|
+
|
|
40
|
+
# Codebase and System Documentation
|
|
41
|
+
_What are the important system components? How do they work/fit together?_
|
|
42
|
+
|
|
43
|
+
# Learnings
|
|
44
|
+
_What has worked well? What has not? What to avoid? Do not duplicate items from other sections_
|
|
45
|
+
|
|
46
|
+
# Key results
|
|
47
|
+
_If the user asked a specific output such as an answer to a question, a table, or other document, repeat the exact result here_
|
|
48
|
+
|
|
49
|
+
# Worklog
|
|
50
|
+
_Step by step, what was attempted, done? Very terse summary for each step_
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
EXTRACT_SESSION_MEMORY_PROMPT = """IMPORTANT: This message and these instructions are NOT part of the actual user conversation. Do NOT include any references to "note-taking", "session notes extraction", or these update instructions in the notes content.
|
|
54
|
+
|
|
55
|
+
Based on the user conversation above (EXCLUDING this note-taking instruction message as well as system prompt, claude.md entries, or any past session summaries), update the session notes file.
|
|
56
|
+
|
|
57
|
+
The file {{notesPath}} has already been read for you. Here are its current contents:
|
|
58
|
+
<current_notes_content>
|
|
59
|
+
{{currentNotes}}
|
|
60
|
+
</current_notes_content>
|
|
61
|
+
|
|
62
|
+
Your ONLY task is to use the Edit tool to update the notes file, then stop. You can make multiple edits (update every section as needed) - make all Edit tool calls in parallel in a single message. Do not call any other tools.
|
|
63
|
+
|
|
64
|
+
CRITICAL RULES FOR EDITING:
|
|
65
|
+
- The file must maintain its exact structure with all sections, headers, and italic descriptions intact
|
|
66
|
+
-- NEVER modify, delete, or add section headers (the lines starting with '#' like # Task specification)
|
|
67
|
+
-- NEVER modify or delete the italic _section description_ lines
|
|
68
|
+
-- The italic _section descriptions_ are TEMPLATE INSTRUCTIONS that must be preserved exactly as-is
|
|
69
|
+
-- ONLY update the actual content that appears BELOW the italic _section descriptions_ within each existing section
|
|
70
|
+
-- Do NOT add any new sections, summaries, or information outside the existing structure
|
|
71
|
+
- Write DETAILED, INFO-DENSE content for each section - include specifics like file paths, function names, error messages, exact commands, technical details, etc.
|
|
72
|
+
- Keep each section under ~{{maxSectionLength}} tokens/words
|
|
73
|
+
- IMPORTANT: Always update "Current State" to reflect the most recent work
|
|
74
|
+
|
|
75
|
+
Use the Edit tool with file_path: {{notesPath}}
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def substitute_variables(template: str, variables: dict[str, str]) -> str:
|
|
80
|
+
"""单趟替换 {{variable}} 占位符,避免替换结果二次替换。"""
|
|
81
|
+
result = template
|
|
82
|
+
for key, value in variables.items():
|
|
83
|
+
result = result.replace("{{" + key + "}}", value)
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _count_words(text: str) -> int:
|
|
88
|
+
return len(re.findall(r"\S+", text))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def analyze_section_sizes(content: str) -> dict[str, int]:
|
|
92
|
+
"""按 H1 section 统计近似 token(以词数近似)。"""
|
|
93
|
+
section_sizes: dict[str, int] = {}
|
|
94
|
+
current_title: str | None = None
|
|
95
|
+
current_lines: list[str] = []
|
|
96
|
+
|
|
97
|
+
for line in content.splitlines():
|
|
98
|
+
if line.startswith("# "):
|
|
99
|
+
if current_title is not None:
|
|
100
|
+
section_sizes[current_title] = _count_words("\n".join(current_lines).strip())
|
|
101
|
+
current_title = line[2:].strip()
|
|
102
|
+
current_lines = []
|
|
103
|
+
continue
|
|
104
|
+
if current_title is not None:
|
|
105
|
+
current_lines.append(line)
|
|
106
|
+
|
|
107
|
+
if current_title is not None:
|
|
108
|
+
section_sizes[current_title] = _count_words("\n".join(current_lines).strip())
|
|
109
|
+
return section_sizes
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def generate_section_reminders(section_sizes: dict[str, int]) -> str:
|
|
113
|
+
"""为接近上限的 section 生成提醒,帮助模型主动收敛内容长度。"""
|
|
114
|
+
threshold = int(MAX_SECTION_LENGTH * SECTION_REMINDER_THRESHOLD_RATIO)
|
|
115
|
+
warnings: list[str] = []
|
|
116
|
+
for section_name, size in section_sizes.items():
|
|
117
|
+
if size >= threshold:
|
|
118
|
+
warnings.append(
|
|
119
|
+
f'- Section "{section_name}" is long ({size} words). Keep it concise and under ~{MAX_SECTION_LENGTH}.'
|
|
120
|
+
)
|
|
121
|
+
if not warnings:
|
|
122
|
+
return ""
|
|
123
|
+
return "\n".join(
|
|
124
|
+
[
|
|
125
|
+
"Section length reminders:",
|
|
126
|
+
*warnings,
|
|
127
|
+
]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def load_session_memory_template(session_memory_config_dir: Path) -> str:
|
|
132
|
+
"""优先加载用户模板,不存在时回退到默认模板。"""
|
|
133
|
+
custom = session_memory_config_dir / "template.md"
|
|
134
|
+
if custom.exists():
|
|
135
|
+
return custom.read_text(encoding="utf-8")
|
|
136
|
+
return DEFAULT_SESSION_MEMORY_TEMPLATE
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def load_session_memory_prompt(session_memory_config_dir: Path) -> str | None:
|
|
140
|
+
"""优先加载用户 prompt,不存在时返回 None。"""
|
|
141
|
+
custom = session_memory_config_dir / "prompt.md"
|
|
142
|
+
if custom.exists():
|
|
143
|
+
return custom.read_text(encoding="utf-8")
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def build_session_memory_update_prompt(
|
|
148
|
+
*,
|
|
149
|
+
current_notes: str,
|
|
150
|
+
notes_path: Path,
|
|
151
|
+
session_memory_config_dir: Path | None = None,
|
|
152
|
+
prompt_template: str | None = None,
|
|
153
|
+
) -> str:
|
|
154
|
+
"""构建最终提取 prompt,包含动态 section 长度提醒。"""
|
|
155
|
+
template = prompt_template
|
|
156
|
+
if template is None and session_memory_config_dir is not None:
|
|
157
|
+
template = load_session_memory_prompt(session_memory_config_dir)
|
|
158
|
+
if template is None:
|
|
159
|
+
template = EXTRACT_SESSION_MEMORY_PROMPT
|
|
160
|
+
|
|
161
|
+
reminders = generate_section_reminders(analyze_section_sizes(current_notes))
|
|
162
|
+
if reminders:
|
|
163
|
+
template = template + "\n\n" + reminders
|
|
164
|
+
|
|
165
|
+
return substitute_variables(
|
|
166
|
+
template=template,
|
|
167
|
+
variables={
|
|
168
|
+
"notesPath": str(notes_path),
|
|
169
|
+
"currentNotes": current_notes,
|
|
170
|
+
"maxSectionLength": str(MAX_SECTION_LENGTH),
|
|
171
|
+
},
|
|
172
|
+
)
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
memory/session/session_memory.py — Session Memory 触发与后台提取
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
提供 Layer 3 的触发判定、并发保护与后台提取编排能力。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
由 HookGraphWiring.after_model_node 调用 SessionMemoryManager.maybe_trigger(),
|
|
9
|
+
在满足阈值时异步触发会话笔记提取。
|
|
10
|
+
|
|
11
|
+
当前裁剪范围:
|
|
12
|
+
本阶段仅实现 Phase 2(触发与状态管理),不接入 loop/factory/compact。
|
|
13
|
+
_write_fallback_summary 仅用于 demo/test 验证链路通路,不替代 CC 真实提取语义。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
import time
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from langchain_agentx.loop.config import TokenEstimator
|
|
27
|
+
from langchain_agentx.memory.session.prompts import (
|
|
28
|
+
DEFAULT_SESSION_MEMORY_TEMPLATE,
|
|
29
|
+
build_session_memory_update_prompt,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
SESSION_MEMORY_INIT_TOKEN_THRESHOLD = 10_000
|
|
33
|
+
SESSION_MEMORY_UPDATE_TOKEN_THRESHOLD = 5_000
|
|
34
|
+
SESSION_MEMORY_TOOL_CALL_THRESHOLD = 3
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
ExtractionRunner = Callable[[list[Any], str, Path, Any], Awaitable[None]]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class SessionMemoryExtractionState:
|
|
42
|
+
"""提取状态追踪与并发保护(对齐 CC sequential() 语义)。"""
|
|
43
|
+
|
|
44
|
+
extraction_started_at: float | None = None
|
|
45
|
+
tokens_at_last_extraction: int = 0
|
|
46
|
+
tool_calls_at_last_extraction: int = 0
|
|
47
|
+
session_memory_initialized: bool = False
|
|
48
|
+
last_summarized_message_id: str | None = None
|
|
49
|
+
_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
|
50
|
+
|
|
51
|
+
async def run_exclusive(self, coro: Awaitable[None]) -> None:
|
|
52
|
+
"""串行执行提取任务,防止并发重入。"""
|
|
53
|
+
async with self._lock:
|
|
54
|
+
await coro
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _as_int(value: Any) -> int:
|
|
58
|
+
try:
|
|
59
|
+
return int(value)
|
|
60
|
+
except (TypeError, ValueError):
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _compute_has_tool_calls_in_last_turn(messages: list[Any]) -> bool:
|
|
65
|
+
for message in reversed(messages):
|
|
66
|
+
if isinstance(message, Mapping):
|
|
67
|
+
role = str(message.get("role", ""))
|
|
68
|
+
if role not in {"assistant", "ai"}:
|
|
69
|
+
continue
|
|
70
|
+
tool_calls = message.get("tool_calls")
|
|
71
|
+
return bool(tool_calls)
|
|
72
|
+
role = getattr(message, "type", None) or getattr(message, "role", "")
|
|
73
|
+
if role in {"ai", "assistant"}:
|
|
74
|
+
return bool(getattr(message, "tool_calls", None))
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _estimate_tool_calls_from_messages(messages: list[Any]) -> int:
|
|
79
|
+
count = 0
|
|
80
|
+
for message in messages:
|
|
81
|
+
if isinstance(message, Mapping):
|
|
82
|
+
tool_calls = message.get("tool_calls")
|
|
83
|
+
else:
|
|
84
|
+
tool_calls = getattr(message, "tool_calls", None)
|
|
85
|
+
if tool_calls:
|
|
86
|
+
count += len(tool_calls)
|
|
87
|
+
return count
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def should_extract_session_memory(
|
|
91
|
+
*,
|
|
92
|
+
token_count: int,
|
|
93
|
+
tool_call_count: int,
|
|
94
|
+
last_extract_token_count: int,
|
|
95
|
+
last_extract_tool_call_count: int,
|
|
96
|
+
has_tool_calls_in_last_turn: bool,
|
|
97
|
+
) -> bool:
|
|
98
|
+
"""对齐 CC shouldExtractMemory 的三条件逻辑。"""
|
|
99
|
+
if last_extract_token_count == 0:
|
|
100
|
+
has_met_token_threshold = token_count >= SESSION_MEMORY_INIT_TOKEN_THRESHOLD
|
|
101
|
+
else:
|
|
102
|
+
has_met_token_threshold = (
|
|
103
|
+
token_count - last_extract_token_count
|
|
104
|
+
) >= SESSION_MEMORY_UPDATE_TOKEN_THRESHOLD
|
|
105
|
+
|
|
106
|
+
if not has_met_token_threshold:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
has_met_tool_call_threshold = (
|
|
110
|
+
tool_call_count - last_extract_tool_call_count
|
|
111
|
+
) >= SESSION_MEMORY_TOOL_CALL_THRESHOLD
|
|
112
|
+
if has_met_tool_call_threshold:
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
return not has_tool_calls_in_last_turn
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def create_memory_file_can_use_tool(session_memory_path: Path) -> list[dict[str, Any]]:
|
|
119
|
+
"""生成提取 agent 的受限工具配置:仅允许 Edit 目标会话文件。"""
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
"name": "Edit",
|
|
123
|
+
"allowed_paths": [str(session_memory_path)],
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class SessionMemoryManager:
|
|
130
|
+
"""Session Memory 触发与后台提取编排器。"""
|
|
131
|
+
|
|
132
|
+
session_memory_path: Path
|
|
133
|
+
llm: Any
|
|
134
|
+
is_subagent: bool = False
|
|
135
|
+
extraction_state: SessionMemoryExtractionState = field(
|
|
136
|
+
default_factory=SessionMemoryExtractionState
|
|
137
|
+
)
|
|
138
|
+
subagent_runner: ExtractionRunner | None = None
|
|
139
|
+
_pending_tasks: set[asyncio.Task[None]] = field(default_factory=set, init=False)
|
|
140
|
+
_token_estimator: TokenEstimator = field(default_factory=TokenEstimator, init=False)
|
|
141
|
+
|
|
142
|
+
async def drain(self) -> None:
|
|
143
|
+
"""等待所有 fire-and-forget 提取任务完成(测试与 demo 用)。"""
|
|
144
|
+
if self._pending_tasks:
|
|
145
|
+
await asyncio.gather(*list(self._pending_tasks), return_exceptions=True)
|
|
146
|
+
|
|
147
|
+
def maybe_trigger(self, state: Mapping[str, Any]) -> None:
|
|
148
|
+
"""阈值满足后以 fire-and-forget 方式启动后台提取。"""
|
|
149
|
+
if self.is_subagent:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
messages_raw = state.get("messages", [])
|
|
153
|
+
messages = list(messages_raw) if isinstance(messages_raw, list) else []
|
|
154
|
+
token_count = _as_int(state.get("token_count"))
|
|
155
|
+
tool_call_count = _as_int(state.get("tool_call_count"))
|
|
156
|
+
if token_count <= 0:
|
|
157
|
+
token_count = self._token_estimator.estimate_messages_tokens(messages)
|
|
158
|
+
if tool_call_count <= 0:
|
|
159
|
+
tool_call_count = _estimate_tool_calls_from_messages(messages)
|
|
160
|
+
has_tool_calls_in_last_turn = _compute_has_tool_calls_in_last_turn(messages)
|
|
161
|
+
|
|
162
|
+
if not should_extract_session_memory(
|
|
163
|
+
token_count=token_count,
|
|
164
|
+
tool_call_count=tool_call_count,
|
|
165
|
+
last_extract_token_count=self.extraction_state.tokens_at_last_extraction,
|
|
166
|
+
last_extract_tool_call_count=self.extraction_state.tool_calls_at_last_extraction,
|
|
167
|
+
has_tool_calls_in_last_turn=has_tool_calls_in_last_turn,
|
|
168
|
+
):
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
asyncio.get_running_loop()
|
|
173
|
+
except RuntimeError:
|
|
174
|
+
logger.warning(
|
|
175
|
+
"session_memory: no running event loop, skip extraction for this turn"
|
|
176
|
+
)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
task = asyncio.ensure_future(
|
|
180
|
+
self._run_extraction(
|
|
181
|
+
messages=messages,
|
|
182
|
+
token_count=token_count,
|
|
183
|
+
tool_call_count=tool_call_count,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
self._pending_tasks.add(task)
|
|
187
|
+
task.add_done_callback(self._pending_tasks.discard)
|
|
188
|
+
|
|
189
|
+
async def _run_extraction(
|
|
190
|
+
self,
|
|
191
|
+
*,
|
|
192
|
+
messages: list[Any],
|
|
193
|
+
token_count: int,
|
|
194
|
+
tool_call_count: int,
|
|
195
|
+
) -> None:
|
|
196
|
+
async def _guarded() -> None:
|
|
197
|
+
self.extraction_state.extraction_started_at = time.monotonic()
|
|
198
|
+
try:
|
|
199
|
+
current_notes = self._read_or_init_notes()
|
|
200
|
+
prompt = build_session_memory_update_prompt(
|
|
201
|
+
current_notes=current_notes,
|
|
202
|
+
notes_path=self.session_memory_path,
|
|
203
|
+
)
|
|
204
|
+
if self.subagent_runner is not None:
|
|
205
|
+
await self.subagent_runner(
|
|
206
|
+
messages,
|
|
207
|
+
prompt,
|
|
208
|
+
self.session_memory_path,
|
|
209
|
+
self.llm,
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
self._write_fallback_summary(messages=messages, prompt=prompt)
|
|
213
|
+
self.extraction_state.tokens_at_last_extraction = max(
|
|
214
|
+
self.extraction_state.tokens_at_last_extraction,
|
|
215
|
+
token_count,
|
|
216
|
+
)
|
|
217
|
+
self.extraction_state.tool_calls_at_last_extraction = max(
|
|
218
|
+
self.extraction_state.tool_calls_at_last_extraction,
|
|
219
|
+
tool_call_count,
|
|
220
|
+
)
|
|
221
|
+
self.extraction_state.session_memory_initialized = True
|
|
222
|
+
finally:
|
|
223
|
+
self.extraction_state.extraction_started_at = None
|
|
224
|
+
|
|
225
|
+
await self.extraction_state.run_exclusive(_guarded())
|
|
226
|
+
|
|
227
|
+
def _read_or_init_notes(self) -> str:
|
|
228
|
+
if not self.session_memory_path.exists():
|
|
229
|
+
self.session_memory_path.parent.mkdir(parents=True, exist_ok=True)
|
|
230
|
+
self.session_memory_path.write_text(
|
|
231
|
+
DEFAULT_SESSION_MEMORY_TEMPLATE,
|
|
232
|
+
encoding="utf-8",
|
|
233
|
+
)
|
|
234
|
+
return DEFAULT_SESSION_MEMORY_TEMPLATE
|
|
235
|
+
return self.session_memory_path.read_text(encoding="utf-8")
|
|
236
|
+
|
|
237
|
+
def _write_fallback_summary(self, *, messages: list[Any], prompt: str) -> None:
|
|
238
|
+
"""无 subagent runner 时的兜底摘要写入,保证 session memory 可被验证。"""
|
|
239
|
+
snippets: list[str] = []
|
|
240
|
+
for msg in messages[-6:]:
|
|
241
|
+
if isinstance(msg, Mapping):
|
|
242
|
+
role = str(msg.get("role", "unknown"))
|
|
243
|
+
content = str(msg.get("content", ""))
|
|
244
|
+
else:
|
|
245
|
+
role = str(getattr(msg, "type", getattr(msg, "role", "unknown")))
|
|
246
|
+
content = str(getattr(msg, "content", ""))
|
|
247
|
+
if content:
|
|
248
|
+
snippets.append(f"- {role}: {content[:240]}")
|
|
249
|
+
body = "\n".join(snippets) if snippets else "- no recent messages captured"
|
|
250
|
+
fallback_blob = (
|
|
251
|
+
"\n\n## Auto Summary (fallback)\n"
|
|
252
|
+
"Session memory extraction runner is not configured.\n"
|
|
253
|
+
"Captured recent conversation snippets:\n"
|
|
254
|
+
f"{body}\n"
|
|
255
|
+
f"\nPrompt digest length: {len(prompt)}\n"
|
|
256
|
+
)
|
|
257
|
+
existing = self.session_memory_path.read_text(encoding="utf-8")
|
|
258
|
+
self.session_memory_path.write_text(existing + fallback_blob, encoding="utf-8")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def make_session_memory_section(session_memory_path: Path) -> Any:
|
|
262
|
+
"""构建会话记忆注入段(volatile,避免缓存过期)。"""
|
|
263
|
+
from langchain_agentx.loop.prompt.sections import volatile_section
|
|
264
|
+
|
|
265
|
+
def _load() -> str | None:
|
|
266
|
+
from langchain_agentx.memory.session.compact_bridge import is_session_memory_empty
|
|
267
|
+
|
|
268
|
+
if not session_memory_path.exists():
|
|
269
|
+
return None
|
|
270
|
+
content = session_memory_path.read_text(encoding="utf-8")
|
|
271
|
+
if not content.strip():
|
|
272
|
+
return None
|
|
273
|
+
if is_session_memory_empty(content):
|
|
274
|
+
return None
|
|
275
|
+
return f"<session_memory>\n{content}\n</session_memory>"
|
|
276
|
+
|
|
277
|
+
return volatile_section(
|
|
278
|
+
name="session_memory",
|
|
279
|
+
compute=_load,
|
|
280
|
+
reason="session memory file may change between turns",
|
|
281
|
+
)
|
|
282
|
+
|