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,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
task_runtime/output/sink.py — 任务持续输出(§10.4 TaskOutputSink)
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
提供与终态 notification 正交的 **append-only 输出缓冲**,支持按 task_id 追加与
|
|
6
|
+
按字节 offset 续读,供排障与(未来)CLI 回放。
|
|
7
|
+
|
|
8
|
+
与 CC 对比:
|
|
9
|
+
CC 将任务 stdout 等落盘到 workspace 路径;本仓库先提供进程内 **InMemory** 实现,
|
|
10
|
+
上限策略与 ``never_truncate`` 类语义对齐为「超上限丢弃头部、保留尾部」。
|
|
11
|
+
|
|
12
|
+
边界:
|
|
13
|
+
- 不负责持久化跨进程;跨重启持久化由调用方换用基于文件/SQLite 的 Sink 实现(未来)。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from threading import RLock
|
|
19
|
+
from typing import Optional, Protocol, runtime_checkable
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class TaskOutputSink(Protocol):
|
|
24
|
+
"""Append-only task output; ``offset`` in :meth:`read_from` is **byte** offset."""
|
|
25
|
+
|
|
26
|
+
def append(self, task_id: str, chunk: str) -> None: ...
|
|
27
|
+
|
|
28
|
+
def read_from(self, task_id: str, offset: int = 0) -> tuple[str, int]:
|
|
29
|
+
"""Return ``(decoded_text_slice, next_byte_offset)`` for the full buffer length."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InMemoryTaskOutputSink:
|
|
34
|
+
"""进程内输出缓冲:UTF-8 字节存储;超上限时丢弃最旧字节(保留尾部)。"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, *, max_bytes_per_task: Optional[int] = 1_048_576) -> None:
|
|
37
|
+
self._max_bytes_per_task = max_bytes_per_task
|
|
38
|
+
self._lock = RLock()
|
|
39
|
+
self._buffers: dict[str, bytearray] = {}
|
|
40
|
+
|
|
41
|
+
def append(self, task_id: str, chunk: str) -> None:
|
|
42
|
+
if not chunk:
|
|
43
|
+
return
|
|
44
|
+
raw = chunk.encode("utf-8")
|
|
45
|
+
with self._lock:
|
|
46
|
+
buf = self._buffers.setdefault(task_id, bytearray())
|
|
47
|
+
buf.extend(raw)
|
|
48
|
+
max_b = self._max_bytes_per_task
|
|
49
|
+
if max_b is not None and len(buf) > max_b:
|
|
50
|
+
del buf[: len(buf) - max_b]
|
|
51
|
+
|
|
52
|
+
def read_from(self, task_id: str, offset: int = 0) -> tuple[str, int]:
|
|
53
|
+
with self._lock:
|
|
54
|
+
buf = self._buffers.get(task_id)
|
|
55
|
+
if not buf:
|
|
56
|
+
return "", 0
|
|
57
|
+
data = bytes(buf)
|
|
58
|
+
total = len(data)
|
|
59
|
+
if offset < 0:
|
|
60
|
+
offset = 0
|
|
61
|
+
if offset >= total:
|
|
62
|
+
return "", total
|
|
63
|
+
tail = data[offset:]
|
|
64
|
+
return tail.decode("utf-8", errors="replace"), total
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
task_runtime/policy/withhold_visibility.py — withhold 与 task 事件可见性(§10.6)
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
冻结 **可测试的策略占位**:withhold(输出截断窗口)期间,task 通知与输出 sink
|
|
6
|
+
与 loop 恢复的相对顺序。默认行为保守:不延迟终态通知、输出始终写入 sink。
|
|
7
|
+
|
|
8
|
+
与 loop 的关系:
|
|
9
|
+
具体注入顺序由 ``create_loop_agent`` / controller 实现;本模块仅提供策略值对象,
|
|
10
|
+
供未来 loop 与 adapter 读取,避免魔法布尔散落在多处。
|
|
11
|
+
|
|
12
|
+
边界:
|
|
13
|
+
- 本阶段不修改 loop 默认行为;未传入策略时等价于默认实例。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class WithholdTaskEventPolicy:
|
|
23
|
+
"""withhold 窗口内 task 相关事件的可见性策略(§10.6 契约占位)。"""
|
|
24
|
+
|
|
25
|
+
delay_terminal_task_notification: bool = False
|
|
26
|
+
"""若为 True,task 终态通知延迟到 withhold 解除后合并注入(未来)。"""
|
|
27
|
+
|
|
28
|
+
write_task_output_sink_during_withhold: bool = True
|
|
29
|
+
"""若为 True,withhold 期间仍向 §10.4 sink 追加(便于排障)。"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
DEFAULT_WITHHOLD_TASK_EVENT_POLICY = WithholdTaskEventPolicy()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
task_runtime/queue/in_memory.py — TaskCommandQueue 内存实现
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
基于 list 的任务通知队列;采用 RLock 保证基础并发安全。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from threading import RLock
|
|
11
|
+
|
|
12
|
+
from ..core.interfaces import TaskCommandQueue
|
|
13
|
+
from ..core.types import (
|
|
14
|
+
QueuePriority,
|
|
15
|
+
TaskNotificationEnvelope,
|
|
16
|
+
TaskScope,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_PRIORITY_ORDER = {
|
|
21
|
+
QueuePriority.NOW: 0,
|
|
22
|
+
QueuePriority.NEXT: 1,
|
|
23
|
+
QueuePriority.LATER: 2,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InMemoryTaskCommandQueue(TaskCommandQueue):
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._lock = RLock()
|
|
30
|
+
self._queue: list[TaskNotificationEnvelope] = []
|
|
31
|
+
|
|
32
|
+
def enqueue(self, env: TaskNotificationEnvelope) -> None:
|
|
33
|
+
with self._lock:
|
|
34
|
+
self._queue.append(env)
|
|
35
|
+
|
|
36
|
+
def peek_for_scope(
|
|
37
|
+
self, scope: TaskScope, max_priority: QueuePriority
|
|
38
|
+
) -> list[TaskNotificationEnvelope]:
|
|
39
|
+
threshold = _PRIORITY_ORDER[max_priority]
|
|
40
|
+
with self._lock:
|
|
41
|
+
filtered = [
|
|
42
|
+
item
|
|
43
|
+
for item in self._queue
|
|
44
|
+
if item.scope.agent_id == scope.agent_id
|
|
45
|
+
and _PRIORITY_ORDER[item.priority] <= threshold
|
|
46
|
+
]
|
|
47
|
+
filtered.sort(key=lambda item: _PRIORITY_ORDER[item.priority])
|
|
48
|
+
return filtered
|
|
49
|
+
|
|
50
|
+
def remove(self, command_ids: list[str]) -> None:
|
|
51
|
+
if not command_ids:
|
|
52
|
+
return
|
|
53
|
+
ids = set(command_ids)
|
|
54
|
+
with self._lock:
|
|
55
|
+
self._queue = [item for item in self._queue if item.command_id not in ids]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
task_runtime/skill_prefetch/attachments.py — Skill 附件构造器
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
将 `SkillHint` 领域对象映射为 loop 可注入的 attachment message(dict)。
|
|
6
|
+
|
|
7
|
+
对照:
|
|
8
|
+
对齐 CC 中 skill_listing / dynamic_skill attachment 的角色,
|
|
9
|
+
但复用本工程 `task_event_reserve` + dedup 契约。
|
|
10
|
+
|
|
11
|
+
实现约束:
|
|
12
|
+
- 采用 OOP 构造器,不在外层散落函数;
|
|
13
|
+
- 兼容现有 dedup:复用 `metadata.task_notification.command_id`。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from .models import SkillHint
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SkillAttachmentBuilder:
|
|
24
|
+
def build(self, hint: SkillHint) -> dict[str, Any]:
|
|
25
|
+
return {
|
|
26
|
+
"role": "user",
|
|
27
|
+
"content": [{"type": "text", "text": self._build_text(hint)}],
|
|
28
|
+
"metadata": {
|
|
29
|
+
# Reuse task_notification.command_id for existing dedup filter.
|
|
30
|
+
"task_notification": {"command_id": hint.command_id},
|
|
31
|
+
"skill_notification": {
|
|
32
|
+
"command_id": hint.command_id,
|
|
33
|
+
"kind": hint.kind,
|
|
34
|
+
"skill_name": hint.payload.get("skill_name"),
|
|
35
|
+
"source": hint.payload.get("source", "prefetch"),
|
|
36
|
+
"provider": "skill_prefetch",
|
|
37
|
+
"turn_index": hint.payload.get("turn_index"),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def _build_text(self, hint: SkillHint) -> str:
|
|
43
|
+
if hint.kind == "skill_listing":
|
|
44
|
+
return f"[skill_listing] {hint.summary}"
|
|
45
|
+
return f"[dynamic_skill] {hint.summary}"
|
|
46
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/task_runtime/skill_prefetch/models.py — Skill 预取领域模型
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
承载 Skill 预取阶段的稳定数据对象(hint / command 元信息)。
|
|
6
|
+
|
|
7
|
+
对照:
|
|
8
|
+
对齐 CC skill discovery 的“轻量列表提示 + 动态候选提示”语义,
|
|
9
|
+
用 dataclass 固化本工程 provider 内部契约。
|
|
10
|
+
|
|
11
|
+
实现约束:
|
|
12
|
+
- 仅定义数据模型,不承载队列/注入/渲染逻辑;
|
|
13
|
+
- 字段命名保持与 loop 注入 metadata 对齐(kind/command_id/source)。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Any, Literal
|
|
21
|
+
|
|
22
|
+
from ..core.types import QueuePriority, TaskScope
|
|
23
|
+
|
|
24
|
+
SkillHintKind = Literal["skill_listing", "dynamic_skill"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class SkillHint:
|
|
29
|
+
kind: SkillHintKind
|
|
30
|
+
command_id: str
|
|
31
|
+
summary: str
|
|
32
|
+
scope: TaskScope
|
|
33
|
+
priority: QueuePriority = QueuePriority.NEXT
|
|
34
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
dedup_key: str | None = None
|
|
36
|
+
created_at: float = field(default_factory=time.time)
|
|
37
|
+
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
task_runtime/skill_prefetch/provider.py — Skill 预取队列提供者
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
在 loop 注入链中提供 skill 可见性附件(skill_listing / dynamic_skill)。
|
|
6
|
+
|
|
7
|
+
对照:
|
|
8
|
+
对齐 CC “loop 提供可见性,SkillTool 负责执行”的分层;
|
|
9
|
+
本类只输出附件批次,不执行任何 skill 内容。
|
|
10
|
+
|
|
11
|
+
实现约束:
|
|
12
|
+
- OOP 协作:技能发现、附件渲染、队列批次管理职责分离;
|
|
13
|
+
- 与 `QueuedCommandProvider` 契约对齐(build/ack/nack/has_pending/ingest);
|
|
14
|
+
- 默认提供首轮可见性的 listing 注入能力。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from threading import RLock
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
from uuid import uuid4
|
|
24
|
+
|
|
25
|
+
from langchain_agentx.workspace import resolve_agent_workspace_config
|
|
26
|
+
|
|
27
|
+
from ..core.types import QueuePriority, TaskScope
|
|
28
|
+
from ..integrations.loop_adapter import LoopInjectionBatch
|
|
29
|
+
from .attachments import SkillAttachmentBuilder
|
|
30
|
+
from .models import SkillHint
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from langchain_agentx.tools.skill.loader import SkillCommandRepository
|
|
34
|
+
|
|
35
|
+
_PRIORITY_ORDER = {
|
|
36
|
+
QueuePriority.NOW: 0,
|
|
37
|
+
QueuePriority.NEXT: 1,
|
|
38
|
+
QueuePriority.LATER: 2,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# 对齐 CC SkillTool prompt 的 listing 预算/截断语义。
|
|
42
|
+
# - 总预算:listing 最多占上下文的一小部分,避免吞噬主对话 token。
|
|
43
|
+
# - 单条上限:每个 skill 的描述做硬截断,避免个别长描述“挤掉”其它技能。
|
|
44
|
+
SKILL_BUDGET_CONTEXT_PERCENT = 0.01
|
|
45
|
+
CHARS_PER_TOKEN = 4
|
|
46
|
+
DEFAULT_CHAR_BUDGET = 8_000
|
|
47
|
+
MAX_LISTING_DESC_CHARS = 250
|
|
48
|
+
MIN_DESC_LENGTH = 20
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SkillPrefetchProvider:
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
workspace_root: str | Path = ".",
|
|
56
|
+
agent_home: str = ".langchain_agentx",
|
|
57
|
+
skills_root: str | Path | None = None,
|
|
58
|
+
repository: "SkillCommandRepository | None" = None,
|
|
59
|
+
attachment_builder: SkillAttachmentBuilder | None = None,
|
|
60
|
+
max_queue_size: int | None = None,
|
|
61
|
+
ttl_sec: float | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
del max_queue_size, ttl_sec
|
|
64
|
+
workspace_cfg = resolve_agent_workspace_config(
|
|
65
|
+
workspace_root=workspace_root,
|
|
66
|
+
agent_home=agent_home,
|
|
67
|
+
)
|
|
68
|
+
skills_dir = (
|
|
69
|
+
Path(skills_root).expanduser().resolve()
|
|
70
|
+
if skills_root
|
|
71
|
+
else workspace_cfg.skills_dir
|
|
72
|
+
)
|
|
73
|
+
if repository is None:
|
|
74
|
+
# 延迟导入,避免 task_runtime <-> tools 初始化循环依赖。
|
|
75
|
+
from langchain_agentx.tools.skill.loader import SkillCommandRepository
|
|
76
|
+
|
|
77
|
+
repository = SkillCommandRepository(skills_dir)
|
|
78
|
+
self._repository = repository
|
|
79
|
+
self._attachment_builder = attachment_builder or SkillAttachmentBuilder()
|
|
80
|
+
self._lock = RLock()
|
|
81
|
+
self._queue: list[SkillHint] = []
|
|
82
|
+
self._reserved_batches: dict[str, list[SkillHint]] = {}
|
|
83
|
+
# 可见 listing 的 (name, description, when_to_use) 签名,用于幂等刷新
|
|
84
|
+
self._listing_signature_by_scope: dict[str, tuple[tuple[str, str, str], ...]] = {}
|
|
85
|
+
|
|
86
|
+
def ingest_skill_items(
|
|
87
|
+
self,
|
|
88
|
+
*,
|
|
89
|
+
scope: TaskScope,
|
|
90
|
+
items: list[dict],
|
|
91
|
+
priority: QueuePriority = QueuePriority.NEXT,
|
|
92
|
+
) -> int:
|
|
93
|
+
inserted = 0
|
|
94
|
+
for idx, item in enumerate(items):
|
|
95
|
+
skill_id = str(item.get("skill_id") or f"skill_{idx}")
|
|
96
|
+
summary = str(item.get("summary") or "").strip() or f"skill {skill_id}"
|
|
97
|
+
hint = SkillHint(
|
|
98
|
+
kind="dynamic_skill",
|
|
99
|
+
command_id=f"skill_{skill_id}",
|
|
100
|
+
summary=summary,
|
|
101
|
+
scope=scope,
|
|
102
|
+
priority=priority,
|
|
103
|
+
payload=dict(item),
|
|
104
|
+
dedup_key=f"skill:{scope.agent_id or 'main'}:{skill_id}",
|
|
105
|
+
)
|
|
106
|
+
self.enqueue_hint(hint)
|
|
107
|
+
inserted += 1
|
|
108
|
+
return inserted
|
|
109
|
+
|
|
110
|
+
def ingest_from_skill_metadata(
|
|
111
|
+
self,
|
|
112
|
+
*,
|
|
113
|
+
scope: TaskScope,
|
|
114
|
+
skills_metadata: list[dict],
|
|
115
|
+
priority: QueuePriority = QueuePriority.NEXT,
|
|
116
|
+
) -> int:
|
|
117
|
+
items: list[dict] = []
|
|
118
|
+
for skill in skills_metadata:
|
|
119
|
+
skill_id = str(skill.get("name") or "").strip()
|
|
120
|
+
if not skill_id:
|
|
121
|
+
continue
|
|
122
|
+
items.append(
|
|
123
|
+
{
|
|
124
|
+
"skill_id": skill_id,
|
|
125
|
+
"summary": str(skill.get("description") or "").strip() or f"skill {skill_id}",
|
|
126
|
+
"path": skill.get("path"),
|
|
127
|
+
"source": skill.get("source", "prefetch"),
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
return self.ingest_skill_items(scope=scope, items=items, priority=priority)
|
|
131
|
+
|
|
132
|
+
def enqueue_hint(self, hint: SkillHint) -> None:
|
|
133
|
+
with self._lock:
|
|
134
|
+
if hint.dedup_key:
|
|
135
|
+
scope_id = self._scope_id(hint.scope)
|
|
136
|
+
self._queue = [
|
|
137
|
+
item
|
|
138
|
+
for item in self._queue
|
|
139
|
+
if not (
|
|
140
|
+
self._scope_id(item.scope) == scope_id
|
|
141
|
+
and item.dedup_key == hint.dedup_key
|
|
142
|
+
)
|
|
143
|
+
]
|
|
144
|
+
self._queue.append(hint)
|
|
145
|
+
|
|
146
|
+
def build_batch(
|
|
147
|
+
self,
|
|
148
|
+
scope: TaskScope,
|
|
149
|
+
max_priority: QueuePriority = QueuePriority.NEXT,
|
|
150
|
+
*,
|
|
151
|
+
limit: int = 8,
|
|
152
|
+
) -> LoopInjectionBatch:
|
|
153
|
+
# 仅把“当前 scope 且优先级满足阈值”的 hint 打包给 loop。
|
|
154
|
+
# 被选中的 hint 会先从队列移除并进入 reserved,等待 ack/nack。
|
|
155
|
+
threshold = _PRIORITY_ORDER[max_priority]
|
|
156
|
+
if limit <= 0:
|
|
157
|
+
return LoopInjectionBatch(batch_id="", command_ids=[], attachment_messages=[])
|
|
158
|
+
with self._lock:
|
|
159
|
+
selected = [
|
|
160
|
+
item
|
|
161
|
+
for item in self._queue
|
|
162
|
+
if self._scope_id(item.scope) == self._scope_id(scope)
|
|
163
|
+
and _PRIORITY_ORDER[item.priority] <= threshold
|
|
164
|
+
]
|
|
165
|
+
selected.sort(key=lambda item: _PRIORITY_ORDER[item.priority])
|
|
166
|
+
selected = selected[:limit]
|
|
167
|
+
if not selected:
|
|
168
|
+
return LoopInjectionBatch(batch_id="", command_ids=[], attachment_messages=[])
|
|
169
|
+
|
|
170
|
+
selected_ids = {item.command_id for item in selected}
|
|
171
|
+
self._queue = [item for item in self._queue if item.command_id not in selected_ids]
|
|
172
|
+
batch_id = f"spbatch_{uuid4().hex[:10]}"
|
|
173
|
+
self._reserved_batches[batch_id] = selected
|
|
174
|
+
return LoopInjectionBatch(
|
|
175
|
+
batch_id=batch_id,
|
|
176
|
+
command_ids=[item.command_id for item in selected],
|
|
177
|
+
attachment_messages=[
|
|
178
|
+
self._attachment_builder.build(item) for item in selected
|
|
179
|
+
],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def ack_batch(self, batch: LoopInjectionBatch) -> None:
|
|
183
|
+
if not batch.batch_id:
|
|
184
|
+
return
|
|
185
|
+
with self._lock:
|
|
186
|
+
self._reserved_batches.pop(batch.batch_id, None)
|
|
187
|
+
|
|
188
|
+
def nack_batch(self, batch: LoopInjectionBatch, *, requeue: bool = True) -> None:
|
|
189
|
+
if not batch.batch_id:
|
|
190
|
+
return
|
|
191
|
+
with self._lock:
|
|
192
|
+
reserved = self._reserved_batches.pop(batch.batch_id, None)
|
|
193
|
+
if reserved and requeue:
|
|
194
|
+
self._queue.extend(reserved)
|
|
195
|
+
|
|
196
|
+
def has_pending(
|
|
197
|
+
self, scope: TaskScope, max_priority: QueuePriority = QueuePriority.NEXT
|
|
198
|
+
) -> bool:
|
|
199
|
+
threshold = _PRIORITY_ORDER[max_priority]
|
|
200
|
+
with self._lock:
|
|
201
|
+
return any(
|
|
202
|
+
self._scope_id(item.scope) == self._scope_id(scope)
|
|
203
|
+
and _PRIORITY_ORDER[item.priority] <= threshold
|
|
204
|
+
for item in self._queue
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def ingest_from_messages(self, *, scope: TaskScope, messages: list) -> int:
|
|
208
|
+
# v1:仅触发 listing 刷新(幂等),不从 messages 抽取动态候选。
|
|
209
|
+
# v2 特性:基于 messages 做 dynamic_skill 候选提取,待专项设计后启用。
|
|
210
|
+
del messages
|
|
211
|
+
hint = self._build_listing_hint(scope=scope)
|
|
212
|
+
if hint is None:
|
|
213
|
+
return 0
|
|
214
|
+
self.enqueue_hint(hint)
|
|
215
|
+
return 1
|
|
216
|
+
|
|
217
|
+
def _build_listing_hint(self, *, scope: TaskScope) -> SkillHint | None:
|
|
218
|
+
# 1) 从仓库读取所有命令并做可见性过滤(description/when_to_use/disable-model-invocation)。
|
|
219
|
+
# 2) 通过签名判断是否变化,未变化则不重复注入(减少上下文噪声)。
|
|
220
|
+
# 3) 生成“仅 metadata”的 listing 文本,正文由 skill 工具按需加载。
|
|
221
|
+
commands = self._repository.list_commands()
|
|
222
|
+
if not commands:
|
|
223
|
+
return None
|
|
224
|
+
ordered = sorted((c for c in commands if c.name), key=lambda c: c.name)
|
|
225
|
+
visible = [c for c in ordered if c.visible_in_slash_command_tool_listing()]
|
|
226
|
+
if not visible:
|
|
227
|
+
return None
|
|
228
|
+
signature = tuple(
|
|
229
|
+
(c.name, (c.description or "").strip(), (c.when_to_use or "").strip()) for c in visible
|
|
230
|
+
)
|
|
231
|
+
scope_id = self._scope_id(scope)
|
|
232
|
+
if self._listing_signature_by_scope.get(scope_id) == signature:
|
|
233
|
+
return None
|
|
234
|
+
self._listing_signature_by_scope[scope_id] = signature
|
|
235
|
+
|
|
236
|
+
# CC 对齐:listing 仅暴露 metadata,正文通过 skill 工具按需加载;
|
|
237
|
+
# 同时按上下文预算进行长度控制,避免 listing 吃掉过多上下文。
|
|
238
|
+
header = (
|
|
239
|
+
"Available skills (metadata only — call the `skill` tool with `skill_name` "
|
|
240
|
+
"to load full SKILL.md):"
|
|
241
|
+
)
|
|
242
|
+
summary = self._format_listing_within_budget(visible, header=header)
|
|
243
|
+
|
|
244
|
+
return SkillHint(
|
|
245
|
+
kind="skill_listing",
|
|
246
|
+
command_id=f"skill_listing_{scope_id}_{len(visible)}",
|
|
247
|
+
summary=summary,
|
|
248
|
+
scope=scope,
|
|
249
|
+
priority=QueuePriority.NEXT,
|
|
250
|
+
payload={"skill_count": len(visible), "source": "prefetch"},
|
|
251
|
+
dedup_key=f"skill_listing:{scope_id}",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def _scope_id(scope: TaskScope) -> str:
|
|
256
|
+
return scope.agent_id or "main"
|
|
257
|
+
|
|
258
|
+
def _get_char_budget(self, context_window_tokens: int | None = None) -> int:
|
|
259
|
+
# 预算优先级:
|
|
260
|
+
# 1) 显式环境变量(便于压测/灰度);
|
|
261
|
+
# 2) 模型上下文窗口推导;
|
|
262
|
+
# 3) 默认常量(与 CC 默认一致量级)。
|
|
263
|
+
raw = os.getenv("SLASH_COMMAND_TOOL_CHAR_BUDGET")
|
|
264
|
+
if raw:
|
|
265
|
+
try:
|
|
266
|
+
value = int(raw)
|
|
267
|
+
except ValueError:
|
|
268
|
+
value = 0
|
|
269
|
+
if value > 0:
|
|
270
|
+
return value
|
|
271
|
+
if context_window_tokens and context_window_tokens > 0:
|
|
272
|
+
return int(context_window_tokens * CHARS_PER_TOKEN * SKILL_BUDGET_CONTEXT_PERCENT)
|
|
273
|
+
return DEFAULT_CHAR_BUDGET
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def _truncate(text: str, max_len: int) -> str:
|
|
277
|
+
if max_len <= 0:
|
|
278
|
+
return ""
|
|
279
|
+
if len(text) <= max_len:
|
|
280
|
+
return text
|
|
281
|
+
if max_len == 1:
|
|
282
|
+
return "…"
|
|
283
|
+
return text[: max_len - 1] + "…"
|
|
284
|
+
|
|
285
|
+
def _command_description(self, cmd) -> str:
|
|
286
|
+
# CC 语义:展示 description + when_to_use(若同时存在),再做单条硬截断。
|
|
287
|
+
desc = (cmd.description or "").strip().replace("\n", " ")
|
|
288
|
+
wtu = (cmd.when_to_use or "").strip().replace("\n", " ")
|
|
289
|
+
merged = f"{desc} - {wtu}" if desc and wtu else (desc or wtu)
|
|
290
|
+
return self._truncate(merged, MAX_LISTING_DESC_CHARS)
|
|
291
|
+
|
|
292
|
+
def _format_command_line(self, cmd, *, desc_max_len: int | None = None) -> str:
|
|
293
|
+
desc = self._command_description(cmd)
|
|
294
|
+
if desc_max_len is not None:
|
|
295
|
+
desc = self._truncate(desc, desc_max_len)
|
|
296
|
+
if desc:
|
|
297
|
+
return f"- `/{cmd.name}`: {desc}"
|
|
298
|
+
return f"- `/{cmd.name}`"
|
|
299
|
+
|
|
300
|
+
def _format_listing_within_budget(self, commands: list, *, header: str) -> str:
|
|
301
|
+
"""
|
|
302
|
+
在给定字符预算内渲染 skill listing。
|
|
303
|
+
|
|
304
|
+
逐级降级策略(对齐 CC 思路):
|
|
305
|
+
1) 先尝试完整条目(每条已做 MAX_LISTING_DESC_CHARS 截断);
|
|
306
|
+
2) 超预算时按“均摊描述长度”二次截断;
|
|
307
|
+
3) 若均摊长度过小,退化为 names-only;
|
|
308
|
+
4) 最后兜底:从尾部裁剪条目,保证绝不超预算。
|
|
309
|
+
"""
|
|
310
|
+
if not commands:
|
|
311
|
+
return header
|
|
312
|
+
budget = max(self._get_char_budget(), len(header) + 1)
|
|
313
|
+
full_entries = [self._format_command_line(cmd) for cmd in commands]
|
|
314
|
+
full_text = header + "\n" + "\n".join(full_entries)
|
|
315
|
+
if len(full_text) <= budget:
|
|
316
|
+
return full_text
|
|
317
|
+
|
|
318
|
+
# 仅名称+固定前缀开销:`- `/{name}`: `
|
|
319
|
+
name_overhead = sum(len(f"- `/{cmd.name}`: ") for cmd in commands)
|
|
320
|
+
newline_overhead = max(len(commands) - 1, 0)
|
|
321
|
+
available_for_desc = budget - len(header) - 1 - name_overhead - newline_overhead
|
|
322
|
+
max_desc_len = available_for_desc // len(commands)
|
|
323
|
+
|
|
324
|
+
if max_desc_len < MIN_DESC_LENGTH:
|
|
325
|
+
names_only = [f"- `/{cmd.name}`" for cmd in commands]
|
|
326
|
+
return header + "\n" + "\n".join(names_only)
|
|
327
|
+
|
|
328
|
+
trimmed_entries = [
|
|
329
|
+
self._format_command_line(cmd, desc_max_len=max_desc_len) for cmd in commands
|
|
330
|
+
]
|
|
331
|
+
trimmed_text = header + "\n" + "\n".join(trimmed_entries)
|
|
332
|
+
if len(trimmed_text) <= budget:
|
|
333
|
+
return trimmed_text
|
|
334
|
+
|
|
335
|
+
# 兜底:从末尾移除,保证不超预算
|
|
336
|
+
kept: list[str] = []
|
|
337
|
+
running_len = len(header)
|
|
338
|
+
for entry in trimmed_entries:
|
|
339
|
+
extra = 1 + len(entry)
|
|
340
|
+
if running_len + extra > budget:
|
|
341
|
+
break
|
|
342
|
+
kept.append(entry)
|
|
343
|
+
running_len += extra
|
|
344
|
+
return header + ("\n" + "\n".join(kept) if kept else "")
|