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,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
memory/memdir/agent_memory.py — Layer 4 agent 记忆路径协作者。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
负责 agent_type 安全化与 Layer 4 路径边界判断,供加载/权限/抽取链路复用。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
subagent 启动加载与 Layer 4 写权限判定都会调用本模块。
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
Phase 2 补充 prompt 注入能力(subagent 专用),抽取编排在后续 Phase。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
import logging
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
|
21
|
+
|
|
22
|
+
from langchain_agentx.workspace import AgentWorkspaceConfig
|
|
23
|
+
from langchain_agentx.workspace.config import _sanitize_agent_type
|
|
24
|
+
|
|
25
|
+
from .loader import MemoryPromptLoader
|
|
26
|
+
from .paths import is_memory_enabled
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from langchain_agentx.loop.prompt.sections import SystemPromptSection
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_SCOPE_GUIDANCE: dict[str, str] = {
|
|
33
|
+
"user": "- Since this memory is user-scope, keep learnings general since they apply across all projects",
|
|
34
|
+
"project": "- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project",
|
|
35
|
+
"local": "- Since this memory is local-scope (not checked into version control), tailor your memories to this project and machine",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class _AgentMemoryWorkspaceView:
|
|
43
|
+
memory_dir: Path
|
|
44
|
+
memory_entrypoint: Path
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class AgentMemoryExtractionContext:
|
|
49
|
+
session_id: str
|
|
50
|
+
container_type: str
|
|
51
|
+
workspace_cfg: AgentWorkspaceConfig
|
|
52
|
+
agent_type: str
|
|
53
|
+
messages: list[Any]
|
|
54
|
+
agent_id: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AgentMemoryPathPolicy:
|
|
58
|
+
"""Layer 4 路径策略协作者。"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, cfg: AgentWorkspaceConfig) -> None:
|
|
61
|
+
self._cfg = cfg
|
|
62
|
+
self._agents_root = (cfg.memory_dir / "agents").resolve()
|
|
63
|
+
|
|
64
|
+
def is_agent_memory_path(self, absolute_path: str | Path) -> bool:
|
|
65
|
+
resolved = Path(absolute_path).resolve()
|
|
66
|
+
try:
|
|
67
|
+
resolved.relative_to(self._agents_root)
|
|
68
|
+
return True
|
|
69
|
+
except ValueError:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def is_agent_memory_path_for_type(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
agent_type: str,
|
|
76
|
+
absolute_path: str | Path,
|
|
77
|
+
allowed_scopes: tuple[str, ...] = ("project", "user", "local"),
|
|
78
|
+
) -> bool:
|
|
79
|
+
resolved = Path(absolute_path).resolve()
|
|
80
|
+
sanitized = _sanitize_agent_type(agent_type)
|
|
81
|
+
for scope in allowed_scopes:
|
|
82
|
+
mem_dir = self._cfg.agent_memory_dir(sanitized, scope).resolve()
|
|
83
|
+
try:
|
|
84
|
+
resolved.relative_to(mem_dir)
|
|
85
|
+
return True
|
|
86
|
+
except ValueError:
|
|
87
|
+
continue
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def sanitize_agent_type(agent_type: str) -> str:
|
|
92
|
+
return _sanitize_agent_type(agent_type)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AgentMemoryPromptService:
|
|
96
|
+
"""Layer 4 agent memory prompt 构建协作者。"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, cfg: AgentWorkspaceConfig) -> None:
|
|
99
|
+
self._cfg = cfg
|
|
100
|
+
|
|
101
|
+
def load_agent_memory_prompt(self, *, agent_type: str, scope: str = "project") -> str | None:
|
|
102
|
+
sanitized = _sanitize_agent_type(agent_type)
|
|
103
|
+
memory_dir = self._cfg.agent_memory_dir(sanitized, scope)
|
|
104
|
+
entrypoint = self._cfg.agent_memory_entrypoint(sanitized, scope)
|
|
105
|
+
prompt = MemoryPromptLoader(
|
|
106
|
+
_AgentMemoryWorkspaceView(memory_dir=memory_dir, memory_entrypoint=entrypoint) # type: ignore[arg-type]
|
|
107
|
+
).load_memory_prompt()
|
|
108
|
+
if prompt is None:
|
|
109
|
+
return None
|
|
110
|
+
guidance = _SCOPE_GUIDANCE.get(scope)
|
|
111
|
+
if guidance:
|
|
112
|
+
return prompt + "\n\n" + guidance
|
|
113
|
+
return prompt
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AgentMemoryPromptBootstrap:
|
|
117
|
+
"""Loop 侧 Layer 4 section 注入协作者。"""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
workspace_cfg: AgentWorkspaceConfig,
|
|
123
|
+
is_subagent: bool,
|
|
124
|
+
subagent_type: str | None,
|
|
125
|
+
scope: str = "project",
|
|
126
|
+
) -> None:
|
|
127
|
+
self._workspace_cfg = workspace_cfg
|
|
128
|
+
self._is_subagent = is_subagent
|
|
129
|
+
self._subagent_type = subagent_type
|
|
130
|
+
self._scope = scope
|
|
131
|
+
|
|
132
|
+
def build_sections(self) -> list["SystemPromptSection"]:
|
|
133
|
+
if not self._is_subagent or not self._subagent_type:
|
|
134
|
+
return []
|
|
135
|
+
from langchain_agentx.loop.prompt.sections import section
|
|
136
|
+
|
|
137
|
+
service = AgentMemoryPromptService(self._workspace_cfg)
|
|
138
|
+
return [
|
|
139
|
+
section(
|
|
140
|
+
"agent_memory",
|
|
141
|
+
lambda: service.load_agent_memory_prompt(
|
|
142
|
+
agent_type=self._subagent_type or "",
|
|
143
|
+
scope=self._scope,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class AgentMemoryExtractionCoordinator:
|
|
150
|
+
"""Layer 4 agent memory 后台抽取协作者(独立于 Layer 2)。"""
|
|
151
|
+
|
|
152
|
+
def __init__(
|
|
153
|
+
self,
|
|
154
|
+
*,
|
|
155
|
+
extraction_executor: Callable[[AgentMemoryExtractionContext, list[Any]], Awaitable[None]]
|
|
156
|
+
| None = None,
|
|
157
|
+
) -> None:
|
|
158
|
+
self._executor = extraction_executor or _noop_agent_memory_extraction_executor
|
|
159
|
+
self._in_progress = False
|
|
160
|
+
self._pending_context: AgentMemoryExtractionContext | None = None
|
|
161
|
+
self._last_message_id: dict[str, str] = {}
|
|
162
|
+
self._tasks: set[asyncio.Task[None]] = set()
|
|
163
|
+
|
|
164
|
+
def trigger_from_state(
|
|
165
|
+
self,
|
|
166
|
+
*,
|
|
167
|
+
state: dict[str, Any],
|
|
168
|
+
workspace_cfg: AgentWorkspaceConfig,
|
|
169
|
+
session_id: str,
|
|
170
|
+
container_type: str,
|
|
171
|
+
agent_type: str | None,
|
|
172
|
+
) -> None:
|
|
173
|
+
if not agent_type:
|
|
174
|
+
logger.warning(
|
|
175
|
+
"skip agent memory extraction trigger: empty agent_type (container=%s)",
|
|
176
|
+
container_type,
|
|
177
|
+
)
|
|
178
|
+
return
|
|
179
|
+
context = AgentMemoryExtractionContext(
|
|
180
|
+
session_id=session_id,
|
|
181
|
+
container_type=container_type,
|
|
182
|
+
workspace_cfg=workspace_cfg,
|
|
183
|
+
agent_type=agent_type,
|
|
184
|
+
messages=list(state.get("messages") or []),
|
|
185
|
+
agent_id=state.get("agent_id"),
|
|
186
|
+
)
|
|
187
|
+
self.trigger(context)
|
|
188
|
+
|
|
189
|
+
def trigger(self, context: AgentMemoryExtractionContext) -> None:
|
|
190
|
+
try:
|
|
191
|
+
loop = asyncio.get_running_loop()
|
|
192
|
+
except RuntimeError:
|
|
193
|
+
logger.warning("skip agent memory extraction trigger: no running event loop")
|
|
194
|
+
return
|
|
195
|
+
task = loop.create_task(self.execute_extract_memories(context))
|
|
196
|
+
self._tasks.add(task)
|
|
197
|
+
task.add_done_callback(self._tasks.discard)
|
|
198
|
+
|
|
199
|
+
async def execute_extract_memories(self, context: AgentMemoryExtractionContext) -> None:
|
|
200
|
+
if not self._can_extract(context):
|
|
201
|
+
return
|
|
202
|
+
if self._in_progress:
|
|
203
|
+
self._pending_context = context
|
|
204
|
+
return
|
|
205
|
+
self._in_progress = True
|
|
206
|
+
try:
|
|
207
|
+
current = context
|
|
208
|
+
while True:
|
|
209
|
+
await self._execute_once(current)
|
|
210
|
+
if self._pending_context is None:
|
|
211
|
+
break
|
|
212
|
+
current = self._pending_context
|
|
213
|
+
self._pending_context = None
|
|
214
|
+
finally:
|
|
215
|
+
self._in_progress = False
|
|
216
|
+
|
|
217
|
+
async def drain_pending_extraction(self, timeout_ms: int = 60_000) -> None:
|
|
218
|
+
if not self._tasks:
|
|
219
|
+
return
|
|
220
|
+
try:
|
|
221
|
+
await asyncio.wait_for(
|
|
222
|
+
asyncio.gather(*list(self._tasks), return_exceptions=True),
|
|
223
|
+
timeout=timeout_ms / 1000.0,
|
|
224
|
+
)
|
|
225
|
+
except asyncio.TimeoutError:
|
|
226
|
+
logger.warning("agent memory extraction drain timed out after %sms", timeout_ms)
|
|
227
|
+
|
|
228
|
+
async def _execute_once(self, context: AgentMemoryExtractionContext) -> None:
|
|
229
|
+
new_messages = self._messages_since_cursor(context)
|
|
230
|
+
if not new_messages:
|
|
231
|
+
return
|
|
232
|
+
if self._has_agent_memory_writes_since(context, new_messages):
|
|
233
|
+
self._advance_cursor(context, new_messages)
|
|
234
|
+
return
|
|
235
|
+
await self._executor(context, new_messages)
|
|
236
|
+
self._advance_cursor(context, new_messages)
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _can_extract(context: AgentMemoryExtractionContext) -> bool:
|
|
240
|
+
return (
|
|
241
|
+
context.container_type == "subagent"
|
|
242
|
+
and bool(context.agent_type)
|
|
243
|
+
and is_memory_enabled(context.workspace_cfg)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def _messages_since_cursor(self, context: AgentMemoryExtractionContext) -> list[Any]:
|
|
247
|
+
if not context.messages:
|
|
248
|
+
return []
|
|
249
|
+
cursor = self._last_message_id.get(self._session_key(context))
|
|
250
|
+
if cursor is None:
|
|
251
|
+
return context.messages
|
|
252
|
+
for idx, message in enumerate(context.messages):
|
|
253
|
+
if self._message_id(message) == cursor:
|
|
254
|
+
return context.messages[idx + 1 :]
|
|
255
|
+
return context.messages
|
|
256
|
+
|
|
257
|
+
def _advance_cursor(self, context: AgentMemoryExtractionContext, messages: list[Any]) -> None:
|
|
258
|
+
last_id = self._message_id(messages[-1])
|
|
259
|
+
if last_id:
|
|
260
|
+
self._last_message_id[self._session_key(context)] = last_id
|
|
261
|
+
|
|
262
|
+
def _has_agent_memory_writes_since(
|
|
263
|
+
self, context: AgentMemoryExtractionContext, messages: list[Any]
|
|
264
|
+
) -> bool:
|
|
265
|
+
for message in messages:
|
|
266
|
+
calls = []
|
|
267
|
+
if isinstance(message, dict):
|
|
268
|
+
calls = list(message.get("tool_calls") or [])
|
|
269
|
+
else:
|
|
270
|
+
calls = list(getattr(message, "tool_calls", None) or [])
|
|
271
|
+
for call in calls:
|
|
272
|
+
if _is_agent_memory_write_call(call, context.workspace_cfg, context.agent_type):
|
|
273
|
+
return True
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _session_key(context: AgentMemoryExtractionContext) -> str:
|
|
278
|
+
return f"{context.container_type}:{context.session_id}:{context.agent_type}"
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def _message_id(message: Any) -> str | None:
|
|
282
|
+
if isinstance(message, dict):
|
|
283
|
+
value = message.get("id")
|
|
284
|
+
return str(value) if value else None
|
|
285
|
+
value = getattr(message, "id", None)
|
|
286
|
+
return str(value) if value else None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def _noop_agent_memory_extraction_executor(
|
|
290
|
+
context: AgentMemoryExtractionContext, messages: list[Any]
|
|
291
|
+
) -> None:
|
|
292
|
+
logger.info(
|
|
293
|
+
"agent memory extraction placeholder executor triggered session=%s agent_type=%s message_count=%s",
|
|
294
|
+
context.session_id,
|
|
295
|
+
context.agent_type,
|
|
296
|
+
len(messages),
|
|
297
|
+
)
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _is_agent_memory_write_call(
|
|
302
|
+
call: Any,
|
|
303
|
+
workspace_cfg: AgentWorkspaceConfig,
|
|
304
|
+
agent_type: str,
|
|
305
|
+
) -> bool:
|
|
306
|
+
if isinstance(call, dict):
|
|
307
|
+
name = str(call.get("name") or call.get("tool_name") or "")
|
|
308
|
+
args = call.get("args") or call.get("tool_input") or {}
|
|
309
|
+
else:
|
|
310
|
+
name = str(getattr(call, "name", "") or getattr(call, "tool_name", ""))
|
|
311
|
+
args = getattr(call, "args", None) or getattr(call, "tool_input", None) or {}
|
|
312
|
+
if name not in {"Write", "Edit"}:
|
|
313
|
+
return False
|
|
314
|
+
if not isinstance(args, dict):
|
|
315
|
+
return False
|
|
316
|
+
candidate = args.get("path") or args.get("file_path")
|
|
317
|
+
if not candidate:
|
|
318
|
+
return False
|
|
319
|
+
return is_agent_memory_path_for_type(
|
|
320
|
+
workspace_cfg,
|
|
321
|
+
agent_type,
|
|
322
|
+
candidate,
|
|
323
|
+
allowed_scopes=("project",),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
_DEFAULT_AGENT_MEMORY_COORDINATOR = AgentMemoryExtractionCoordinator()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def get_default_agent_memory_extraction_coordinator() -> AgentMemoryExtractionCoordinator:
|
|
331
|
+
return _DEFAULT_AGENT_MEMORY_COORDINATOR
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def drain_pending_agent_memory_extraction(timeout_ms: int = 60_000) -> None:
|
|
335
|
+
await _DEFAULT_AGENT_MEMORY_COORDINATOR.drain_pending_extraction(timeout_ms=timeout_ms)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def is_agent_memory_path(cfg: AgentWorkspaceConfig, absolute_path: str | Path) -> bool:
|
|
339
|
+
return AgentMemoryPathPolicy(cfg).is_agent_memory_path(absolute_path)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def is_agent_memory_path_for_type(
|
|
343
|
+
cfg: AgentWorkspaceConfig,
|
|
344
|
+
agent_type: str,
|
|
345
|
+
absolute_path: str | Path,
|
|
346
|
+
*,
|
|
347
|
+
allowed_scopes: tuple[str, ...] = ("project", "user", "local"),
|
|
348
|
+
) -> bool:
|
|
349
|
+
return AgentMemoryPathPolicy(cfg).is_agent_memory_path_for_type(
|
|
350
|
+
agent_type=agent_type,
|
|
351
|
+
absolute_path=absolute_path,
|
|
352
|
+
allowed_scopes=allowed_scopes,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def load_agent_memory_prompt(
|
|
357
|
+
cfg: AgentWorkspaceConfig,
|
|
358
|
+
*,
|
|
359
|
+
agent_type: str,
|
|
360
|
+
scope: str = "project",
|
|
361
|
+
) -> str | None:
|
|
362
|
+
return AgentMemoryPromptService(cfg).load_agent_memory_prompt(
|
|
363
|
+
agent_type=agent_type,
|
|
364
|
+
scope=scope,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
__all__ = [
|
|
369
|
+
"AgentMemoryExtractionContext",
|
|
370
|
+
"AgentMemoryExtractionCoordinator",
|
|
371
|
+
"AgentMemoryPromptBootstrap",
|
|
372
|
+
"AgentMemoryPromptService",
|
|
373
|
+
"AgentMemoryPathPolicy",
|
|
374
|
+
"drain_pending_agent_memory_extraction",
|
|
375
|
+
"get_default_agent_memory_extraction_coordinator",
|
|
376
|
+
"is_agent_memory_path",
|
|
377
|
+
"is_agent_memory_path_for_type",
|
|
378
|
+
"load_agent_memory_prompt",
|
|
379
|
+
"sanitize_agent_type",
|
|
380
|
+
]
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
memory/memdir/extractor.py — Layer 2 后台抽取协作者。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
在 loop stop 时以 fire-and-forget 触发记忆抽取,提供并发保护与 session 退出 drain。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
HookGraphWiring.stop_hooks_node() 触发 -> MemoryExtractionCoordinator 执行。
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
Phase 4 先实现门禁链、游标推进、并发与 drain;抽取写入细节使用可注入执行器。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
import logging
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Awaitable, Callable
|
|
21
|
+
|
|
22
|
+
from langchain_agentx.workspace import AgentWorkspaceConfig
|
|
23
|
+
|
|
24
|
+
from .agent_memory import is_agent_memory_path
|
|
25
|
+
from .paths import is_memory_enabled
|
|
26
|
+
from .scan import format_memory_manifest, scan_memory_files
|
|
27
|
+
from .types import HOW_TO_SAVE_SECTION, TYPES_SECTION_INDIVIDUAL, WHAT_NOT_TO_SAVE_SECTION
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
_WRITE_TOOLS = {"Write", "Edit"}
|
|
32
|
+
_READ_TOOLS = {"Read", "Grep", "Glob"}
|
|
33
|
+
_READ_ONLY_BASH_PREFIX = ("ls", "find", "grep", "cat", "stat", "wc", "head", "tail")
|
|
34
|
+
DEFAULT_EXTRACTION_WINDOW_MESSAGES = 30
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class MemoryExtractionContext:
|
|
39
|
+
session_id: str
|
|
40
|
+
container_type: str
|
|
41
|
+
workspace_cfg: AgentWorkspaceConfig
|
|
42
|
+
messages: list[Any]
|
|
43
|
+
agent_id: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MemoryExtractionCoordinator:
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
extraction_executor: Callable[[MemoryExtractionContext, list[Any]], Awaitable[None]] | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self._executor = extraction_executor or _noop_extraction_executor
|
|
53
|
+
self._in_progress = False
|
|
54
|
+
self._pending_context: MemoryExtractionContext | None = None
|
|
55
|
+
self._last_message_id: dict[str, str] = {}
|
|
56
|
+
self._tasks: set[asyncio.Task[None]] = set()
|
|
57
|
+
|
|
58
|
+
def trigger_from_state(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
state: dict[str, Any],
|
|
62
|
+
workspace_cfg: AgentWorkspaceConfig,
|
|
63
|
+
session_id: str,
|
|
64
|
+
container_type: str,
|
|
65
|
+
) -> None:
|
|
66
|
+
context = MemoryExtractionContext(
|
|
67
|
+
session_id=session_id,
|
|
68
|
+
container_type=container_type,
|
|
69
|
+
workspace_cfg=workspace_cfg,
|
|
70
|
+
messages=list(state.get("messages") or []),
|
|
71
|
+
agent_id=state.get("agent_id"),
|
|
72
|
+
)
|
|
73
|
+
self.trigger(context)
|
|
74
|
+
|
|
75
|
+
def trigger(self, context: MemoryExtractionContext) -> None:
|
|
76
|
+
try:
|
|
77
|
+
loop = asyncio.get_running_loop()
|
|
78
|
+
except RuntimeError:
|
|
79
|
+
logger.warning("skip memory extraction trigger: no running event loop")
|
|
80
|
+
return
|
|
81
|
+
task = loop.create_task(self.execute_extract_memories(context))
|
|
82
|
+
self._tasks.add(task)
|
|
83
|
+
task.add_done_callback(self._tasks.discard)
|
|
84
|
+
|
|
85
|
+
async def execute_extract_memories(self, context: MemoryExtractionContext) -> None:
|
|
86
|
+
if not self._can_extract(context):
|
|
87
|
+
return
|
|
88
|
+
if self._in_progress:
|
|
89
|
+
# 单槽覆盖语义:只保留最新上下文。
|
|
90
|
+
self._pending_context = context
|
|
91
|
+
return
|
|
92
|
+
self._in_progress = True
|
|
93
|
+
try:
|
|
94
|
+
current = context
|
|
95
|
+
while True:
|
|
96
|
+
await self._execute_once(current)
|
|
97
|
+
if self._pending_context is None:
|
|
98
|
+
break
|
|
99
|
+
current = self._pending_context
|
|
100
|
+
self._pending_context = None
|
|
101
|
+
finally:
|
|
102
|
+
self._in_progress = False
|
|
103
|
+
|
|
104
|
+
async def drain_pending_extraction(self, timeout_ms: int = 60_000) -> None:
|
|
105
|
+
if not self._tasks:
|
|
106
|
+
return
|
|
107
|
+
try:
|
|
108
|
+
await asyncio.wait_for(
|
|
109
|
+
asyncio.gather(*list(self._tasks), return_exceptions=True),
|
|
110
|
+
timeout=timeout_ms / 1000.0,
|
|
111
|
+
)
|
|
112
|
+
except asyncio.TimeoutError:
|
|
113
|
+
logger.warning("memory extraction drain timed out after %sms", timeout_ms)
|
|
114
|
+
|
|
115
|
+
async def _execute_once(self, context: MemoryExtractionContext) -> None:
|
|
116
|
+
new_messages = self._messages_since_cursor(context)
|
|
117
|
+
if not new_messages:
|
|
118
|
+
return
|
|
119
|
+
if self._has_memory_writes_since(context, new_messages):
|
|
120
|
+
self._advance_cursor(context, new_messages)
|
|
121
|
+
return
|
|
122
|
+
await self._executor(context, new_messages)
|
|
123
|
+
self._advance_cursor(context, new_messages)
|
|
124
|
+
|
|
125
|
+
def _can_extract(self, context: MemoryExtractionContext) -> bool:
|
|
126
|
+
if context.container_type not in ("interactive", "conversation"):
|
|
127
|
+
return False
|
|
128
|
+
if context.agent_id:
|
|
129
|
+
return False
|
|
130
|
+
return is_memory_enabled(context.workspace_cfg)
|
|
131
|
+
|
|
132
|
+
def _messages_since_cursor(self, context: MemoryExtractionContext) -> list[Any]:
|
|
133
|
+
if not context.messages:
|
|
134
|
+
return []
|
|
135
|
+
session_key = self._session_key(context)
|
|
136
|
+
cursor = self._last_message_id.get(session_key) or self._load_cursor(context)
|
|
137
|
+
if cursor is None:
|
|
138
|
+
return context.messages
|
|
139
|
+
for idx, message in enumerate(context.messages):
|
|
140
|
+
if self._message_id(message) == cursor:
|
|
141
|
+
return context.messages[idx + 1 :]
|
|
142
|
+
return context.messages
|
|
143
|
+
|
|
144
|
+
def _advance_cursor(self, context: MemoryExtractionContext, messages: list[Any]) -> None:
|
|
145
|
+
last_id = self._message_id(messages[-1])
|
|
146
|
+
if not last_id:
|
|
147
|
+
return
|
|
148
|
+
session_key = self._session_key(context)
|
|
149
|
+
self._last_message_id[session_key] = last_id
|
|
150
|
+
if context.container_type == "conversation":
|
|
151
|
+
cursor_path = context.workspace_cfg.sessions_dir / f"{context.session_id}.cursor"
|
|
152
|
+
cursor_path.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
cursor_path.write_text(last_id, encoding="utf-8")
|
|
154
|
+
|
|
155
|
+
def _load_cursor(self, context: MemoryExtractionContext) -> str | None:
|
|
156
|
+
if context.container_type != "conversation":
|
|
157
|
+
return None
|
|
158
|
+
path = context.workspace_cfg.sessions_dir / f"{context.session_id}.cursor"
|
|
159
|
+
if not path.exists():
|
|
160
|
+
return None
|
|
161
|
+
return path.read_text(encoding="utf-8").strip() or None
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _session_key(context: MemoryExtractionContext) -> str:
|
|
165
|
+
return f"{context.container_type}:{context.session_id}"
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _message_id(message: Any) -> str | None:
|
|
169
|
+
if isinstance(message, dict):
|
|
170
|
+
value = message.get("id")
|
|
171
|
+
return str(value) if value else None
|
|
172
|
+
value = getattr(message, "id", None)
|
|
173
|
+
return str(value) if value else None
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _has_memory_writes_since(context: MemoryExtractionContext, messages: list[Any]) -> bool:
|
|
177
|
+
for message in messages:
|
|
178
|
+
calls = []
|
|
179
|
+
if isinstance(message, dict):
|
|
180
|
+
calls = list(message.get("tool_calls") or [])
|
|
181
|
+
else:
|
|
182
|
+
calls = list(getattr(message, "tool_calls", None) or [])
|
|
183
|
+
for call in calls:
|
|
184
|
+
if _is_memory_write_call(call, context.workspace_cfg):
|
|
185
|
+
return True
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def create_auto_mem_can_use_tool(memory_dir: Path) -> Callable[[str, dict[str, Any]], bool]:
|
|
190
|
+
resolved_memory_dir = memory_dir.resolve()
|
|
191
|
+
|
|
192
|
+
def _can_use(tool_name: str, tool_input: dict[str, Any]) -> bool:
|
|
193
|
+
if tool_name in _READ_TOOLS:
|
|
194
|
+
return True
|
|
195
|
+
if tool_name == "Bash":
|
|
196
|
+
command = str(tool_input.get("command") or "").strip()
|
|
197
|
+
return command.startswith(_READ_ONLY_BASH_PREFIX)
|
|
198
|
+
if tool_name in _WRITE_TOOLS:
|
|
199
|
+
candidate = tool_input.get("path") or tool_input.get("file_path")
|
|
200
|
+
if candidate is None:
|
|
201
|
+
return False
|
|
202
|
+
try:
|
|
203
|
+
Path(candidate).resolve().relative_to(resolved_memory_dir)
|
|
204
|
+
return True
|
|
205
|
+
except (ValueError, OSError, RuntimeError, TypeError):
|
|
206
|
+
return False
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
return _can_use
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def _noop_extraction_executor(
|
|
213
|
+
context: MemoryExtractionContext, messages: list[Any]
|
|
214
|
+
) -> None:
|
|
215
|
+
prompt = build_extract_prompt(
|
|
216
|
+
workspace_cfg=context.workspace_cfg,
|
|
217
|
+
window_messages=min(DEFAULT_EXTRACTION_WINDOW_MESSAGES, len(messages)),
|
|
218
|
+
)
|
|
219
|
+
logger.info(
|
|
220
|
+
"memory extraction placeholder executor triggered session=%s container=%s prompt_chars=%s message_count=%s",
|
|
221
|
+
context.session_id,
|
|
222
|
+
context.container_type,
|
|
223
|
+
len(prompt),
|
|
224
|
+
len(messages),
|
|
225
|
+
)
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
_DEFAULT_COORDINATOR = MemoryExtractionCoordinator()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
async def execute_extract_memories(context: MemoryExtractionContext) -> None:
|
|
233
|
+
await _DEFAULT_COORDINATOR.execute_extract_memories(context)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
async def drain_pending_extraction(timeout_ms: int = 60_000) -> None:
|
|
237
|
+
await _DEFAULT_COORDINATOR.drain_pending_extraction(timeout_ms=timeout_ms)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _is_memory_write_call(call: Any, workspace_cfg: AgentWorkspaceConfig) -> bool:
|
|
241
|
+
if isinstance(call, dict):
|
|
242
|
+
name = str(call.get("name") or call.get("tool_name") or "")
|
|
243
|
+
args = call.get("args") or call.get("tool_input") or {}
|
|
244
|
+
else:
|
|
245
|
+
name = str(getattr(call, "name", "") or getattr(call, "tool_name", ""))
|
|
246
|
+
args = getattr(call, "args", None) or getattr(call, "tool_input", None) or {}
|
|
247
|
+
if name not in _WRITE_TOOLS:
|
|
248
|
+
return False
|
|
249
|
+
if not isinstance(args, dict):
|
|
250
|
+
return False
|
|
251
|
+
candidate = args.get("path") or args.get("file_path")
|
|
252
|
+
if not candidate:
|
|
253
|
+
return False
|
|
254
|
+
try:
|
|
255
|
+
resolved = Path(candidate).resolve()
|
|
256
|
+
if is_agent_memory_path(workspace_cfg, resolved):
|
|
257
|
+
return True
|
|
258
|
+
resolved.relative_to(workspace_cfg.memory_dir.resolve())
|
|
259
|
+
return True
|
|
260
|
+
except (ValueError, OSError, RuntimeError, TypeError):
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def build_extract_prompt(
|
|
265
|
+
*,
|
|
266
|
+
workspace_cfg: AgentWorkspaceConfig,
|
|
267
|
+
window_messages: int = DEFAULT_EXTRACTION_WINDOW_MESSAGES,
|
|
268
|
+
) -> str:
|
|
269
|
+
headers = scan_memory_files(workspace_cfg.memory_dir)
|
|
270
|
+
manifest = format_memory_manifest(headers)
|
|
271
|
+
opener = (
|
|
272
|
+
"You are now acting as the memory extraction subagent. Analyze the most recent\n"
|
|
273
|
+
f"~{window_messages} messages above and use them to update your persistent memory systems.\n\n"
|
|
274
|
+
"Available tools: Read, Grep, Glob, read-only Bash (ls/find/cat/stat/wc/head/tail\n"
|
|
275
|
+
"and similar), and Edit/Write for paths inside the memory directory only.\n"
|
|
276
|
+
"Bash rm is not permitted. All other tools — MCP, Agent, write-capable Bash,\n"
|
|
277
|
+
"etc — will be denied.\n\n"
|
|
278
|
+
"You have a limited turn budget. Edit requires a prior Read of the same file,\n"
|
|
279
|
+
"so the efficient strategy is: turn 1 — issue all Read calls in parallel for\n"
|
|
280
|
+
"every file you might update; turn 2 — issue all Write/Edit calls in parallel.\n"
|
|
281
|
+
"Do not interleave reads and writes across multiple turns.\n"
|
|
282
|
+
)
|
|
283
|
+
existing = ""
|
|
284
|
+
if manifest:
|
|
285
|
+
existing = (
|
|
286
|
+
"\n\n## Existing memory files\n\n"
|
|
287
|
+
f"{manifest}\n\n"
|
|
288
|
+
"Check this list before writing — update an existing file rather than creating\n"
|
|
289
|
+
"a duplicate."
|
|
290
|
+
)
|
|
291
|
+
body = (
|
|
292
|
+
"If the user explicitly asks you to remember something, save it immediately as\n"
|
|
293
|
+
"whichever type fits best. If they ask you to forget something, find and remove\n"
|
|
294
|
+
"the relevant entry.\n\n"
|
|
295
|
+
f"{TYPES_SECTION_INDIVIDUAL}\n\n"
|
|
296
|
+
f"{WHAT_NOT_TO_SAVE_SECTION}\n\n"
|
|
297
|
+
f"{HOW_TO_SAVE_SECTION}"
|
|
298
|
+
)
|
|
299
|
+
return opener + existing + "\n\n" + body
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
__all__ = [
|
|
303
|
+
"MemoryExtractionContext",
|
|
304
|
+
"MemoryExtractionCoordinator",
|
|
305
|
+
"build_extract_prompt",
|
|
306
|
+
"create_auto_mem_can_use_tool",
|
|
307
|
+
"drain_pending_extraction",
|
|
308
|
+
"execute_extract_memories",
|
|
309
|
+
]
|