claude-smart 0.2.45 → 0.2.46
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.
- package/.claude-plugin/marketplace.json +3 -3
- package/README.md +34 -5
- package/bin/claude-smart.js +295 -5
- package/package.json +22 -3
- package/plugin/.claude-plugin/plugin.json +4 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +22 -2
- package/plugin/commands/clear-all.md +1 -0
- package/plugin/commands/dashboard.md +1 -0
- package/plugin/commands/learn.md +1 -0
- package/plugin/commands/restart.md +1 -0
- package/plugin/commands/show.md +1 -0
- package/plugin/dashboard/app/configure/env/page.tsx +40 -14
- package/plugin/dashboard/app/configure/server/page.tsx +51 -1
- package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
- package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
- package/plugin/dashboard/lib/reflexio-client.ts +16 -0
- package/plugin/dashboard/lib/status.ts +10 -3
- package/plugin/dashboard/lib/types.ts +18 -6
- package/plugin/hooks/codex-hooks.json +0 -1
- package/plugin/opencode/assistant-buffer.ts +108 -0
- package/plugin/opencode/dist/assistant-buffer.js +96 -0
- package/plugin/opencode/dist/internal.js +12 -0
- package/plugin/opencode/dist/payload.js +85 -0
- package/plugin/opencode/dist/server.mjs +158 -0
- package/plugin/opencode/internal.ts +18 -0
- package/plugin/opencode/package.json +3 -0
- package/plugin/opencode/payload.ts +90 -0
- package/plugin/opencode/server.mts +174 -0
- package/plugin/opencode/tsconfig.json +13 -0
- package/plugin/pyproject.toml +20 -3
- package/plugin/scripts/_lib.sh +5 -1
- package/plugin/scripts/backend-service.sh +22 -2
- package/plugin/scripts/ensure-plugin-root.sh +4 -4
- package/plugin/scripts/hook_entry.sh +1 -1
- package/plugin/scripts/opencode-claude-compat +4 -0
- package/plugin/scripts/opencode-claude-compat.cmd +3 -0
- package/plugin/scripts/opencode-claude-compat.js +225 -0
- package/plugin/scripts/smart-install.sh +10 -8
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +304 -6
- package/plugin/src/claude_smart/env_config.py +12 -4
- package/plugin/src/claude_smart/events/session_start.py +26 -7
- package/plugin/src/claude_smart/events/stop.py +1 -1
- package/plugin/src/claude_smart/ids.py +1 -1
- package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
- package/plugin/src/claude_smart/runtime.py +7 -1
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +54 -30
- package/plugin/vendor/reflexio/README.md +14 -8
- package/plugin/vendor/reflexio/pyproject.toml +13 -1
- package/plugin/vendor/reflexio/reflexio/README.md +1 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
- package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
- package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
- package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
- package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +210 -50
- package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
- package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
- package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
- package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
- package/plugin/vendor/reflexio/reflexio/client/client.py +57 -13
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +238 -10
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +16 -5
- package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +198 -6
- package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +29 -20
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +2 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +52 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +88 -7
- package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +45 -32
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
- package/plugin/vendor/reflexio/reflexio/server/_auth.py +65 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +734 -85
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
- package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
- package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +266 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +26 -11
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +39 -16
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +11 -12
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +316 -178
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +84 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +30 -22
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +83 -6
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +27 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +642 -197
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +61 -104
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +628 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +978 -184
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +519 -128
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +77 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +131 -20
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +36 -53
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +243 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +121 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +60 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +229 -32
- package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
- package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
- package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
- package/scripts/setup-claude-smart.sh +8 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
- /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
"""Playbook CRUD + search methods for SQLite storage."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import sqlite3
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import Sequence
|
|
5
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
6
9
|
from typing import Any
|
|
7
10
|
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
8
13
|
from reflexio.models.api_schema.common import BlockingIssue
|
|
9
14
|
from reflexio.models.api_schema.retriever_schema import (
|
|
10
15
|
SearchAgentPlaybookRequest,
|
|
@@ -23,11 +28,16 @@ from reflexio.models.api_schema.service_schemas import (
|
|
|
23
28
|
UserPlaybook,
|
|
24
29
|
)
|
|
25
30
|
from reflexio.models.config_schema import SearchMode, SearchOptions
|
|
31
|
+
from reflexio.server.services.storage.storage_base._playbook import (
|
|
32
|
+
AGGREGATE_REASON_PREFIX,
|
|
33
|
+
)
|
|
26
34
|
|
|
27
35
|
from ._base import (
|
|
36
|
+
_TOMBSTONE_STATUS_VALUES,
|
|
28
37
|
SQLiteStorageBase,
|
|
29
38
|
_build_status_sql,
|
|
30
39
|
_effective_search_mode,
|
|
40
|
+
_epoch_now,
|
|
31
41
|
_epoch_to_iso,
|
|
32
42
|
_json_dumps,
|
|
33
43
|
_json_loads,
|
|
@@ -38,6 +48,83 @@ from ._base import (
|
|
|
38
48
|
_true_rrf_merge,
|
|
39
49
|
_vector_rank_rows,
|
|
40
50
|
)
|
|
51
|
+
from ._lineage import _GC_ELIGIBLE_STATUSES, _append_event_stmt
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _emit_hard_delete_playbook(
|
|
55
|
+
conn: sqlite3.Connection,
|
|
56
|
+
*,
|
|
57
|
+
org_id: str,
|
|
58
|
+
entity_type: str,
|
|
59
|
+
entity_id: str,
|
|
60
|
+
request_id: str,
|
|
61
|
+
actor: str = "api",
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Emit a single hard_delete lineage event for a playbook entity."""
|
|
64
|
+
_append_event_stmt(
|
|
65
|
+
conn,
|
|
66
|
+
org_id=org_id,
|
|
67
|
+
entity_type=entity_type,
|
|
68
|
+
entity_id=entity_id,
|
|
69
|
+
op="hard_delete",
|
|
70
|
+
prov="wasInvalidatedBy",
|
|
71
|
+
source_ids=[],
|
|
72
|
+
actor=actor,
|
|
73
|
+
request_id=request_id,
|
|
74
|
+
reason="erasure",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _emit_supersede_playbook(
|
|
79
|
+
conn: sqlite3.Connection,
|
|
80
|
+
*,
|
|
81
|
+
org_id: str,
|
|
82
|
+
entity_id: str,
|
|
83
|
+
old_status: str | None,
|
|
84
|
+
request_id: str,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Emit a single status_change->superseded lineage event for an agent playbook."""
|
|
87
|
+
_append_event_stmt(
|
|
88
|
+
conn,
|
|
89
|
+
org_id=org_id,
|
|
90
|
+
entity_type="agent_playbook",
|
|
91
|
+
entity_id=entity_id,
|
|
92
|
+
op="status_change",
|
|
93
|
+
prov="wasInvalidatedBy",
|
|
94
|
+
source_ids=[],
|
|
95
|
+
actor="aggregator",
|
|
96
|
+
request_id=request_id,
|
|
97
|
+
reason=f"{old_status or 'None'}->superseded",
|
|
98
|
+
from_status=old_status,
|
|
99
|
+
to_status=Status.SUPERSEDED.value,
|
|
100
|
+
status_namespace="lifecycle_status",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _emit_supersede_user_playbook(
|
|
105
|
+
conn: sqlite3.Connection,
|
|
106
|
+
*,
|
|
107
|
+
org_id: str,
|
|
108
|
+
entity_id: str,
|
|
109
|
+
old_status: str | None,
|
|
110
|
+
request_id: str,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Emit a single status_change->superseded lineage event for a user playbook."""
|
|
113
|
+
_append_event_stmt(
|
|
114
|
+
conn,
|
|
115
|
+
org_id=org_id,
|
|
116
|
+
entity_type="user_playbook",
|
|
117
|
+
entity_id=entity_id,
|
|
118
|
+
op="status_change",
|
|
119
|
+
prov="wasInvalidatedBy",
|
|
120
|
+
source_ids=[],
|
|
121
|
+
actor="consolidator",
|
|
122
|
+
request_id=request_id,
|
|
123
|
+
reason=f"{old_status or 'None'}->superseded",
|
|
124
|
+
from_status=old_status,
|
|
125
|
+
to_status=Status.SUPERSEDED.value,
|
|
126
|
+
status_namespace="lifecycle_status",
|
|
127
|
+
)
|
|
41
128
|
|
|
42
129
|
|
|
43
130
|
def _row_to_playbook_optimization_candidate(
|
|
@@ -51,6 +138,7 @@ def _row_to_playbook_optimization_candidate(
|
|
|
51
138
|
parent_candidate_ids=_json_loads(row["parent_candidate_ids"]) or [],
|
|
52
139
|
aggregate_score=row["aggregate_score"],
|
|
53
140
|
is_winner=bool(row["is_winner"]),
|
|
141
|
+
metadata_json=row["metadata_json"] or "{}",
|
|
54
142
|
created_at=row["created_at"],
|
|
55
143
|
)
|
|
56
144
|
|
|
@@ -77,12 +165,23 @@ def _row_to_playbook_optimization_evaluation(
|
|
|
77
165
|
)
|
|
78
166
|
|
|
79
167
|
|
|
168
|
+
def _build_tags_sql(alias: str, tags: list[str] | None) -> tuple[str, list[Any]]:
|
|
169
|
+
if not tags:
|
|
170
|
+
return "", []
|
|
171
|
+
placeholders = ",".join("?" for _ in tags)
|
|
172
|
+
return (
|
|
173
|
+
f"EXISTS (SELECT 1 FROM json_each({alias}.tags) WHERE value IN ({placeholders}))",
|
|
174
|
+
list(tags),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
80
178
|
class PlaybookMixin:
|
|
81
179
|
"""Mixin providing user playbook, agent playbook, and evaluation CRUD + search."""
|
|
82
180
|
|
|
83
181
|
# Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
|
|
84
182
|
_lock: Any
|
|
85
183
|
conn: sqlite3.Connection
|
|
184
|
+
org_id: str
|
|
86
185
|
_execute: Any
|
|
87
186
|
_fetchone: Any
|
|
88
187
|
_fetchall: Any
|
|
@@ -93,6 +192,7 @@ class PlaybookMixin:
|
|
|
93
192
|
_fts_delete: Any
|
|
94
193
|
_vec_upsert: Any
|
|
95
194
|
_vec_delete: Any
|
|
195
|
+
_delete_playbook_search_rows: Any
|
|
96
196
|
_has_sqlite_vec: bool
|
|
97
197
|
|
|
98
198
|
# ------------------------------------------------------------------
|
|
@@ -125,8 +225,9 @@ class PlaybookMixin:
|
|
|
125
225
|
content, trigger, rationale, blocking_issue,
|
|
126
226
|
source_interaction_ids,
|
|
127
227
|
status, source, embedding, expanded_terms,
|
|
128
|
-
source_span, notes, reader_angle
|
|
129
|
-
|
|
228
|
+
source_span, notes, reader_angle, tags,
|
|
229
|
+
merged_into, superseded_by)
|
|
230
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
130
231
|
(
|
|
131
232
|
up.user_id,
|
|
132
233
|
up.playbook_name,
|
|
@@ -147,6 +248,9 @@ class PlaybookMixin:
|
|
|
147
248
|
up.source_span,
|
|
148
249
|
up.notes,
|
|
149
250
|
up.reader_angle,
|
|
251
|
+
_json_dumps(up.tags),
|
|
252
|
+
up.merged_into,
|
|
253
|
+
up.superseded_by,
|
|
150
254
|
),
|
|
151
255
|
)
|
|
152
256
|
upid = cur.lastrowid or 0
|
|
@@ -175,6 +279,7 @@ class PlaybookMixin:
|
|
|
175
279
|
start_time: int | None = None,
|
|
176
280
|
end_time: int | None = None,
|
|
177
281
|
include_embedding: bool = False,
|
|
282
|
+
tags: list[str] | None = None,
|
|
178
283
|
) -> list[UserPlaybook]:
|
|
179
284
|
sql = "SELECT * FROM user_playbooks WHERE 1=1"
|
|
180
285
|
params: list[Any] = []
|
|
@@ -198,6 +303,14 @@ class PlaybookMixin:
|
|
|
198
303
|
frag, sparams = _build_status_sql(status_filter)
|
|
199
304
|
sql += f" AND {frag}"
|
|
200
305
|
params.extend(sparams)
|
|
306
|
+
else:
|
|
307
|
+
# Default: exclude tombstone statuses (MERGED/SUPERSEDED)
|
|
308
|
+
sql += " AND (status IS NULL OR status NOT IN (?, ?))"
|
|
309
|
+
params.extend(_TOMBSTONE_STATUS_VALUES)
|
|
310
|
+
tag_frag, tag_params = _build_tags_sql("user_playbooks", tags)
|
|
311
|
+
if tag_frag:
|
|
312
|
+
sql += f" AND {tag_frag}"
|
|
313
|
+
params.extend(tag_params)
|
|
201
314
|
|
|
202
315
|
sql += " ORDER BY created_at DESC LIMIT ?"
|
|
203
316
|
params.append(limit)
|
|
@@ -234,6 +347,10 @@ class PlaybookMixin:
|
|
|
234
347
|
frag, sparams = _build_status_sql(status_filter)
|
|
235
348
|
sql += f" AND {frag}"
|
|
236
349
|
params.extend(sparams)
|
|
350
|
+
else:
|
|
351
|
+
# Default: exclude tombstone statuses (MERGED/SUPERSEDED)
|
|
352
|
+
sql += " AND (status IS NULL OR status NOT IN (?, ?))"
|
|
353
|
+
params.extend(_TOMBSTONE_STATUS_VALUES)
|
|
237
354
|
|
|
238
355
|
row = self._fetchone(sql, params)
|
|
239
356
|
return row["cnt"] if row else 0
|
|
@@ -243,25 +360,51 @@ class PlaybookMixin:
|
|
|
243
360
|
row = self._fetchone(
|
|
244
361
|
"""SELECT COUNT(*) as cnt FROM user_playbooks up
|
|
245
362
|
JOIN requests r ON up.request_id = r.request_id
|
|
246
|
-
WHERE r.session_id = ?
|
|
247
|
-
|
|
363
|
+
WHERE r.session_id = ?
|
|
364
|
+
AND (up.status IS NULL OR up.status NOT IN (?, ?))""",
|
|
365
|
+
(session_id, *_TOMBSTONE_STATUS_VALUES),
|
|
248
366
|
)
|
|
249
367
|
return row["cnt"] if row else 0
|
|
250
368
|
|
|
251
369
|
@SQLiteStorageBase.handle_exceptions
|
|
252
370
|
def delete_all_user_playbooks(self) -> None:
|
|
371
|
+
batch_request_id = uuid.uuid4().hex
|
|
253
372
|
with self._lock:
|
|
254
|
-
|
|
373
|
+
ids = [
|
|
374
|
+
r["user_playbook_id"]
|
|
375
|
+
for r in self.conn.execute(
|
|
376
|
+
"SELECT user_playbook_id FROM user_playbooks"
|
|
377
|
+
).fetchall()
|
|
378
|
+
]
|
|
255
379
|
self.conn.execute("DELETE FROM user_playbooks")
|
|
380
|
+
for upid in ids:
|
|
381
|
+
_emit_hard_delete_playbook(
|
|
382
|
+
self.conn,
|
|
383
|
+
org_id=self.org_id,
|
|
384
|
+
entity_type="user_playbook",
|
|
385
|
+
entity_id=str(upid),
|
|
386
|
+
request_id=batch_request_id,
|
|
387
|
+
)
|
|
256
388
|
self.conn.commit()
|
|
389
|
+
self._delete_playbook_search_rows("user", ids)
|
|
257
390
|
|
|
258
391
|
@SQLiteStorageBase.handle_exceptions
|
|
259
392
|
def delete_user_playbook(self, user_playbook_id: int) -> None:
|
|
260
|
-
self.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
393
|
+
with self._lock:
|
|
394
|
+
cur = self.conn.execute(
|
|
395
|
+
"DELETE FROM user_playbooks WHERE user_playbook_id = ?",
|
|
396
|
+
(user_playbook_id,),
|
|
397
|
+
)
|
|
398
|
+
if cur.rowcount > 0:
|
|
399
|
+
_emit_hard_delete_playbook(
|
|
400
|
+
self.conn,
|
|
401
|
+
org_id=self.org_id,
|
|
402
|
+
entity_type="user_playbook",
|
|
403
|
+
entity_id=str(user_playbook_id),
|
|
404
|
+
request_id=uuid.uuid4().hex,
|
|
405
|
+
)
|
|
406
|
+
self._delete_playbook_search_rows("user", [user_playbook_id], commit=False)
|
|
407
|
+
self.conn.commit()
|
|
265
408
|
|
|
266
409
|
@SQLiteStorageBase.handle_exceptions
|
|
267
410
|
def delete_all_user_playbooks_by_playbook_name(
|
|
@@ -272,37 +415,60 @@ class PlaybookMixin:
|
|
|
272
415
|
if agent_version is not None:
|
|
273
416
|
sql += " AND agent_version = ?"
|
|
274
417
|
params.append(agent_version)
|
|
275
|
-
|
|
276
|
-
|
|
418
|
+
batch_request_id = uuid.uuid4().hex
|
|
419
|
+
with self._lock:
|
|
420
|
+
ids = [
|
|
421
|
+
r["user_playbook_id"] for r in self.conn.execute(sql, params).fetchall()
|
|
422
|
+
]
|
|
423
|
+
if not ids:
|
|
424
|
+
return
|
|
277
425
|
ph = ",".join("?" for _ in ids)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
426
|
+
self.conn.execute(
|
|
427
|
+
f"DELETE FROM user_playbooks WHERE user_playbook_id IN ({ph})", ids
|
|
428
|
+
)
|
|
429
|
+
for upid in ids:
|
|
430
|
+
_emit_hard_delete_playbook(
|
|
431
|
+
self.conn,
|
|
432
|
+
org_id=self.org_id,
|
|
433
|
+
entity_type="user_playbook",
|
|
434
|
+
entity_id=str(upid),
|
|
435
|
+
request_id=batch_request_id,
|
|
281
436
|
)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
del_sql = "DELETE FROM user_playbooks WHERE playbook_name = ?"
|
|
285
|
-
del_params: list[Any] = [playbook_name]
|
|
286
|
-
if agent_version is not None:
|
|
287
|
-
del_sql += " AND agent_version = ?"
|
|
288
|
-
del_params.append(agent_version)
|
|
289
|
-
self._execute(del_sql, del_params)
|
|
437
|
+
self.conn.commit()
|
|
438
|
+
self._delete_playbook_search_rows("user", ids)
|
|
290
439
|
|
|
291
440
|
@SQLiteStorageBase.handle_exceptions
|
|
292
|
-
def delete_user_playbooks_by_ids(
|
|
441
|
+
def delete_user_playbooks_by_ids(
|
|
442
|
+
self, user_playbook_ids: list[int], *, emit_hard_delete: bool = True
|
|
443
|
+
) -> int:
|
|
293
444
|
if not user_playbook_ids:
|
|
294
445
|
return 0
|
|
295
446
|
ph = ",".join("?" for _ in user_playbook_ids)
|
|
447
|
+
batch_request_id = uuid.uuid4().hex
|
|
296
448
|
with self._lock:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
449
|
+
existing = [
|
|
450
|
+
r["user_playbook_id"]
|
|
451
|
+
for r in self.conn.execute(
|
|
452
|
+
f"SELECT user_playbook_id FROM user_playbooks WHERE user_playbook_id IN ({ph})",
|
|
453
|
+
user_playbook_ids,
|
|
454
|
+
).fetchall()
|
|
455
|
+
]
|
|
301
456
|
cur = self.conn.execute(
|
|
302
457
|
f"DELETE FROM user_playbooks WHERE user_playbook_id IN ({ph})",
|
|
303
458
|
user_playbook_ids,
|
|
304
459
|
)
|
|
460
|
+
if emit_hard_delete:
|
|
461
|
+
for upid in existing:
|
|
462
|
+
_emit_hard_delete_playbook(
|
|
463
|
+
self.conn,
|
|
464
|
+
org_id=self.org_id,
|
|
465
|
+
entity_type="user_playbook",
|
|
466
|
+
entity_id=str(upid),
|
|
467
|
+
request_id=batch_request_id,
|
|
468
|
+
actor="system",
|
|
469
|
+
)
|
|
305
470
|
self.conn.commit()
|
|
471
|
+
self._delete_playbook_search_rows("user", user_playbook_ids)
|
|
306
472
|
return cur.rowcount
|
|
307
473
|
|
|
308
474
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -314,26 +480,63 @@ class PlaybookMixin:
|
|
|
314
480
|
playbook_name: str | None = None,
|
|
315
481
|
) -> int:
|
|
316
482
|
new_val = new_status.value if new_status else None
|
|
317
|
-
|
|
483
|
+
now_ts = _epoch_now()
|
|
484
|
+
old_val_str = old_status.value if old_status else "None"
|
|
485
|
+
new_val_str = new_status.value if new_status else "None"
|
|
486
|
+
reason = f"{old_val_str}->{new_val_str}"
|
|
318
487
|
|
|
319
488
|
if old_status is None or (
|
|
320
489
|
hasattr(old_status, "value") and old_status.value is None
|
|
321
490
|
):
|
|
322
491
|
where = "status IS NULL"
|
|
492
|
+
select_params: list[Any] = []
|
|
323
493
|
else:
|
|
324
494
|
where = "status = ?"
|
|
325
|
-
|
|
495
|
+
select_params = [old_status.value]
|
|
326
496
|
|
|
497
|
+
extra_params: list[Any] = []
|
|
327
498
|
if agent_version is not None:
|
|
328
499
|
where += " AND agent_version = ?"
|
|
329
|
-
|
|
500
|
+
extra_params.append(agent_version)
|
|
330
501
|
if playbook_name is not None:
|
|
331
502
|
where += " AND playbook_name = ?"
|
|
332
|
-
|
|
503
|
+
extra_params.append(playbook_name)
|
|
333
504
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
505
|
+
# Set retired_at = now when transitioning to a GC-eligible status; clear to NULL otherwise.
|
|
506
|
+
retired_at_val = now_ts if new_val in _GC_ELIGIBLE_STATUSES else None
|
|
507
|
+
|
|
508
|
+
batch_request_id = uuid.uuid4().hex
|
|
509
|
+
with self._lock:
|
|
510
|
+
affected = [
|
|
511
|
+
r["user_playbook_id"]
|
|
512
|
+
for r in self.conn.execute(
|
|
513
|
+
f"SELECT user_playbook_id FROM user_playbooks WHERE {where}",
|
|
514
|
+
select_params + extra_params,
|
|
515
|
+
).fetchall()
|
|
516
|
+
]
|
|
517
|
+
cur = self.conn.execute(
|
|
518
|
+
f"UPDATE user_playbooks SET status = ?, retired_at = ? WHERE {where}",
|
|
519
|
+
[new_val, retired_at_val] + select_params + extra_params,
|
|
520
|
+
)
|
|
521
|
+
from_val = old_status.value if old_status else None
|
|
522
|
+
to_val = new_status.value if new_status else None
|
|
523
|
+
for upid in affected:
|
|
524
|
+
_append_event_stmt(
|
|
525
|
+
self.conn,
|
|
526
|
+
org_id=self.org_id,
|
|
527
|
+
entity_type="user_playbook",
|
|
528
|
+
entity_id=str(upid),
|
|
529
|
+
op="status_change",
|
|
530
|
+
prov="wasInvalidatedBy",
|
|
531
|
+
source_ids=[],
|
|
532
|
+
actor="api",
|
|
533
|
+
request_id=batch_request_id,
|
|
534
|
+
reason=reason,
|
|
535
|
+
from_status=from_val,
|
|
536
|
+
to_status=to_val,
|
|
537
|
+
status_namespace="lifecycle_status",
|
|
538
|
+
)
|
|
539
|
+
self.conn.commit()
|
|
337
540
|
return cur.rowcount
|
|
338
541
|
|
|
339
542
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -343,6 +546,10 @@ class PlaybookMixin:
|
|
|
343
546
|
agent_version: str | None = None,
|
|
344
547
|
playbook_name: str | None = None,
|
|
345
548
|
) -> int:
|
|
549
|
+
# Bulk delete-by-status emits no hard_delete lineage events (parity with
|
|
550
|
+
# the Supabase backend, which routes this through _hard_delete_and_log with
|
|
551
|
+
# emit_hard_delete=False). Accepts any status: the upgrade flow legitimately
|
|
552
|
+
# deletes old ARCHIVED playbooks via _delete_items_by_status(Status.ARCHIVED).
|
|
346
553
|
where = "status = ?"
|
|
347
554
|
params: list[Any] = [status.value]
|
|
348
555
|
if agent_version is not None:
|
|
@@ -352,22 +559,23 @@ class PlaybookMixin:
|
|
|
352
559
|
where += " AND playbook_name = ?"
|
|
353
560
|
params.append(playbook_name)
|
|
354
561
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
562
|
+
with self._lock:
|
|
563
|
+
ids = [
|
|
564
|
+
r["user_playbook_id"]
|
|
565
|
+
for r in self.conn.execute(
|
|
566
|
+
f"SELECT user_playbook_id FROM user_playbooks WHERE {where}",
|
|
567
|
+
params, # noqa: S608
|
|
568
|
+
).fetchall()
|
|
569
|
+
]
|
|
570
|
+
if not ids:
|
|
571
|
+
return 0
|
|
363
572
|
ph = ",".join("?" for _ in ids)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
cur = self._execute(f"DELETE FROM user_playbooks WHERE {where}", params)
|
|
573
|
+
cur = self.conn.execute(
|
|
574
|
+
f"DELETE FROM user_playbooks WHERE user_playbook_id IN ({ph})", # noqa: S608
|
|
575
|
+
ids,
|
|
576
|
+
)
|
|
577
|
+
self._delete_playbook_search_rows("user", ids, commit=False)
|
|
578
|
+
self.conn.commit()
|
|
371
579
|
return cur.rowcount
|
|
372
580
|
|
|
373
581
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -393,9 +601,9 @@ class PlaybookMixin:
|
|
|
393
601
|
@SQLiteStorageBase.handle_exceptions
|
|
394
602
|
def archive_user_playbook_by_id(self, user_id: str, user_playbook_id: int) -> bool:
|
|
395
603
|
cur = self._execute(
|
|
396
|
-
"UPDATE user_playbooks SET status = ? "
|
|
604
|
+
"UPDATE user_playbooks SET status = ?, retired_at = ? "
|
|
397
605
|
"WHERE user_playbook_id = ? AND user_id = ? AND status IS NULL",
|
|
398
|
-
(Status.ARCHIVED.value, user_playbook_id, user_id),
|
|
606
|
+
(Status.ARCHIVED.value, _epoch_now(), user_playbook_id, user_id),
|
|
399
607
|
)
|
|
400
608
|
return cur.rowcount > 0
|
|
401
609
|
|
|
@@ -470,6 +678,14 @@ class PlaybookMixin:
|
|
|
470
678
|
frag, sparams = _build_status_sql(status_filter)
|
|
471
679
|
conditions.append(frag)
|
|
472
680
|
params.extend(sparams)
|
|
681
|
+
else:
|
|
682
|
+
# Default: exclude tombstone statuses (MERGED/SUPERSEDED)
|
|
683
|
+
conditions.append("(up.status IS NULL OR up.status NOT IN (?, ?))")
|
|
684
|
+
params.extend(_TOMBSTONE_STATUS_VALUES)
|
|
685
|
+
tag_frag, tag_params = _build_tags_sql("up", request.tags)
|
|
686
|
+
if tag_frag:
|
|
687
|
+
conditions.append(tag_frag)
|
|
688
|
+
params.extend(tag_params)
|
|
473
689
|
|
|
474
690
|
where_extra = (" AND " + " AND ".join(conditions)) if conditions else ""
|
|
475
691
|
overfetch = match_count * 5 if mode != SearchMode.FTS else match_count
|
|
@@ -534,11 +750,15 @@ class PlaybookMixin:
|
|
|
534
750
|
return [_row_to_user_playbook(r) for r in rows]
|
|
535
751
|
|
|
536
752
|
@SQLiteStorageBase.handle_exceptions
|
|
537
|
-
def get_user_playbook_by_id(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
753
|
+
def get_user_playbook_by_id(
|
|
754
|
+
self, user_playbook_id: int, *, include_tombstones: bool = False
|
|
755
|
+
) -> UserPlaybook | None:
|
|
756
|
+
sql = "SELECT * FROM user_playbooks WHERE user_playbook_id = ?"
|
|
757
|
+
if not include_tombstones:
|
|
758
|
+
sql += " AND (status IS NULL OR status NOT IN (?, ?))"
|
|
759
|
+
row = self._fetchone(sql, (user_playbook_id, *_TOMBSTONE_STATUS_VALUES))
|
|
760
|
+
else:
|
|
761
|
+
row = self._fetchone(sql, (user_playbook_id,))
|
|
542
762
|
return _row_to_user_playbook(row) if row else None
|
|
543
763
|
|
|
544
764
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -567,6 +787,77 @@ class PlaybookMixin:
|
|
|
567
787
|
# Agent Playbook methods
|
|
568
788
|
# ------------------------------------------------------------------
|
|
569
789
|
|
|
790
|
+
def _index_agent_playbook_fts_vec(self, ap: AgentPlaybook) -> None:
|
|
791
|
+
"""Update the FTS and vector indexes for a single agent playbook row.
|
|
792
|
+
|
|
793
|
+
Must be called AFTER the row's transaction has been committed. The FTS
|
|
794
|
+
and vec helpers self-commit, so they must never be interleaved inside a
|
|
795
|
+
transaction that still has pending mutations.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
ap (AgentPlaybook): The already-saved playbook (``agent_playbook_id``
|
|
799
|
+
must be set).
|
|
800
|
+
"""
|
|
801
|
+
fts_parts = [ap.trigger or "", ap.content or ""]
|
|
802
|
+
if ap.expanded_terms:
|
|
803
|
+
fts_parts.append(ap.expanded_terms)
|
|
804
|
+
self._fts_upsert(
|
|
805
|
+
"agent_playbooks_fts",
|
|
806
|
+
ap.agent_playbook_id,
|
|
807
|
+
search_text=" ".join(p for p in fts_parts if p) or "",
|
|
808
|
+
)
|
|
809
|
+
if ap.embedding:
|
|
810
|
+
self._vec_upsert("agent_playbooks_vec", ap.agent_playbook_id, ap.embedding)
|
|
811
|
+
|
|
812
|
+
def _insert_agent_playbook_row(
|
|
813
|
+
self, conn: "sqlite3.Connection", ap: AgentPlaybook, created_at_iso: str
|
|
814
|
+
) -> "sqlite3.Cursor":
|
|
815
|
+
"""Execute the agent_playbooks INSERT and populate ``ap.agent_playbook_id``.
|
|
816
|
+
|
|
817
|
+
Runs the INSERT inside the caller's connection context; does NOT commit.
|
|
818
|
+
The caller is responsible for committing (or rolling back) the transaction.
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
conn: The open SQLite connection to execute against.
|
|
822
|
+
ap: The playbook to insert; ``agent_playbook_id`` is set on return.
|
|
823
|
+
created_at_iso: ISO-8601 timestamp string for ``created_at``.
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
sqlite3.Cursor: The cursor from the INSERT (``lastrowid`` is the new PK).
|
|
827
|
+
"""
|
|
828
|
+
cur = conn.execute(
|
|
829
|
+
"""INSERT INTO agent_playbooks
|
|
830
|
+
(playbook_name, created_at, agent_version, content,
|
|
831
|
+
trigger, rationale, blocking_issue,
|
|
832
|
+
playbook_status, playbook_metadata, embedding,
|
|
833
|
+
expanded_terms, tags, status,
|
|
834
|
+
merged_into, superseded_by)
|
|
835
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
836
|
+
(
|
|
837
|
+
ap.playbook_name,
|
|
838
|
+
created_at_iso,
|
|
839
|
+
ap.agent_version,
|
|
840
|
+
ap.content,
|
|
841
|
+
ap.trigger,
|
|
842
|
+
ap.rationale,
|
|
843
|
+
json.dumps(ap.blocking_issue.model_dump())
|
|
844
|
+
if ap.blocking_issue
|
|
845
|
+
else None,
|
|
846
|
+
ap.playbook_status.value
|
|
847
|
+
if isinstance(ap.playbook_status, PlaybookStatus)
|
|
848
|
+
else ap.playbook_status,
|
|
849
|
+
ap.playbook_metadata,
|
|
850
|
+
_json_dumps(ap.embedding),
|
|
851
|
+
ap.expanded_terms,
|
|
852
|
+
_json_dumps(ap.tags),
|
|
853
|
+
ap.status.value if ap.status else None,
|
|
854
|
+
ap.merged_into,
|
|
855
|
+
ap.superseded_by,
|
|
856
|
+
),
|
|
857
|
+
)
|
|
858
|
+
ap.agent_playbook_id = cur.lastrowid or 0
|
|
859
|
+
return cur
|
|
860
|
+
|
|
570
861
|
@SQLiteStorageBase.handle_exceptions
|
|
571
862
|
def save_agent_playbooks(
|
|
572
863
|
self, agent_playbooks: list[AgentPlaybook]
|
|
@@ -585,50 +876,89 @@ class PlaybookMixin:
|
|
|
585
876
|
|
|
586
877
|
created_at_iso = _epoch_to_iso(ap.created_at)
|
|
587
878
|
with self._lock:
|
|
588
|
-
|
|
589
|
-
"""INSERT INTO agent_playbooks
|
|
590
|
-
(playbook_name, created_at, agent_version, content,
|
|
591
|
-
trigger, rationale, blocking_issue,
|
|
592
|
-
playbook_status, playbook_metadata, embedding,
|
|
593
|
-
expanded_terms, status)
|
|
594
|
-
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
595
|
-
(
|
|
596
|
-
ap.playbook_name,
|
|
597
|
-
created_at_iso,
|
|
598
|
-
ap.agent_version,
|
|
599
|
-
ap.content,
|
|
600
|
-
ap.trigger,
|
|
601
|
-
ap.rationale,
|
|
602
|
-
json.dumps(ap.blocking_issue.model_dump())
|
|
603
|
-
if ap.blocking_issue
|
|
604
|
-
else None,
|
|
605
|
-
ap.playbook_status.value
|
|
606
|
-
if isinstance(ap.playbook_status, PlaybookStatus)
|
|
607
|
-
else ap.playbook_status,
|
|
608
|
-
ap.playbook_metadata,
|
|
609
|
-
_json_dumps(ap.embedding),
|
|
610
|
-
ap.expanded_terms,
|
|
611
|
-
ap.status.value if ap.status else None,
|
|
612
|
-
),
|
|
613
|
-
)
|
|
614
|
-
ap.agent_playbook_id = cur.lastrowid or 0
|
|
879
|
+
self._insert_agent_playbook_row(self.conn, ap, created_at_iso)
|
|
615
880
|
self.conn.commit()
|
|
616
881
|
|
|
617
|
-
|
|
618
|
-
if ap.expanded_terms:
|
|
619
|
-
fts_parts.append(ap.expanded_terms)
|
|
620
|
-
self._fts_upsert(
|
|
621
|
-
"agent_playbooks_fts",
|
|
622
|
-
ap.agent_playbook_id,
|
|
623
|
-
search_text=" ".join(p for p in fts_parts if p) or "",
|
|
624
|
-
)
|
|
625
|
-
if ap.embedding:
|
|
626
|
-
self._vec_upsert(
|
|
627
|
-
"agent_playbooks_vec", ap.agent_playbook_id, ap.embedding
|
|
628
|
-
)
|
|
882
|
+
self._index_agent_playbook_fts_vec(ap)
|
|
629
883
|
saved.append(ap)
|
|
630
884
|
return saved
|
|
631
885
|
|
|
886
|
+
@SQLiteStorageBase.handle_exceptions
|
|
887
|
+
def save_agent_playbook_with_aggregate_event(
|
|
888
|
+
self,
|
|
889
|
+
agent_playbook: AgentPlaybook,
|
|
890
|
+
*,
|
|
891
|
+
source_ids: list[str],
|
|
892
|
+
request_id: str,
|
|
893
|
+
run_mode: str,
|
|
894
|
+
) -> AgentPlaybook:
|
|
895
|
+
"""Persist an agent playbook AND its ``op=aggregate`` lineage event atomically.
|
|
896
|
+
|
|
897
|
+
The INSERT and the event are committed in a single transaction — if either
|
|
898
|
+
fails, both roll back. The event is the sole record of the run->playbook
|
|
899
|
+
membership for reconstruction, so atomicity is critical.
|
|
900
|
+
|
|
901
|
+
Args:
|
|
902
|
+
agent_playbook (AgentPlaybook): The playbook to persist.
|
|
903
|
+
source_ids (list[str]): IDs of the source entities that produced this playbook.
|
|
904
|
+
request_id (str): The aggregation run ID.
|
|
905
|
+
run_mode (str): Aggregation run mode (e.g. ``full_archive`` or ``incremental``).
|
|
906
|
+
|
|
907
|
+
Returns:
|
|
908
|
+
AgentPlaybook: The saved playbook with ``agent_playbook_id`` populated.
|
|
909
|
+
|
|
910
|
+
Raises:
|
|
911
|
+
ValueError: If ``request_id`` is empty (would produce an unreconstructable event).
|
|
912
|
+
"""
|
|
913
|
+
if not request_id or not request_id.strip():
|
|
914
|
+
raise ValueError(
|
|
915
|
+
"save_agent_playbook_with_aggregate_event requires a non-empty request_id"
|
|
916
|
+
)
|
|
917
|
+
ap = agent_playbook
|
|
918
|
+
embedding_text = ap.trigger or ap.content
|
|
919
|
+
if self._should_expand_documents():
|
|
920
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
921
|
+
emb_future = executor.submit(self._get_embedding, embedding_text)
|
|
922
|
+
exp_future = executor.submit(self._expand_document, embedding_text)
|
|
923
|
+
ap.embedding = emb_future.result(timeout=15)
|
|
924
|
+
ap.expanded_terms = exp_future.result(timeout=15)
|
|
925
|
+
else:
|
|
926
|
+
ap.embedding = self._get_embedding(embedding_text)
|
|
927
|
+
|
|
928
|
+
created_at_iso = _epoch_to_iso(ap.created_at)
|
|
929
|
+
with self._lock:
|
|
930
|
+
try:
|
|
931
|
+
self._insert_agent_playbook_row(self.conn, ap, created_at_iso)
|
|
932
|
+
_append_event_stmt(
|
|
933
|
+
self.conn,
|
|
934
|
+
org_id=self.org_id,
|
|
935
|
+
entity_type="agent_playbook",
|
|
936
|
+
entity_id=str(ap.agent_playbook_id),
|
|
937
|
+
op="aggregate",
|
|
938
|
+
prov="wasDerivedFrom",
|
|
939
|
+
source_ids=source_ids,
|
|
940
|
+
actor="aggregator",
|
|
941
|
+
request_id=request_id,
|
|
942
|
+
reason=f"{AGGREGATE_REASON_PREFIX}{run_mode}",
|
|
943
|
+
)
|
|
944
|
+
self.conn.commit()
|
|
945
|
+
except Exception:
|
|
946
|
+
self.conn.rollback()
|
|
947
|
+
raise
|
|
948
|
+
|
|
949
|
+
# FTS/vec indexing AFTER commit — these helpers self-commit and must
|
|
950
|
+
# not be interleaved inside the atomic transaction above.
|
|
951
|
+
# Index failure does NOT invalidate the committed row+event; the index
|
|
952
|
+
# is reconstructable from the authoritative row.
|
|
953
|
+
try:
|
|
954
|
+
self._index_agent_playbook_fts_vec(ap)
|
|
955
|
+
except Exception:
|
|
956
|
+
logger.exception(
|
|
957
|
+
"FTS/vec indexing failed for agent_playbook %s (row committed, index skipped)",
|
|
958
|
+
ap.agent_playbook_id,
|
|
959
|
+
)
|
|
960
|
+
return ap
|
|
961
|
+
|
|
632
962
|
@SQLiteStorageBase.handle_exceptions
|
|
633
963
|
def get_agent_playbooks(
|
|
634
964
|
self,
|
|
@@ -637,6 +967,7 @@ class PlaybookMixin:
|
|
|
637
967
|
agent_version: str | None = None,
|
|
638
968
|
status_filter: list[Status | None] | None = None,
|
|
639
969
|
playbook_status_filter: list[PlaybookStatus] | None = None,
|
|
970
|
+
tags: list[str] | None = None,
|
|
640
971
|
) -> list[AgentPlaybook]:
|
|
641
972
|
sql = "SELECT * FROM agent_playbooks WHERE 1=1"
|
|
642
973
|
params: list[Any] = []
|
|
@@ -660,6 +991,10 @@ class PlaybookMixin:
|
|
|
660
991
|
ph = ",".join("?" for _ in playbook_status_filter)
|
|
661
992
|
sql += f" AND playbook_status IN ({ph})"
|
|
662
993
|
params.extend(ps.value for ps in playbook_status_filter)
|
|
994
|
+
tag_frag, tag_params = _build_tags_sql("agent_playbooks", tags)
|
|
995
|
+
if tag_frag:
|
|
996
|
+
sql += f" AND {tag_frag}"
|
|
997
|
+
params.extend(tag_params)
|
|
663
998
|
|
|
664
999
|
sql += " ORDER BY created_at DESC LIMIT ?"
|
|
665
1000
|
params.append(limit)
|
|
@@ -667,28 +1002,58 @@ class PlaybookMixin:
|
|
|
667
1002
|
return [_row_to_agent_playbook(r) for r in rows]
|
|
668
1003
|
|
|
669
1004
|
@SQLiteStorageBase.handle_exceptions
|
|
670
|
-
def get_agent_playbook_by_id(
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1005
|
+
def get_agent_playbook_by_id(
|
|
1006
|
+
self, agent_playbook_id: int, *, include_tombstones: bool = False
|
|
1007
|
+
) -> AgentPlaybook | None:
|
|
1008
|
+
sql = "SELECT * FROM agent_playbooks WHERE agent_playbook_id = ?"
|
|
1009
|
+
if not include_tombstones:
|
|
1010
|
+
sql += " AND (status IS NULL OR status NOT IN (?, ?))"
|
|
1011
|
+
row = self._fetchone(sql, (agent_playbook_id, *_TOMBSTONE_STATUS_VALUES))
|
|
1012
|
+
else:
|
|
1013
|
+
row = self._fetchone(sql, (agent_playbook_id,))
|
|
675
1014
|
return _row_to_agent_playbook(row) if row else None
|
|
676
1015
|
|
|
677
1016
|
@SQLiteStorageBase.handle_exceptions
|
|
678
1017
|
def delete_all_agent_playbooks(self) -> None:
|
|
1018
|
+
batch_request_id = uuid.uuid4().hex
|
|
679
1019
|
with self._lock:
|
|
680
|
-
|
|
1020
|
+
ids = [
|
|
1021
|
+
r["agent_playbook_id"]
|
|
1022
|
+
for r in self.conn.execute(
|
|
1023
|
+
"SELECT agent_playbook_id FROM agent_playbooks"
|
|
1024
|
+
).fetchall()
|
|
1025
|
+
]
|
|
681
1026
|
self.conn.execute("DELETE FROM agent_playbooks")
|
|
1027
|
+
for apid in ids:
|
|
1028
|
+
_emit_hard_delete_playbook(
|
|
1029
|
+
self.conn,
|
|
1030
|
+
org_id=self.org_id,
|
|
1031
|
+
entity_type="agent_playbook",
|
|
1032
|
+
entity_id=str(apid),
|
|
1033
|
+
request_id=batch_request_id,
|
|
1034
|
+
)
|
|
682
1035
|
self.conn.commit()
|
|
1036
|
+
self._delete_playbook_search_rows("agent", ids)
|
|
683
1037
|
|
|
684
1038
|
@SQLiteStorageBase.handle_exceptions
|
|
685
1039
|
def delete_agent_playbook(self, agent_playbook_id: int) -> None:
|
|
686
|
-
self.
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1040
|
+
with self._lock:
|
|
1041
|
+
cur = self.conn.execute(
|
|
1042
|
+
"DELETE FROM agent_playbooks WHERE agent_playbook_id = ?",
|
|
1043
|
+
(agent_playbook_id,),
|
|
1044
|
+
)
|
|
1045
|
+
if cur.rowcount > 0:
|
|
1046
|
+
_emit_hard_delete_playbook(
|
|
1047
|
+
self.conn,
|
|
1048
|
+
org_id=self.org_id,
|
|
1049
|
+
entity_type="agent_playbook",
|
|
1050
|
+
entity_id=str(agent_playbook_id),
|
|
1051
|
+
request_id=uuid.uuid4().hex,
|
|
1052
|
+
)
|
|
1053
|
+
self._delete_playbook_search_rows(
|
|
1054
|
+
"agent", [agent_playbook_id], commit=False
|
|
1055
|
+
)
|
|
1056
|
+
self.conn.commit()
|
|
692
1057
|
|
|
693
1058
|
@SQLiteStorageBase.handle_exceptions
|
|
694
1059
|
def delete_all_agent_playbooks_by_playbook_name(
|
|
@@ -699,52 +1064,103 @@ class PlaybookMixin:
|
|
|
699
1064
|
if agent_version is not None:
|
|
700
1065
|
sql += " AND agent_version = ?"
|
|
701
1066
|
params.append(agent_version)
|
|
702
|
-
|
|
703
|
-
|
|
1067
|
+
batch_request_id = uuid.uuid4().hex
|
|
1068
|
+
with self._lock:
|
|
1069
|
+
ids = [
|
|
1070
|
+
r["agent_playbook_id"]
|
|
1071
|
+
for r in self.conn.execute(sql, params).fetchall()
|
|
1072
|
+
]
|
|
1073
|
+
if not ids:
|
|
1074
|
+
return
|
|
704
1075
|
ph = ",".join("?" for _ in ids)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1076
|
+
self.conn.execute(
|
|
1077
|
+
f"DELETE FROM agent_playbooks WHERE agent_playbook_id IN ({ph})", ids
|
|
1078
|
+
)
|
|
1079
|
+
for apid in ids:
|
|
1080
|
+
_emit_hard_delete_playbook(
|
|
1081
|
+
self.conn,
|
|
1082
|
+
org_id=self.org_id,
|
|
1083
|
+
entity_type="agent_playbook",
|
|
1084
|
+
entity_id=str(apid),
|
|
1085
|
+
request_id=batch_request_id,
|
|
708
1086
|
)
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
del_sql = "DELETE FROM agent_playbooks WHERE playbook_name = ?"
|
|
712
|
-
del_params: list[Any] = [playbook_name]
|
|
713
|
-
if agent_version is not None:
|
|
714
|
-
del_sql += " AND agent_version = ?"
|
|
715
|
-
del_params.append(agent_version)
|
|
716
|
-
self._execute(del_sql, del_params)
|
|
1087
|
+
self.conn.commit()
|
|
1088
|
+
self._delete_playbook_search_rows("agent", ids)
|
|
717
1089
|
|
|
718
1090
|
@SQLiteStorageBase.handle_exceptions
|
|
719
|
-
def delete_agent_playbooks_by_ids(
|
|
1091
|
+
def delete_agent_playbooks_by_ids(
|
|
1092
|
+
self, agent_playbook_ids: list[int], *, emit_hard_delete: bool = True
|
|
1093
|
+
) -> None:
|
|
720
1094
|
if not agent_playbook_ids:
|
|
721
1095
|
return
|
|
722
1096
|
ph = ",".join("?" for _ in agent_playbook_ids)
|
|
1097
|
+
batch_request_id = uuid.uuid4().hex
|
|
723
1098
|
with self._lock:
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1099
|
+
existing = [
|
|
1100
|
+
r["agent_playbook_id"]
|
|
1101
|
+
for r in self.conn.execute(
|
|
1102
|
+
f"SELECT agent_playbook_id FROM agent_playbooks WHERE agent_playbook_id IN ({ph})",
|
|
1103
|
+
agent_playbook_ids,
|
|
1104
|
+
).fetchall()
|
|
1105
|
+
]
|
|
728
1106
|
self.conn.execute(
|
|
729
1107
|
f"DELETE FROM agent_playbooks WHERE agent_playbook_id IN ({ph})",
|
|
730
1108
|
agent_playbook_ids,
|
|
731
1109
|
)
|
|
1110
|
+
if emit_hard_delete:
|
|
1111
|
+
for apid in existing:
|
|
1112
|
+
_emit_hard_delete_playbook(
|
|
1113
|
+
self.conn,
|
|
1114
|
+
org_id=self.org_id,
|
|
1115
|
+
entity_type="agent_playbook",
|
|
1116
|
+
entity_id=str(apid),
|
|
1117
|
+
request_id=batch_request_id,
|
|
1118
|
+
actor="system",
|
|
1119
|
+
)
|
|
732
1120
|
self.conn.commit()
|
|
1121
|
+
self._delete_playbook_search_rows("agent", agent_playbook_ids)
|
|
733
1122
|
|
|
734
1123
|
@SQLiteStorageBase.handle_exceptions
|
|
735
1124
|
def update_agent_playbook_status(
|
|
736
1125
|
self, agent_playbook_id: int, playbook_status: PlaybookStatus
|
|
737
1126
|
) -> None:
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
)
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1127
|
+
"""Update an agent playbook's status and emit a status_change lineage event.
|
|
1128
|
+
|
|
1129
|
+
Each call generates a fresh request_id so every status change is recorded as
|
|
1130
|
+
a distinct audit event (not collapsed by the idempotency key).
|
|
1131
|
+
"""
|
|
1132
|
+
with self._lock:
|
|
1133
|
+
row = self.conn.execute(
|
|
1134
|
+
"SELECT playbook_status FROM agent_playbooks WHERE agent_playbook_id = ?",
|
|
1135
|
+
(agent_playbook_id,),
|
|
1136
|
+
).fetchone()
|
|
1137
|
+
if not row:
|
|
1138
|
+
raise ValueError(
|
|
1139
|
+
f"Agent playbook with ID {agent_playbook_id} not found"
|
|
1140
|
+
)
|
|
1141
|
+
prior_playbook_status = row["playbook_status"]
|
|
1142
|
+
old_status = prior_playbook_status or "None"
|
|
1143
|
+
cur = self.conn.execute(
|
|
1144
|
+
"UPDATE agent_playbooks SET playbook_status = ? WHERE agent_playbook_id = ?",
|
|
1145
|
+
(playbook_status.value, agent_playbook_id),
|
|
1146
|
+
)
|
|
1147
|
+
if cur.rowcount > 0:
|
|
1148
|
+
_append_event_stmt(
|
|
1149
|
+
self.conn,
|
|
1150
|
+
org_id=self.org_id,
|
|
1151
|
+
entity_type="agent_playbook",
|
|
1152
|
+
entity_id=str(agent_playbook_id),
|
|
1153
|
+
op="status_change",
|
|
1154
|
+
prov="wasInvalidatedBy",
|
|
1155
|
+
source_ids=[],
|
|
1156
|
+
actor="api",
|
|
1157
|
+
request_id=uuid.uuid4().hex,
|
|
1158
|
+
reason=f"{old_status}->{playbook_status.value}",
|
|
1159
|
+
from_status=prior_playbook_status,
|
|
1160
|
+
to_status=playbook_status.value,
|
|
1161
|
+
status_namespace="playbook_status",
|
|
1162
|
+
)
|
|
1163
|
+
self.conn.commit()
|
|
748
1164
|
|
|
749
1165
|
@SQLiteStorageBase.handle_exceptions
|
|
750
1166
|
def update_agent_playbook(
|
|
@@ -756,13 +1172,8 @@ class PlaybookMixin:
|
|
|
756
1172
|
rationale: str | None = None,
|
|
757
1173
|
blocking_issue: BlockingIssue | None = None,
|
|
758
1174
|
playbook_status: PlaybookStatus | None = None,
|
|
1175
|
+
tags: list[str] | None = None,
|
|
759
1176
|
) -> None:
|
|
760
|
-
row = self._fetchone(
|
|
761
|
-
"SELECT agent_playbook_id FROM agent_playbooks WHERE agent_playbook_id = ?",
|
|
762
|
-
(agent_playbook_id,),
|
|
763
|
-
)
|
|
764
|
-
if not row:
|
|
765
|
-
raise ValueError(f"Agent playbook with ID {agent_playbook_id} not found")
|
|
766
1177
|
updates: list[str] = []
|
|
767
1178
|
params: list[Any] = []
|
|
768
1179
|
if playbook_name is not None:
|
|
@@ -783,12 +1194,54 @@ class PlaybookMixin:
|
|
|
783
1194
|
if playbook_status is not None:
|
|
784
1195
|
updates.append("playbook_status = ?")
|
|
785
1196
|
params.append(playbook_status.value)
|
|
1197
|
+
if tags is not None:
|
|
1198
|
+
updates.append("tags = ?")
|
|
1199
|
+
params.append(_json_dumps(tags))
|
|
786
1200
|
if updates:
|
|
787
1201
|
params.append(agent_playbook_id)
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1202
|
+
op = "revise" if content is not None else "status_change"
|
|
1203
|
+
prov = "wasRevisionOf" if op == "revise" else "wasInvalidatedBy"
|
|
1204
|
+
with self._lock:
|
|
1205
|
+
prior_row = self.conn.execute(
|
|
1206
|
+
"SELECT playbook_status FROM agent_playbooks WHERE agent_playbook_id = ?",
|
|
1207
|
+
(agent_playbook_id,),
|
|
1208
|
+
).fetchone()
|
|
1209
|
+
if not prior_row:
|
|
1210
|
+
raise ValueError(
|
|
1211
|
+
f"Agent playbook with ID {agent_playbook_id} not found"
|
|
1212
|
+
)
|
|
1213
|
+
prior_playbook_status = prior_row["playbook_status"]
|
|
1214
|
+
cur = self.conn.execute(
|
|
1215
|
+
f"UPDATE agent_playbooks SET {', '.join(updates)} WHERE agent_playbook_id = ?",
|
|
1216
|
+
tuple(params),
|
|
1217
|
+
)
|
|
1218
|
+
if cur.rowcount > 0:
|
|
1219
|
+
# Populate structured status fields only when playbook_status is
|
|
1220
|
+
# among the updated fields and the op is status_change (not revise).
|
|
1221
|
+
if op == "status_change" and playbook_status is not None:
|
|
1222
|
+
from_status = prior_playbook_status
|
|
1223
|
+
to_status = playbook_status.value
|
|
1224
|
+
status_namespace: str | None = "playbook_status"
|
|
1225
|
+
else:
|
|
1226
|
+
from_status = None
|
|
1227
|
+
to_status = None
|
|
1228
|
+
status_namespace = None
|
|
1229
|
+
_append_event_stmt(
|
|
1230
|
+
self.conn,
|
|
1231
|
+
org_id=self.org_id,
|
|
1232
|
+
entity_type="agent_playbook",
|
|
1233
|
+
entity_id=str(agent_playbook_id),
|
|
1234
|
+
op=op,
|
|
1235
|
+
prov=prov,
|
|
1236
|
+
source_ids=[],
|
|
1237
|
+
actor="api",
|
|
1238
|
+
request_id=uuid.uuid4().hex,
|
|
1239
|
+
reason="in-place update",
|
|
1240
|
+
from_status=from_status,
|
|
1241
|
+
to_status=to_status,
|
|
1242
|
+
status_namespace=status_namespace,
|
|
1243
|
+
)
|
|
1244
|
+
self.conn.commit()
|
|
792
1245
|
|
|
793
1246
|
@SQLiteStorageBase.handle_exceptions
|
|
794
1247
|
def update_user_playbook(
|
|
@@ -799,13 +1252,8 @@ class PlaybookMixin:
|
|
|
799
1252
|
trigger: str | None = None,
|
|
800
1253
|
rationale: str | None = None,
|
|
801
1254
|
blocking_issue: BlockingIssue | None = None,
|
|
1255
|
+
tags: list[str] | None = None,
|
|
802
1256
|
) -> None:
|
|
803
|
-
row = self._fetchone(
|
|
804
|
-
"SELECT user_playbook_id FROM user_playbooks WHERE user_playbook_id = ?",
|
|
805
|
-
(user_playbook_id,),
|
|
806
|
-
)
|
|
807
|
-
if not row:
|
|
808
|
-
raise ValueError(f"User playbook with ID {user_playbook_id} not found")
|
|
809
1257
|
updates: list[str] = []
|
|
810
1258
|
params: list[Any] = []
|
|
811
1259
|
if playbook_name is not None:
|
|
@@ -823,39 +1271,298 @@ class PlaybookMixin:
|
|
|
823
1271
|
if blocking_issue is not None:
|
|
824
1272
|
updates.append("blocking_issue = ?")
|
|
825
1273
|
params.append(json.dumps(blocking_issue.model_dump()))
|
|
1274
|
+
if tags is not None:
|
|
1275
|
+
updates.append("tags = ?")
|
|
1276
|
+
params.append(_json_dumps(tags))
|
|
826
1277
|
if updates:
|
|
827
1278
|
params.append(user_playbook_id)
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
tuple(params),
|
|
1279
|
+
semantic_change = any(
|
|
1280
|
+
value is not None for value in (content, trigger, rationale)
|
|
831
1281
|
)
|
|
1282
|
+
op = "revise" if semantic_change else "status_change"
|
|
1283
|
+
prov = "wasRevisionOf" if op == "revise" else "wasInvalidatedBy"
|
|
1284
|
+
with self._lock:
|
|
1285
|
+
cur = self.conn.execute(
|
|
1286
|
+
f"UPDATE user_playbooks SET {', '.join(updates)} WHERE user_playbook_id = ?",
|
|
1287
|
+
tuple(params),
|
|
1288
|
+
)
|
|
1289
|
+
if cur.rowcount > 0:
|
|
1290
|
+
_append_event_stmt(
|
|
1291
|
+
self.conn,
|
|
1292
|
+
org_id=self.org_id,
|
|
1293
|
+
entity_type="user_playbook",
|
|
1294
|
+
entity_id=str(user_playbook_id),
|
|
1295
|
+
op=op,
|
|
1296
|
+
prov=prov,
|
|
1297
|
+
source_ids=[],
|
|
1298
|
+
actor="api",
|
|
1299
|
+
request_id=uuid.uuid4().hex,
|
|
1300
|
+
reason="in-place update",
|
|
1301
|
+
from_status=None,
|
|
1302
|
+
to_status=None,
|
|
1303
|
+
status_namespace=None,
|
|
1304
|
+
)
|
|
1305
|
+
self.conn.commit()
|
|
1306
|
+
|
|
1307
|
+
@SQLiteStorageBase.handle_exceptions
|
|
1308
|
+
def supersede_user_playbooks_by_ids(
|
|
1309
|
+
self, user_playbook_ids: list[int], request_id: str
|
|
1310
|
+
) -> int:
|
|
1311
|
+
"""Soft-delete user playbooks by setting status to SUPERSEDED.
|
|
1312
|
+
|
|
1313
|
+
Preserves the row content for strict point-in-time attribution reads.
|
|
1314
|
+
Eligible rows are any non-tombstoned status (CURRENT / PENDING /
|
|
1315
|
+
ARCHIVED). Atomic: all updates and lineage events commit together.
|
|
1316
|
+
"""
|
|
1317
|
+
if not user_playbook_ids:
|
|
1318
|
+
return 0
|
|
1319
|
+
if not request_id:
|
|
1320
|
+
raise ValueError("request_id must be non-empty for supersede")
|
|
1321
|
+
now_ts = _epoch_now()
|
|
1322
|
+
updated = 0
|
|
1323
|
+
with self._lock:
|
|
1324
|
+
for upid in user_playbook_ids:
|
|
1325
|
+
row = self.conn.execute(
|
|
1326
|
+
"SELECT status FROM user_playbooks WHERE user_playbook_id = ?",
|
|
1327
|
+
(upid,),
|
|
1328
|
+
).fetchone()
|
|
1329
|
+
if row is None:
|
|
1330
|
+
continue
|
|
1331
|
+
old_status = row["status"]
|
|
1332
|
+
cur = self.conn.execute(
|
|
1333
|
+
"UPDATE user_playbooks SET status = ?, retired_at = ?"
|
|
1334
|
+
" WHERE user_playbook_id = ?"
|
|
1335
|
+
" AND (status IS NULL OR status NOT IN (?, ?))",
|
|
1336
|
+
(
|
|
1337
|
+
Status.SUPERSEDED.value,
|
|
1338
|
+
now_ts,
|
|
1339
|
+
upid,
|
|
1340
|
+
*_TOMBSTONE_STATUS_VALUES,
|
|
1341
|
+
),
|
|
1342
|
+
)
|
|
1343
|
+
if cur.rowcount > 0:
|
|
1344
|
+
_emit_supersede_user_playbook(
|
|
1345
|
+
self.conn,
|
|
1346
|
+
org_id=self.org_id,
|
|
1347
|
+
entity_id=str(upid),
|
|
1348
|
+
old_status=old_status,
|
|
1349
|
+
request_id=request_id,
|
|
1350
|
+
)
|
|
1351
|
+
updated += 1
|
|
1352
|
+
self.conn.commit()
|
|
1353
|
+
return updated
|
|
832
1354
|
|
|
833
1355
|
@SQLiteStorageBase.handle_exceptions
|
|
834
1356
|
def archive_agent_playbooks_by_playbook_name(
|
|
835
1357
|
self, playbook_name: str, agent_version: str | None = None
|
|
836
1358
|
) -> None:
|
|
837
|
-
|
|
1359
|
+
where = (
|
|
1360
|
+
"playbook_name = ? AND playbook_status != ?"
|
|
1361
|
+
" AND (status IS NULL OR status != 'archived')"
|
|
1362
|
+
)
|
|
838
1363
|
params: list[Any] = [playbook_name, PlaybookStatus.APPROVED.value]
|
|
839
1364
|
if agent_version is not None:
|
|
840
|
-
|
|
1365
|
+
where += " AND agent_version = ?"
|
|
841
1366
|
params.append(agent_version)
|
|
842
|
-
|
|
1367
|
+
now_ts = _epoch_now()
|
|
1368
|
+
batch_request_id = uuid.uuid4().hex
|
|
1369
|
+
with self._lock:
|
|
1370
|
+
affected = self.conn.execute(
|
|
1371
|
+
f"SELECT agent_playbook_id, status FROM agent_playbooks WHERE {where}",
|
|
1372
|
+
params,
|
|
1373
|
+
).fetchall()
|
|
1374
|
+
self.conn.execute(
|
|
1375
|
+
f"UPDATE agent_playbooks SET status = 'archived', retired_at = ? WHERE {where}",
|
|
1376
|
+
[now_ts, *params],
|
|
1377
|
+
)
|
|
1378
|
+
for row in affected:
|
|
1379
|
+
prior = row["status"] or "None"
|
|
1380
|
+
_append_event_stmt(
|
|
1381
|
+
self.conn,
|
|
1382
|
+
org_id=self.org_id,
|
|
1383
|
+
entity_type="agent_playbook",
|
|
1384
|
+
entity_id=str(row["agent_playbook_id"]),
|
|
1385
|
+
op="status_change",
|
|
1386
|
+
prov="wasInvalidatedBy",
|
|
1387
|
+
source_ids=[],
|
|
1388
|
+
actor="api",
|
|
1389
|
+
request_id=batch_request_id,
|
|
1390
|
+
reason=f"{prior}->archived",
|
|
1391
|
+
from_status=row["status"],
|
|
1392
|
+
to_status="archived",
|
|
1393
|
+
status_namespace="lifecycle_status",
|
|
1394
|
+
)
|
|
1395
|
+
self.conn.commit()
|
|
843
1396
|
|
|
844
1397
|
@SQLiteStorageBase.handle_exceptions
|
|
845
1398
|
def archive_agent_playbooks_by_ids(self, agent_playbook_ids: list[int]) -> None:
|
|
846
1399
|
if not agent_playbook_ids:
|
|
847
1400
|
return
|
|
848
1401
|
ph = ",".join("?" for _ in agent_playbook_ids)
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1402
|
+
now_ts = _epoch_now()
|
|
1403
|
+
batch_request_id = uuid.uuid4().hex
|
|
1404
|
+
with self._lock:
|
|
1405
|
+
affected = self.conn.execute(
|
|
1406
|
+
f"SELECT agent_playbook_id, status FROM agent_playbooks"
|
|
1407
|
+
f" WHERE agent_playbook_id IN ({ph}) AND playbook_status != ?"
|
|
1408
|
+
f" AND (status IS NULL OR status != 'archived')",
|
|
1409
|
+
[*agent_playbook_ids, PlaybookStatus.APPROVED.value],
|
|
1410
|
+
).fetchall()
|
|
1411
|
+
self.conn.execute(
|
|
1412
|
+
f"UPDATE agent_playbooks SET status = 'archived', retired_at = ?"
|
|
1413
|
+
f" WHERE agent_playbook_id IN ({ph}) AND playbook_status != ?"
|
|
1414
|
+
f" AND (status IS NULL OR status != 'archived')",
|
|
1415
|
+
[now_ts, *agent_playbook_ids, PlaybookStatus.APPROVED.value],
|
|
1416
|
+
)
|
|
1417
|
+
for row in affected:
|
|
1418
|
+
prior = row["status"] or "None"
|
|
1419
|
+
_append_event_stmt(
|
|
1420
|
+
self.conn,
|
|
1421
|
+
org_id=self.org_id,
|
|
1422
|
+
entity_type="agent_playbook",
|
|
1423
|
+
entity_id=str(row["agent_playbook_id"]),
|
|
1424
|
+
op="status_change",
|
|
1425
|
+
prov="wasInvalidatedBy",
|
|
1426
|
+
source_ids=[],
|
|
1427
|
+
actor="api",
|
|
1428
|
+
request_id=batch_request_id,
|
|
1429
|
+
reason=f"{prior}->archived",
|
|
1430
|
+
from_status=row["status"],
|
|
1431
|
+
to_status="archived",
|
|
1432
|
+
status_namespace="lifecycle_status",
|
|
1433
|
+
)
|
|
1434
|
+
self.conn.commit()
|
|
1435
|
+
|
|
1436
|
+
@SQLiteStorageBase.handle_exceptions
|
|
1437
|
+
def supersede_agent_playbooks_by_ids(
|
|
1438
|
+
self, agent_playbook_ids: list[int], request_id: str
|
|
1439
|
+
) -> int:
|
|
1440
|
+
"""Soft-delete agent playbooks by setting status to SUPERSEDED, emitting set-based lineage.
|
|
1441
|
+
|
|
1442
|
+
For each eligible id (not APPROVED, not already tombstoned), updates status to
|
|
1443
|
+
SUPERSEDED and emits one ``status_change`` event under the shared ``request_id``.
|
|
1444
|
+
Atomic: one ``conn.commit()`` at the end, guarded on rowcount per id.
|
|
1445
|
+
FTS/vec rows are NOT removed — reads exclude tombstones by status filter.
|
|
1446
|
+
|
|
1447
|
+
Args:
|
|
1448
|
+
agent_playbook_ids (list[int]): Agent playbook ids to supersede.
|
|
1449
|
+
request_id (str): Shared request id for all emitted lineage events.
|
|
1450
|
+
|
|
1451
|
+
Returns:
|
|
1452
|
+
int: Number of agent playbooks actually updated.
|
|
1453
|
+
"""
|
|
1454
|
+
if not agent_playbook_ids:
|
|
1455
|
+
return 0
|
|
1456
|
+
if not request_id:
|
|
1457
|
+
raise ValueError("request_id must be non-empty for supersede")
|
|
1458
|
+
now_ts = _epoch_now()
|
|
1459
|
+
updated = 0
|
|
1460
|
+
with self._lock:
|
|
1461
|
+
for apid in agent_playbook_ids:
|
|
1462
|
+
row = self.conn.execute(
|
|
1463
|
+
"SELECT status FROM agent_playbooks WHERE agent_playbook_id = ?",
|
|
1464
|
+
(apid,),
|
|
1465
|
+
).fetchone()
|
|
1466
|
+
if row is None:
|
|
1467
|
+
continue
|
|
1468
|
+
old_status = row["status"]
|
|
1469
|
+
# NOTE (M3): The model supersede_profiles_by_ids adds an eligible-check
|
|
1470
|
+
# `if old_status_val not in eligible: continue` before the UPDATE. For
|
|
1471
|
+
# agent_playbooks the ineligible condition spans two columns (status in
|
|
1472
|
+
# _TOMBSTONE_STATUS_VALUES OR playbook_status == APPROVED), so aligning
|
|
1473
|
+
# would require adding playbook_status to the SELECT. The UPDATE WHERE
|
|
1474
|
+
# clause already excludes those rows atomically; the extra continue here
|
|
1475
|
+
# would be a no-op and not worth the added complexity.
|
|
1476
|
+
cur = self.conn.execute(
|
|
1477
|
+
"UPDATE agent_playbooks SET status = ?, retired_at = ?"
|
|
1478
|
+
" WHERE agent_playbook_id = ? AND playbook_status != ?"
|
|
1479
|
+
" AND (status IS NULL OR status NOT IN (?, ?))",
|
|
1480
|
+
(
|
|
1481
|
+
Status.SUPERSEDED.value,
|
|
1482
|
+
now_ts,
|
|
1483
|
+
apid,
|
|
1484
|
+
PlaybookStatus.APPROVED.value,
|
|
1485
|
+
*_TOMBSTONE_STATUS_VALUES,
|
|
1486
|
+
),
|
|
1487
|
+
)
|
|
1488
|
+
if cur.rowcount > 0:
|
|
1489
|
+
_emit_supersede_playbook(
|
|
1490
|
+
self.conn,
|
|
1491
|
+
org_id=self.org_id,
|
|
1492
|
+
entity_id=str(apid),
|
|
1493
|
+
old_status=old_status,
|
|
1494
|
+
request_id=request_id,
|
|
1495
|
+
)
|
|
1496
|
+
updated += 1
|
|
1497
|
+
self.conn.commit()
|
|
1498
|
+
return updated
|
|
1499
|
+
|
|
1500
|
+
@SQLiteStorageBase.handle_exceptions
|
|
1501
|
+
def supersede_agent_playbooks_by_playbook_name(
|
|
1502
|
+
self, playbook_name: str, agent_version: str | None, request_id: str
|
|
1503
|
+
) -> int:
|
|
1504
|
+
"""Soft-delete archived agent playbooks by name/version via SUPERSEDED status.
|
|
1505
|
+
|
|
1506
|
+
Mirrors ``delete_archived_agent_playbooks_by_playbook_name`` but converts
|
|
1507
|
+
the hard-delete to a soft-supersede with status_change lineage events.
|
|
1508
|
+
Atomic: one ``conn.commit()`` at the end.
|
|
1509
|
+
FTS/vec rows are NOT removed.
|
|
1510
|
+
|
|
1511
|
+
Args:
|
|
1512
|
+
playbook_name (str): Playbook name to supersede.
|
|
1513
|
+
agent_version (str | None): Agent version filter. None matches all versions.
|
|
1514
|
+
request_id (str): Shared request id for all emitted lineage events.
|
|
1515
|
+
|
|
1516
|
+
Returns:
|
|
1517
|
+
int: Number of agent playbooks actually updated.
|
|
1518
|
+
"""
|
|
1519
|
+
if not request_id:
|
|
1520
|
+
raise ValueError("request_id must be non-empty for supersede")
|
|
1521
|
+
sql = (
|
|
1522
|
+
"SELECT agent_playbook_id, status FROM agent_playbooks"
|
|
1523
|
+
" WHERE playbook_name = ? AND status = 'archived'"
|
|
852
1524
|
)
|
|
1525
|
+
params: list[Any] = [playbook_name]
|
|
1526
|
+
if agent_version is not None:
|
|
1527
|
+
sql += " AND agent_version = ?"
|
|
1528
|
+
params.append(agent_version)
|
|
1529
|
+
updated = 0
|
|
1530
|
+
with self._lock:
|
|
1531
|
+
rows = self.conn.execute(sql, params).fetchall()
|
|
1532
|
+
if not rows:
|
|
1533
|
+
return 0
|
|
1534
|
+
now_ts = _epoch_now()
|
|
1535
|
+
for row in rows:
|
|
1536
|
+
apid = row["agent_playbook_id"]
|
|
1537
|
+
old_status = row["status"]
|
|
1538
|
+
cur = self.conn.execute(
|
|
1539
|
+
"UPDATE agent_playbooks SET status = ?, retired_at = ?"
|
|
1540
|
+
" WHERE agent_playbook_id = ? AND playbook_status != ?"
|
|
1541
|
+
" AND status = 'archived'",
|
|
1542
|
+
(
|
|
1543
|
+
Status.SUPERSEDED.value,
|
|
1544
|
+
now_ts,
|
|
1545
|
+
apid,
|
|
1546
|
+
PlaybookStatus.APPROVED.value,
|
|
1547
|
+
),
|
|
1548
|
+
)
|
|
1549
|
+
if cur.rowcount > 0:
|
|
1550
|
+
_emit_supersede_playbook(
|
|
1551
|
+
self.conn,
|
|
1552
|
+
org_id=self.org_id,
|
|
1553
|
+
entity_id=str(apid),
|
|
1554
|
+
old_status=old_status,
|
|
1555
|
+
request_id=request_id,
|
|
1556
|
+
)
|
|
1557
|
+
updated += 1
|
|
1558
|
+
self.conn.commit()
|
|
1559
|
+
return updated
|
|
853
1560
|
|
|
854
1561
|
@SQLiteStorageBase.handle_exceptions
|
|
855
1562
|
def restore_archived_agent_playbooks_by_playbook_name(
|
|
856
1563
|
self, playbook_name: str, agent_version: str | None = None
|
|
857
1564
|
) -> None:
|
|
858
|
-
sql = "UPDATE agent_playbooks SET status = NULL WHERE playbook_name = ? AND status = 'archived'"
|
|
1565
|
+
sql = "UPDATE agent_playbooks SET status = NULL, retired_at = NULL WHERE playbook_name = ? AND status = 'archived'"
|
|
859
1566
|
params: list[Any] = [playbook_name]
|
|
860
1567
|
if agent_version is not None:
|
|
861
1568
|
sql += " AND agent_version = ?"
|
|
@@ -870,7 +1577,7 @@ class PlaybookMixin:
|
|
|
870
1577
|
return
|
|
871
1578
|
ph = ",".join("?" for _ in agent_playbook_ids)
|
|
872
1579
|
self._execute(
|
|
873
|
-
f"UPDATE agent_playbooks SET status = NULL WHERE agent_playbook_id IN ({ph}) AND status = 'archived'",
|
|
1580
|
+
f"UPDATE agent_playbooks SET status = NULL, retired_at = NULL WHERE agent_playbook_id IN ({ph}) AND status = 'archived'",
|
|
874
1581
|
agent_playbook_ids,
|
|
875
1582
|
)
|
|
876
1583
|
|
|
@@ -901,6 +1608,32 @@ class PlaybookMixin:
|
|
|
901
1608
|
for window in self.get_source_windows_for_agent_playbook(agent_playbook_id)
|
|
902
1609
|
]
|
|
903
1610
|
|
|
1611
|
+
@SQLiteStorageBase.handle_exceptions
|
|
1612
|
+
def get_source_user_playbook_ids_for_agent_playbooks(
|
|
1613
|
+
self, agent_playbook_ids: Sequence[int]
|
|
1614
|
+
) -> dict[int, list[int]]:
|
|
1615
|
+
if not agent_playbook_ids:
|
|
1616
|
+
return {}
|
|
1617
|
+
unique_ids = list(dict.fromkeys(int(apid) for apid in agent_playbook_ids))
|
|
1618
|
+
ph = ",".join("?" for _ in unique_ids)
|
|
1619
|
+
rows = self._fetchall(
|
|
1620
|
+
f"""SELECT agent_playbook_id, user_playbook_id
|
|
1621
|
+
FROM agent_playbook_source_user_playbooks
|
|
1622
|
+
WHERE agent_playbook_id IN ({ph})
|
|
1623
|
+
ORDER BY agent_playbook_id ASC, user_playbook_id ASC""",
|
|
1624
|
+
unique_ids,
|
|
1625
|
+
)
|
|
1626
|
+
by_agent_id: dict[int, list[int]] = {apid: [] for apid in unique_ids}
|
|
1627
|
+
seen_by_agent_id: dict[int, set[int]] = {apid: set() for apid in unique_ids}
|
|
1628
|
+
for row in rows:
|
|
1629
|
+
agent_playbook_id = int(row["agent_playbook_id"])
|
|
1630
|
+
user_playbook_id = int(row["user_playbook_id"])
|
|
1631
|
+
seen = seen_by_agent_id.setdefault(agent_playbook_id, set())
|
|
1632
|
+
if user_playbook_id not in seen:
|
|
1633
|
+
by_agent_id.setdefault(agent_playbook_id, []).append(user_playbook_id)
|
|
1634
|
+
seen.add(user_playbook_id)
|
|
1635
|
+
return by_agent_id
|
|
1636
|
+
|
|
904
1637
|
@SQLiteStorageBase.handle_exceptions
|
|
905
1638
|
def set_source_windows_for_agent_playbook(
|
|
906
1639
|
self,
|
|
@@ -1023,8 +1756,8 @@ class PlaybookMixin:
|
|
|
1023
1756
|
cur = self.conn.execute(
|
|
1024
1757
|
"""INSERT INTO playbook_optimization_candidates
|
|
1025
1758
|
(job_id, candidate_index, content, parent_candidate_ids,
|
|
1026
|
-
aggregate_score, is_winner, created_at)
|
|
1027
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
1759
|
+
aggregate_score, is_winner, metadata_json, created_at)
|
|
1760
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
1028
1761
|
(
|
|
1029
1762
|
candidate.job_id,
|
|
1030
1763
|
candidate.candidate_index,
|
|
@@ -1032,6 +1765,7 @@ class PlaybookMixin:
|
|
|
1032
1765
|
_json_dumps(candidate.parent_candidate_ids) or "[]",
|
|
1033
1766
|
candidate.aggregate_score,
|
|
1034
1767
|
1 if candidate.is_winner else 0,
|
|
1768
|
+
candidate.metadata_json,
|
|
1035
1769
|
candidate.created_at,
|
|
1036
1770
|
),
|
|
1037
1771
|
)
|
|
@@ -1135,27 +1869,33 @@ class PlaybookMixin:
|
|
|
1135
1869
|
def delete_archived_agent_playbooks_by_playbook_name(
|
|
1136
1870
|
self, playbook_name: str, agent_version: str | None = None
|
|
1137
1871
|
) -> None:
|
|
1138
|
-
# Get IDs for FTS cleanup
|
|
1139
1872
|
sql = "SELECT agent_playbook_id FROM agent_playbooks WHERE playbook_name = ? AND status = 'archived'"
|
|
1140
1873
|
params: list[Any] = [playbook_name]
|
|
1141
1874
|
if agent_version is not None:
|
|
1142
1875
|
sql += " AND agent_version = ?"
|
|
1143
1876
|
params.append(agent_version)
|
|
1144
|
-
|
|
1145
|
-
|
|
1877
|
+
batch_request_id = uuid.uuid4().hex
|
|
1878
|
+
with self._lock:
|
|
1879
|
+
ids = [
|
|
1880
|
+
r["agent_playbook_id"]
|
|
1881
|
+
for r in self.conn.execute(sql, params).fetchall()
|
|
1882
|
+
]
|
|
1883
|
+
if not ids:
|
|
1884
|
+
return
|
|
1146
1885
|
ph = ",".join("?" for _ in ids)
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1886
|
+
self.conn.execute(
|
|
1887
|
+
f"DELETE FROM agent_playbooks WHERE agent_playbook_id IN ({ph})", ids
|
|
1888
|
+
)
|
|
1889
|
+
for apid in ids:
|
|
1890
|
+
_emit_hard_delete_playbook(
|
|
1891
|
+
self.conn,
|
|
1892
|
+
org_id=self.org_id,
|
|
1893
|
+
entity_type="agent_playbook",
|
|
1894
|
+
entity_id=str(apid),
|
|
1895
|
+
request_id=batch_request_id,
|
|
1150
1896
|
)
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
del_sql = "DELETE FROM agent_playbooks WHERE playbook_name = ? AND status = 'archived'"
|
|
1154
|
-
del_params: list[Any] = [playbook_name]
|
|
1155
|
-
if agent_version is not None:
|
|
1156
|
-
del_sql += " AND agent_version = ?"
|
|
1157
|
-
del_params.append(agent_version)
|
|
1158
|
-
self._execute(del_sql, del_params)
|
|
1897
|
+
self.conn.commit()
|
|
1898
|
+
self._delete_playbook_search_rows("agent", ids)
|
|
1159
1899
|
|
|
1160
1900
|
@SQLiteStorageBase.handle_exceptions
|
|
1161
1901
|
def search_agent_playbooks( # noqa: C901
|
|
@@ -1209,6 +1949,14 @@ class PlaybookMixin:
|
|
|
1209
1949
|
frag, sparams = _build_status_sql(status_filter)
|
|
1210
1950
|
conditions.append(frag)
|
|
1211
1951
|
params.extend(sparams)
|
|
1952
|
+
else:
|
|
1953
|
+
# Default: exclude tombstone statuses (MERGED/SUPERSEDED)
|
|
1954
|
+
conditions.append("(ap.status IS NULL OR ap.status NOT IN (?, ?))")
|
|
1955
|
+
params.extend(_TOMBSTONE_STATUS_VALUES)
|
|
1956
|
+
tag_frag, tag_params = _build_tags_sql("ap", request.tags)
|
|
1957
|
+
if tag_frag:
|
|
1958
|
+
conditions.append(tag_frag)
|
|
1959
|
+
params.extend(tag_params)
|
|
1212
1960
|
|
|
1213
1961
|
where_extra = (" AND " + " AND ".join(conditions)) if conditions else ""
|
|
1214
1962
|
overfetch = match_count * 5 if mode != SearchMode.FTS else match_count
|
|
@@ -1290,12 +2038,13 @@ class PlaybookMixin:
|
|
|
1290
2038
|
created_at_iso = _epoch_to_iso(result.created_at)
|
|
1291
2039
|
self._execute(
|
|
1292
2040
|
"""INSERT INTO agent_success_evaluation_result
|
|
1293
|
-
(session_id, agent_version, evaluation_name, is_success,
|
|
2041
|
+
(user_id, session_id, agent_version, evaluation_name, is_success,
|
|
1294
2042
|
failure_type, failure_reason, regular_vs_shadow,
|
|
1295
2043
|
number_of_correction_per_session, user_turns_to_resolution,
|
|
1296
2044
|
is_escalated, embedding, created_at)
|
|
1297
|
-
VALUES (
|
|
2045
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
1298
2046
|
(
|
|
2047
|
+
result.user_id,
|
|
1299
2048
|
result.session_id,
|
|
1300
2049
|
result.agent_version,
|
|
1301
2050
|
result.evaluation_name,
|
|
@@ -1327,6 +2076,46 @@ class PlaybookMixin:
|
|
|
1327
2076
|
rows = self._fetchall(sql, params)
|
|
1328
2077
|
return [_row_to_eval_result(r) for r in rows]
|
|
1329
2078
|
|
|
2079
|
+
@SQLiteStorageBase.handle_exceptions
|
|
2080
|
+
def get_agent_success_evaluation_results_in_window(
|
|
2081
|
+
self,
|
|
2082
|
+
from_ts: int,
|
|
2083
|
+
to_ts: int,
|
|
2084
|
+
agent_version: str | None = None,
|
|
2085
|
+
limit: int | None = None,
|
|
2086
|
+
) -> list[AgentSuccessEvaluationResult]:
|
|
2087
|
+
sql = """SELECT * FROM agent_success_evaluation_result
|
|
2088
|
+
WHERE created_at >= ? AND created_at <= ?"""
|
|
2089
|
+
params: list[Any] = [_epoch_to_iso(from_ts), _epoch_to_iso(to_ts)]
|
|
2090
|
+
if agent_version is not None:
|
|
2091
|
+
sql += " AND agent_version = ?"
|
|
2092
|
+
params.append(agent_version)
|
|
2093
|
+
sql += " ORDER BY created_at DESC"
|
|
2094
|
+
if limit is not None:
|
|
2095
|
+
sql += " LIMIT ?"
|
|
2096
|
+
params.append(limit)
|
|
2097
|
+
rows = self._fetchall(sql, params)
|
|
2098
|
+
return [_row_to_eval_result(r) for r in rows]
|
|
2099
|
+
|
|
2100
|
+
@SQLiteStorageBase.handle_exceptions
|
|
2101
|
+
def get_agent_success_evaluation_result_ids(
|
|
2102
|
+
self,
|
|
2103
|
+
user_id: str,
|
|
2104
|
+
session_id: str,
|
|
2105
|
+
evaluation_name: str,
|
|
2106
|
+
agent_version: str,
|
|
2107
|
+
) -> list[int]:
|
|
2108
|
+
rows = self._fetchall(
|
|
2109
|
+
"""SELECT result_id FROM agent_success_evaluation_result
|
|
2110
|
+
WHERE user_id = ?
|
|
2111
|
+
AND session_id = ?
|
|
2112
|
+
AND evaluation_name = ?
|
|
2113
|
+
AND agent_version = ?
|
|
2114
|
+
ORDER BY created_at DESC""",
|
|
2115
|
+
(user_id, session_id, evaluation_name, agent_version),
|
|
2116
|
+
)
|
|
2117
|
+
return [int(r["result_id"]) for r in rows]
|
|
2118
|
+
|
|
1330
2119
|
@SQLiteStorageBase.handle_exceptions
|
|
1331
2120
|
def delete_all_agent_success_evaluation_results(self) -> None:
|
|
1332
2121
|
self._execute("DELETE FROM agent_success_evaluation_result")
|
|
@@ -1334,13 +2123,15 @@ class PlaybookMixin:
|
|
|
1334
2123
|
@SQLiteStorageBase.handle_exceptions
|
|
1335
2124
|
def delete_agent_success_evaluation_results_for_session(
|
|
1336
2125
|
self,
|
|
2126
|
+
user_id: str,
|
|
1337
2127
|
session_id: str,
|
|
1338
2128
|
evaluation_name: str,
|
|
1339
2129
|
agent_version: str,
|
|
1340
2130
|
) -> int:
|
|
1341
|
-
"""Delete results scoped to (session_id, evaluation_name, agent_version).
|
|
2131
|
+
"""Delete results scoped to (user_id, session_id, evaluation_name, agent_version).
|
|
1342
2132
|
|
|
1343
2133
|
Args:
|
|
2134
|
+
user_id (str): User whose session results to clear.
|
|
1344
2135
|
session_id (str): Session whose results to clear.
|
|
1345
2136
|
evaluation_name (str): Which evaluator's results to clear.
|
|
1346
2137
|
agent_version (str): Agent version scope.
|
|
@@ -1350,8 +2141,11 @@ class PlaybookMixin:
|
|
|
1350
2141
|
"""
|
|
1351
2142
|
cur = self._execute(
|
|
1352
2143
|
"""DELETE FROM agent_success_evaluation_result
|
|
1353
|
-
WHERE
|
|
1354
|
-
|
|
2144
|
+
WHERE user_id = ?
|
|
2145
|
+
AND session_id = ?
|
|
2146
|
+
AND evaluation_name = ?
|
|
2147
|
+
AND agent_version = ?""",
|
|
2148
|
+
(user_id, session_id, evaluation_name, agent_version),
|
|
1355
2149
|
)
|
|
1356
2150
|
return cur.rowcount
|
|
1357
2151
|
|