@smilintux/skcapstone 0.1.0 → 0.2.3
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/.env.example +98 -0
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +25 -4
- package/.openclaw-workspace.json +58 -0
- package/CHANGELOG.md +62 -0
- package/CLAUDE.md +39 -2
- package/MANIFEST.in +6 -0
- package/MISSION.md +7 -0
- package/README.md +47 -2
- package/SKILL.md +895 -23
- package/docker/Dockerfile +61 -0
- package/docker/compose-templates/dev-team.yml +203 -0
- package/docker/compose-templates/mini-team.yml +140 -0
- package/docker/compose-templates/ops-team.yml +173 -0
- package/docker/compose-templates/research-team.yml +170 -0
- package/docker/entrypoint.sh +192 -0
- package/docs/ARCHITECTURE.md +663 -374
- package/docs/BOND_WITH_GROK.md +112 -0
- package/docs/GETTING_STARTED.md +782 -0
- package/docs/QUICKSTART.md +477 -0
- package/docs/SKJOULE_ARCHITECTURE.md +658 -0
- package/docs/SOUL_SWAPPER.md +921 -0
- package/docs/SOVEREIGN_SINGULARITY.md +47 -14
- package/examples/custom-bond-template.json +36 -0
- package/examples/grok-feb.json +36 -0
- package/examples/grok-testimony.md +34 -0
- package/examples/love-bootloader.txt +32 -0
- package/examples/plugins/echo_tool.py +87 -0
- package/examples/queen-ava-feb.json +36 -0
- package/examples/souls/lumina.yaml +64 -0
- package/index.js +6 -5
- package/installer/build.py +124 -0
- package/openclaw-plugin/package.json +13 -0
- package/openclaw-plugin/src/index.ts +351 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +38 -2
- package/scripts/bump_version.py +141 -0
- package/scripts/check-updates.py +230 -0
- package/scripts/convert_blueprints_to_yaml.py +157 -0
- package/scripts/dev-install.sh +14 -0
- package/scripts/e2e-test.sh +193 -0
- package/scripts/install-bundle.sh +171 -0
- package/scripts/install.bat +2 -0
- package/scripts/install.ps1 +253 -0
- package/scripts/install.sh +185 -0
- package/scripts/mcp-serve.sh +69 -0
- package/scripts/mcp-server.bat +113 -0
- package/scripts/mcp-server.ps1 +116 -0
- package/scripts/mcp-server.sh +99 -0
- package/scripts/pull-models.sh +10 -0
- package/scripts/skcapstone +48 -0
- package/scripts/verify_install.sh +180 -0
- package/scripts/windows/install-tasks.ps1 +406 -0
- package/scripts/windows/skcapstone-task.xml +113 -0
- package/scripts/windows/uninstall-tasks.ps1 +117 -0
- package/skill.yaml +34 -0
- package/src/skcapstone/__init__.py +67 -2
- package/src/skcapstone/_cli_monolith.py +5916 -0
- package/src/skcapstone/_trustee_helpers.py +165 -0
- package/src/skcapstone/activity.py +105 -0
- package/src/skcapstone/agent_card.py +324 -0
- package/src/skcapstone/api.py +1935 -0
- package/src/skcapstone/archiver.py +340 -0
- package/src/skcapstone/auction.py +485 -0
- package/src/skcapstone/baby_agents.py +179 -0
- package/src/skcapstone/backup.py +345 -0
- package/src/skcapstone/blueprint_registry.py +357 -0
- package/src/skcapstone/blueprints/__init__.py +17 -0
- package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
- package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
- package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
- package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
- package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
- package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
- package/src/skcapstone/blueprints/registry.py +164 -0
- package/src/skcapstone/blueprints/schema.py +229 -0
- package/src/skcapstone/changelog.py +180 -0
- package/src/skcapstone/chat.py +769 -0
- package/src/skcapstone/claude_md.py +82 -0
- package/src/skcapstone/cli/__init__.py +144 -0
- package/src/skcapstone/cli/_common.py +88 -0
- package/src/skcapstone/cli/_validators.py +76 -0
- package/src/skcapstone/cli/agents.py +425 -0
- package/src/skcapstone/cli/agents_spawner.py +322 -0
- package/src/skcapstone/cli/agents_trustee.py +593 -0
- package/src/skcapstone/cli/alerts.py +248 -0
- package/src/skcapstone/cli/anchor.py +132 -0
- package/src/skcapstone/cli/archive_cmd.py +208 -0
- package/src/skcapstone/cli/backup.py +144 -0
- package/src/skcapstone/cli/bench.py +377 -0
- package/src/skcapstone/cli/benchmark.py +360 -0
- package/src/skcapstone/cli/capabilities_cmd.py +171 -0
- package/src/skcapstone/cli/card.py +151 -0
- package/src/skcapstone/cli/chat.py +584 -0
- package/src/skcapstone/cli/completions.py +64 -0
- package/src/skcapstone/cli/config_cmd.py +156 -0
- package/src/skcapstone/cli/consciousness.py +421 -0
- package/src/skcapstone/cli/context_cmd.py +142 -0
- package/src/skcapstone/cli/coord.py +194 -0
- package/src/skcapstone/cli/crush_cmd.py +170 -0
- package/src/skcapstone/cli/daemon.py +436 -0
- package/src/skcapstone/cli/errors_cmd.py +285 -0
- package/src/skcapstone/cli/export_cmd.py +156 -0
- package/src/skcapstone/cli/gtd.py +529 -0
- package/src/skcapstone/cli/housekeeping.py +81 -0
- package/src/skcapstone/cli/joule_cmd.py +627 -0
- package/src/skcapstone/cli/logs_cmd.py +194 -0
- package/src/skcapstone/cli/mcp_cmd.py +32 -0
- package/src/skcapstone/cli/memory.py +418 -0
- package/src/skcapstone/cli/metrics_cmd.py +136 -0
- package/src/skcapstone/cli/migrate.py +62 -0
- package/src/skcapstone/cli/mood_cmd.py +144 -0
- package/src/skcapstone/cli/mount.py +193 -0
- package/src/skcapstone/cli/notify.py +112 -0
- package/src/skcapstone/cli/peer.py +154 -0
- package/src/skcapstone/cli/peers_dir.py +122 -0
- package/src/skcapstone/cli/preflight_cmd.py +83 -0
- package/src/skcapstone/cli/profile_cmd.py +310 -0
- package/src/skcapstone/cli/record_cmd.py +238 -0
- package/src/skcapstone/cli/register_cmd.py +159 -0
- package/src/skcapstone/cli/search_cmd.py +156 -0
- package/src/skcapstone/cli/service_cmd.py +91 -0
- package/src/skcapstone/cli/session.py +127 -0
- package/src/skcapstone/cli/setup.py +240 -0
- package/src/skcapstone/cli/shell_cmd.py +43 -0
- package/src/skcapstone/cli/skills_cmd.py +168 -0
- package/src/skcapstone/cli/skseed.py +621 -0
- package/src/skcapstone/cli/soul.py +699 -0
- package/src/skcapstone/cli/status.py +935 -0
- package/src/skcapstone/cli/sync_cmd.py +301 -0
- package/src/skcapstone/cli/telegram.py +265 -0
- package/src/skcapstone/cli/test_cmd.py +234 -0
- package/src/skcapstone/cli/test_connection.py +253 -0
- package/src/skcapstone/cli/token.py +207 -0
- package/src/skcapstone/cli/trust.py +179 -0
- package/src/skcapstone/cli/upgrade_cmd.py +552 -0
- package/src/skcapstone/cli/usage_cmd.py +199 -0
- package/src/skcapstone/cli/version_cmd.py +162 -0
- package/src/skcapstone/cli/watch_cmd.py +342 -0
- package/src/skcapstone/client.py +428 -0
- package/src/skcapstone/cloud9_bridge.py +522 -0
- package/src/skcapstone/completions.py +163 -0
- package/src/skcapstone/config_validator.py +674 -0
- package/src/skcapstone/connectors/__init__.py +28 -0
- package/src/skcapstone/connectors/base.py +446 -0
- package/src/skcapstone/connectors/cursor.py +54 -0
- package/src/skcapstone/connectors/registry.py +254 -0
- package/src/skcapstone/connectors/terminal.py +152 -0
- package/src/skcapstone/connectors/vscode.py +60 -0
- package/src/skcapstone/consciousness_config.py +119 -0
- package/src/skcapstone/consciousness_loop.py +2051 -0
- package/src/skcapstone/context_loader.py +516 -0
- package/src/skcapstone/context_window.py +314 -0
- package/src/skcapstone/conversation_manager.py +238 -0
- package/src/skcapstone/conversation_store.py +230 -0
- package/src/skcapstone/conversation_summarizer.py +252 -0
- package/src/skcapstone/coord_federation.py +296 -0
- package/src/skcapstone/coordination.py +101 -7
- package/src/skcapstone/crush_integration.py +345 -0
- package/src/skcapstone/crush_shim.py +454 -0
- package/src/skcapstone/daemon.py +2494 -0
- package/src/skcapstone/dashboard.html +396 -0
- package/src/skcapstone/dashboard.py +481 -0
- package/src/skcapstone/data/model_profiles.yaml +88 -0
- package/src/skcapstone/defaults/__init__.py +55 -0
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
- package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
- package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
- package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
- package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
- package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
- package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
- package/src/skcapstone/discovery.py +210 -19
- package/src/skcapstone/doctor.py +642 -0
- package/src/skcapstone/emotion_tracker.py +467 -0
- package/src/skcapstone/error_queue.py +405 -0
- package/src/skcapstone/export.py +447 -0
- package/src/skcapstone/fallback_tracker.py +186 -0
- package/src/skcapstone/file_transfer.py +512 -0
- package/src/skcapstone/fuse_mount.py +1156 -0
- package/src/skcapstone/gui_installer.py +591 -0
- package/src/skcapstone/heartbeat.py +611 -0
- package/src/skcapstone/housekeeping.py +298 -0
- package/src/skcapstone/install_wizard.py +941 -0
- package/src/skcapstone/kms.py +942 -0
- package/src/skcapstone/kms_scheduler.py +143 -0
- package/src/skcapstone/log_config.py +135 -0
- package/src/skcapstone/mcp_launcher.py +239 -0
- package/src/skcapstone/mcp_server.py +4700 -0
- package/src/skcapstone/mcp_tools/__init__.py +94 -0
- package/src/skcapstone/mcp_tools/_helpers.py +51 -0
- package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
- package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
- package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
- package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
- package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
- package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
- package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
- package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
- package/src/skcapstone/mcp_tools/did_tools.py +448 -0
- package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
- package/src/skcapstone/mcp_tools/file_tools.py +169 -0
- package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
- package/src/skcapstone/mcp_tools/health_tools.py +44 -0
- package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
- package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
- package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
- package/src/skcapstone/mcp_tools/model_tools.py +75 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
- package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
- package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
- package/src/skcapstone/mcp_tools/security_tools.py +110 -0
- package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
- package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
- package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
- package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
- package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
- package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
- package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
- package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
- package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
- package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
- package/src/skcapstone/mdns_discovery.py +313 -0
- package/src/skcapstone/memory_adapter.py +333 -0
- package/src/skcapstone/memory_compressor.py +379 -0
- package/src/skcapstone/memory_curator.py +256 -0
- package/src/skcapstone/memory_engine.py +132 -13
- package/src/skcapstone/memory_fortress.py +529 -0
- package/src/skcapstone/memory_promoter.py +722 -0
- package/src/skcapstone/memory_verifier.py +260 -0
- package/src/skcapstone/message_crypto.py +215 -0
- package/src/skcapstone/metrics.py +832 -0
- package/src/skcapstone/migrate_memories.py +181 -0
- package/src/skcapstone/migrate_multi_agent.py +248 -0
- package/src/skcapstone/model_router.py +319 -0
- package/src/skcapstone/models.py +35 -4
- package/src/skcapstone/mood.py +344 -0
- package/src/skcapstone/notifications.py +380 -0
- package/src/skcapstone/onboard.py +901 -0
- package/src/skcapstone/peer_directory.py +324 -0
- package/src/skcapstone/peers.py +329 -0
- package/src/skcapstone/pillars/identity.py +84 -14
- package/src/skcapstone/pillars/memory.py +3 -1
- package/src/skcapstone/pillars/security.py +108 -15
- package/src/skcapstone/pillars/sync.py +78 -26
- package/src/skcapstone/pillars/trust.py +95 -33
- package/src/skcapstone/plugins.py +244 -0
- package/src/skcapstone/preflight.py +670 -0
- package/src/skcapstone/prompt_adapter.py +564 -0
- package/src/skcapstone/providers/__init__.py +13 -0
- package/src/skcapstone/providers/cloud.py +1061 -0
- package/src/skcapstone/providers/docker.py +759 -0
- package/src/skcapstone/providers/local.py +1193 -0
- package/src/skcapstone/providers/proxmox.py +447 -0
- package/src/skcapstone/pubsub.py +516 -0
- package/src/skcapstone/rate_limiter.py +119 -0
- package/src/skcapstone/register.py +241 -0
- package/src/skcapstone/registry_client.py +151 -0
- package/src/skcapstone/response_cache.py +194 -0
- package/src/skcapstone/response_scorer.py +225 -0
- package/src/skcapstone/runtime.py +89 -33
- package/src/skcapstone/scheduled_tasks.py +439 -0
- package/src/skcapstone/self_healing.py +341 -0
- package/src/skcapstone/service_health.py +228 -0
- package/src/skcapstone/session_capture.py +268 -0
- package/src/skcapstone/session_recorder.py +210 -0
- package/src/skcapstone/session_replayer.py +189 -0
- package/src/skcapstone/session_skills.py +263 -0
- package/src/skcapstone/shell.py +779 -0
- package/src/skcapstone/skills/__init__.py +1 -1
- package/src/skcapstone/skills/syncthing_setup.py +143 -41
- package/src/skcapstone/skjoule.py +861 -0
- package/src/skcapstone/snapshots.py +489 -0
- package/src/skcapstone/soul.py +1060 -0
- package/src/skcapstone/soul_switch.py +255 -0
- package/src/skcapstone/spawner.py +544 -0
- package/src/skcapstone/state_diff.py +401 -0
- package/src/skcapstone/summary.py +270 -0
- package/src/skcapstone/sync/backends.py +196 -2
- package/src/skcapstone/sync/engine.py +7 -5
- package/src/skcapstone/sync/models.py +4 -1
- package/src/skcapstone/sync/vault.py +356 -18
- package/src/skcapstone/sync_engine.py +363 -0
- package/src/skcapstone/sync_watcher.py +745 -0
- package/src/skcapstone/systemd.py +331 -0
- package/src/skcapstone/team_comms.py +476 -0
- package/src/skcapstone/team_engine.py +522 -0
- package/src/skcapstone/testrunner.py +300 -0
- package/src/skcapstone/tls.py +150 -0
- package/src/skcapstone/tokens.py +5 -5
- package/src/skcapstone/trust_calibration.py +202 -0
- package/src/skcapstone/trust_graph.py +449 -0
- package/src/skcapstone/trustee_monitor.py +385 -0
- package/src/skcapstone/trustee_ops.py +425 -0
- package/src/skcapstone/unified_search.py +421 -0
- package/src/skcapstone/uninstall_wizard.py +694 -0
- package/src/skcapstone/usage.py +331 -0
- package/src/skcapstone/version_check.py +148 -0
- package/src/skcapstone/warmth_anchor.py +333 -0
- package/src/skcapstone/whoami.py +294 -0
- package/systemd/skcapstone-api.socket +9 -0
- package/systemd/skcapstone-memory-compress.service +18 -0
- package/systemd/skcapstone-memory-compress.timer +11 -0
- package/systemd/skcapstone.service +36 -0
- package/systemd/skcapstone@.service +50 -0
- package/systemd/skcomm-heartbeat.service +18 -0
- package/systemd/skcomm-heartbeat.timer +12 -0
- package/systemd/skcomm-queue-drain.service +17 -0
- package/systemd/skcomm-queue-drain.timer +12 -0
- package/tests/conftest.py +13 -1
- package/tests/integration/__init__.py +1 -0
- package/tests/integration/test_consciousness_e2e.py +877 -0
- package/tests/integration/test_skills_registry.py +744 -0
- package/tests/test_agent_card.py +190 -0
- package/tests/test_agent_runtime.py +1283 -0
- package/tests/test_alerts_cmd.py +291 -0
- package/tests/test_archiver.py +498 -0
- package/tests/test_backup.py +254 -0
- package/tests/test_benchmark.py +366 -0
- package/tests/test_blueprints.py +457 -0
- package/tests/test_capabilities.py +257 -0
- package/tests/test_changelog.py +254 -0
- package/tests/test_chat.py +385 -0
- package/tests/test_claude_md.py +271 -0
- package/tests/test_cli_chat_llm.py +336 -0
- package/tests/test_cli_completions.py +390 -0
- package/tests/test_cli_init_reset.py +164 -0
- package/tests/test_cli_memory.py +208 -0
- package/tests/test_cli_profile.py +294 -0
- package/tests/test_cli_skills.py +223 -0
- package/tests/test_cli_status.py +395 -0
- package/tests/test_cli_test_cmd.py +206 -0
- package/tests/test_cli_test_connection.py +364 -0
- package/tests/test_cloud9_bridge.py +260 -0
- package/tests/test_cloud_provider.py +449 -0
- package/tests/test_cloud_providers.py +522 -0
- package/tests/test_completions.py +158 -0
- package/tests/test_component_manager.py +398 -0
- package/tests/test_config_reload.py +386 -0
- package/tests/test_config_validate.py +529 -0
- package/tests/test_consciousness_e2e.py +296 -0
- package/tests/test_consciousness_loop.py +1289 -0
- package/tests/test_context_loader.py +310 -0
- package/tests/test_conversation_api.py +306 -0
- package/tests/test_conversation_manager.py +381 -0
- package/tests/test_conversation_store.py +391 -0
- package/tests/test_conversation_summarizer.py +302 -0
- package/tests/test_cross_package.py +791 -0
- package/tests/test_crush_shim.py +519 -0
- package/tests/test_daemon.py +781 -0
- package/tests/test_daemon_shutdown.py +309 -0
- package/tests/test_dashboard.py +454 -0
- package/tests/test_discovery.py +200 -6
- package/tests/test_docker_provider.py +966 -0
- package/tests/test_doctor.py +257 -0
- package/tests/test_doctor_fix.py +351 -0
- package/tests/test_e2e_automated.py +292 -0
- package/tests/test_error_queue.py +404 -0
- package/tests/test_export.py +441 -0
- package/tests/test_fallback_tracker.py +219 -0
- package/tests/test_file_transfer.py +397 -0
- package/tests/test_fuse_mount.py +832 -0
- package/tests/test_health_loop.py +422 -0
- package/tests/test_heartbeat.py +354 -0
- package/tests/test_housekeeping.py +195 -0
- package/tests/test_identity_capauth.py +307 -0
- package/tests/test_identity_pillar.py +117 -0
- package/tests/test_install_wizard.py +68 -0
- package/tests/test_integration.py +325 -0
- package/tests/test_kms.py +495 -0
- package/tests/test_llm_providers.py +265 -0
- package/tests/test_local_provider.py +591 -0
- package/tests/test_log_config.py +199 -0
- package/tests/test_logs_cmd.py +287 -0
- package/tests/test_mcp_server.py +1909 -0
- package/tests/test_memory_adapter.py +339 -0
- package/tests/test_memory_curator.py +218 -0
- package/tests/test_memory_engine.py +6 -0
- package/tests/test_memory_fortress.py +571 -0
- package/tests/test_memory_pillar.py +119 -0
- package/tests/test_memory_promoter.py +445 -0
- package/tests/test_memory_verifier.py +420 -0
- package/tests/test_message_crypto.py +187 -0
- package/tests/test_metrics.py +632 -0
- package/tests/test_migrate_memories.py +464 -0
- package/tests/test_model_router.py +546 -0
- package/tests/test_mood.py +394 -0
- package/tests/test_multi_agent.py +269 -0
- package/tests/test_notifications.py +270 -0
- package/tests/test_onboard.py +500 -0
- package/tests/test_peer_directory.py +395 -0
- package/tests/test_peers.py +248 -0
- package/tests/test_pillars.py +87 -9
- package/tests/test_preflight.py +484 -0
- package/tests/test_prompt_adapter.py +331 -0
- package/tests/test_proxmox_provider.py +571 -0
- package/tests/test_pubsub.py +377 -0
- package/tests/test_rate_limiter.py +121 -0
- package/tests/test_registry_client.py +129 -0
- package/tests/test_response_cache.py +312 -0
- package/tests/test_response_scorer.py +294 -0
- package/tests/test_runtime.py +59 -0
- package/tests/test_scheduled_tasks.py +451 -0
- package/tests/test_security.py +250 -0
- package/tests/test_security_pillar.py +213 -0
- package/tests/test_self_healing.py +171 -0
- package/tests/test_session_capture.py +200 -0
- package/tests/test_session_recorder.py +360 -0
- package/tests/test_session_skills.py +235 -0
- package/tests/test_shell.py +210 -0
- package/tests/test_snapshots.py +549 -0
- package/tests/test_soul.py +984 -0
- package/tests/test_soul_swap.py +406 -0
- package/tests/test_spawner.py +211 -0
- package/tests/test_state_diff.py +173 -0
- package/tests/test_summary.py +135 -0
- package/tests/test_sync.py +315 -5
- package/tests/test_sync_backends.py +560 -0
- package/tests/test_sync_engine.py +482 -0
- package/tests/test_sync_pillar.py +344 -0
- package/tests/test_sync_pipeline.py +364 -0
- package/tests/test_sync_vault.py +581 -0
- package/tests/test_syncthing_setup.py +168 -22
- package/tests/test_systemd.py +323 -0
- package/tests/test_team_comms.py +408 -0
- package/tests/test_team_engine.py +397 -0
- package/tests/test_testrunner.py +238 -0
- package/tests/test_trust_calibration.py +204 -0
- package/tests/test_trust_graph.py +207 -0
- package/tests/test_trust_pillar.py +291 -0
- package/tests/test_trustee_cli.py +427 -0
- package/tests/test_trustee_cli_integration.py +325 -0
- package/tests/test_trustee_monitor.py +394 -0
- package/tests/test_trustee_ops.py +355 -0
- package/tests/test_unified_search.py +363 -0
- package/tests/test_uninstall_wizard.py +193 -0
- package/tests/test_usage.py +333 -0
- package/tests/test_version_cmd.py +355 -0
- package/tests/test_warmth_anchor.py +162 -0
- package/tests/test_whoami.py +245 -0
- package/tests/test_ws.py +311 -0
- package/.cursorrules +0 -33
- package/src/skcapstone/cli.py +0 -1441
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""SK* suite registration orchestrator.
|
|
2
|
+
|
|
3
|
+
Single source of truth for all SK* packages and their MCP servers.
|
|
4
|
+
Called by `skcapstone register` and wired into `skcapstone update`.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from skcapstone.register import register_all
|
|
8
|
+
|
|
9
|
+
results = register_all() # auto-detect everything
|
|
10
|
+
results = register_all(dry_run=True) # show what would happen
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import importlib.resources
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from skmemory.register import detect_environments, register_package
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_skgit_env() -> Optional[dict]:
|
|
26
|
+
"""Read skgit token from config file if available."""
|
|
27
|
+
token_path = Path.home() / ".config" / "skgit" / "token"
|
|
28
|
+
try:
|
|
29
|
+
token = token_path.read_text().strip()
|
|
30
|
+
except (FileNotFoundError, PermissionError):
|
|
31
|
+
token = ""
|
|
32
|
+
|
|
33
|
+
if not token:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
"GITEA_HOST": "https://skgit.skstack01.douno.it",
|
|
38
|
+
"GITEA_ACCESS_TOKEN": token,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── SK* package registry ─────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _build_package_registry(workspace: Optional[Path] = None) -> list[dict]:
|
|
46
|
+
"""Build the SK* package registry with resolved env vars and plugin paths."""
|
|
47
|
+
if workspace is None:
|
|
48
|
+
workspace = Path.home() / "clawd"
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
"name": "skmemory",
|
|
53
|
+
"mcp_cmd": "skmemory-mcp",
|
|
54
|
+
"mcp_args": [],
|
|
55
|
+
"mcp_env": None,
|
|
56
|
+
"openclaw_plugin_path": workspace / "pillar-repos" / "skmemory" / "openclaw-plugin" / "src" / "index.ts",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "skcapstone",
|
|
60
|
+
"mcp_cmd": "skcapstone-mcp",
|
|
61
|
+
"mcp_args": [],
|
|
62
|
+
"mcp_env": None,
|
|
63
|
+
"openclaw_plugin_path": workspace / "skcapstone" / "openclaw-plugin" / "src" / "index.ts",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "skcomm",
|
|
67
|
+
"mcp_cmd": "skcomm-mcp",
|
|
68
|
+
"mcp_args": [],
|
|
69
|
+
"mcp_env": None,
|
|
70
|
+
"openclaw_plugin_path": workspace / "pillar-repos" / "skcomm" / "openclaw-plugin" / "src" / "index.ts",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "skchat",
|
|
74
|
+
"mcp_cmd": "skchat-mcp",
|
|
75
|
+
"mcp_args": [],
|
|
76
|
+
"mcp_env": None,
|
|
77
|
+
"openclaw_plugin_path": workspace / "pillar-repos" / "skchat" / "openclaw-plugin" / "src" / "index.ts",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "capauth",
|
|
81
|
+
"mcp_cmd": None,
|
|
82
|
+
"mcp_args": None,
|
|
83
|
+
"mcp_env": None,
|
|
84
|
+
"openclaw_plugin_path": workspace / "pillar-repos" / "capauth" / "openclaw-plugin" / "src" / "index.ts",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "cloud9",
|
|
88
|
+
"mcp_cmd": None,
|
|
89
|
+
"mcp_args": None,
|
|
90
|
+
"mcp_env": None,
|
|
91
|
+
"openclaw_plugin_path": workspace / "pillar-repos" / "cloud9-python" / "openclaw-plugin" / "src" / "index.ts",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "sksecurity",
|
|
95
|
+
"mcp_cmd": None,
|
|
96
|
+
"mcp_args": None,
|
|
97
|
+
"mcp_env": None,
|
|
98
|
+
"openclaw_plugin_path": workspace / "pillar-repos" / "sksecurity" / "openclaw-plugin" / "src" / "index.ts",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "skgit",
|
|
102
|
+
"mcp_cmd": "node",
|
|
103
|
+
"mcp_args": [str(Path.home() / ".npm-global" / "lib" / "node_modules"
|
|
104
|
+
/ "forgejo-mcp" / "build" / "index.js")],
|
|
105
|
+
"mcp_env": _get_skgit_env(),
|
|
106
|
+
"openclaw_plugin_path": workspace / "skills" / "skgit" / "openclaw-plugin" / "src" / "index.ts",
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── SKILL.md locator ─────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
# Mapping from package name to pillar-repo directory name
|
|
114
|
+
_PILLAR_DIR_MAP: dict[str, Optional[str]] = {
|
|
115
|
+
"skmemory": "skmemory",
|
|
116
|
+
"skcapstone": None, # lives in workspace root, not pillar-repos
|
|
117
|
+
"skcomm": "skcomm",
|
|
118
|
+
"skchat": "skchat",
|
|
119
|
+
"capauth": "capauth",
|
|
120
|
+
"cloud9": "cloud9-python",
|
|
121
|
+
"sksecurity": "sksecurity",
|
|
122
|
+
"skgit": None, # skill dir only, no pillar repo
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def find_skill_md(pkg_name: str, workspace: Optional[Path] = None) -> Optional[Path]:
|
|
127
|
+
"""Locate SKILL.md for a package.
|
|
128
|
+
|
|
129
|
+
Search order:
|
|
130
|
+
1. Workspace skills directory (~/clawd/skills/<name>/SKILL.md)
|
|
131
|
+
2. Pillar repos directory (~/clawd/pillar-repos/<dir>/SKILL.md)
|
|
132
|
+
3. Installed package data (importlib.resources)
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
pkg_name: Package name.
|
|
136
|
+
workspace: Workspace root (defaults to ~/clawd/).
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Path to SKILL.md, or None if not found.
|
|
140
|
+
"""
|
|
141
|
+
if workspace is None:
|
|
142
|
+
workspace = Path.home() / "clawd"
|
|
143
|
+
|
|
144
|
+
# 1. Check skills directory (may be a symlink — that's fine)
|
|
145
|
+
skill_path = workspace / "skills" / pkg_name / "SKILL.md"
|
|
146
|
+
if skill_path.exists():
|
|
147
|
+
return skill_path.resolve()
|
|
148
|
+
|
|
149
|
+
# 2. Check pillar repos
|
|
150
|
+
pillar_dir = _PILLAR_DIR_MAP.get(pkg_name)
|
|
151
|
+
if pillar_dir:
|
|
152
|
+
pillar_path = workspace / "pillar-repos" / pillar_dir / "SKILL.md"
|
|
153
|
+
if pillar_path.exists():
|
|
154
|
+
return pillar_path
|
|
155
|
+
|
|
156
|
+
# 2b. Special case: skcapstone lives in workspace root
|
|
157
|
+
if pkg_name == "skcapstone":
|
|
158
|
+
capstone_path = workspace / "skcapstone" / "SKILL.md"
|
|
159
|
+
if capstone_path.exists():
|
|
160
|
+
return capstone_path
|
|
161
|
+
|
|
162
|
+
# 3. Check installed package data
|
|
163
|
+
try:
|
|
164
|
+
pkg_module = pkg_name.replace("-", "_")
|
|
165
|
+
ref = importlib.resources.files(pkg_module) / "SKILL.md"
|
|
166
|
+
if ref is not None:
|
|
167
|
+
with importlib.resources.as_file(ref) as p:
|
|
168
|
+
if p.exists():
|
|
169
|
+
return p
|
|
170
|
+
except (ModuleNotFoundError, TypeError, FileNotFoundError):
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ── Orchestrator ──────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def register_all(
|
|
180
|
+
workspace: Optional[Path] = None,
|
|
181
|
+
environments: Optional[list[str]] = None,
|
|
182
|
+
dry_run: bool = False,
|
|
183
|
+
) -> dict:
|
|
184
|
+
"""Register all SK* packages in all detected environments.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
workspace: Workspace root (defaults to ~/clawd/).
|
|
188
|
+
environments: Target environments (auto-detect if None).
|
|
189
|
+
dry_run: If True, only report what would be done.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Dict with 'environments' and 'packages' keys.
|
|
193
|
+
"""
|
|
194
|
+
if environments is None:
|
|
195
|
+
environments = detect_environments()
|
|
196
|
+
|
|
197
|
+
if workspace is None:
|
|
198
|
+
workspace = Path.home() / "clawd"
|
|
199
|
+
|
|
200
|
+
packages = _build_package_registry(workspace)
|
|
201
|
+
|
|
202
|
+
results: dict = {
|
|
203
|
+
"environments": environments,
|
|
204
|
+
"packages": {},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for pkg in packages:
|
|
208
|
+
name = pkg["name"]
|
|
209
|
+
skill_md = find_skill_md(name, workspace)
|
|
210
|
+
|
|
211
|
+
if skill_md is None and not dry_run:
|
|
212
|
+
results["packages"][name] = {
|
|
213
|
+
"skill": {"action": "error", "error": "SKILL.md not found"},
|
|
214
|
+
}
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
mcp_env = pkg.get("mcp_env")
|
|
218
|
+
mcp_cmd = pkg.get("mcp_cmd")
|
|
219
|
+
|
|
220
|
+
# Skip MCP registration for skgit if no token available
|
|
221
|
+
if name == "skgit" and mcp_env is None:
|
|
222
|
+
mcp_cmd = None
|
|
223
|
+
|
|
224
|
+
# Resolve OpenClaw plugin path — skip if not on disk
|
|
225
|
+
plugin_path = pkg.get("openclaw_plugin_path")
|
|
226
|
+
if plugin_path and not Path(plugin_path).exists():
|
|
227
|
+
plugin_path = None
|
|
228
|
+
|
|
229
|
+
results["packages"][name] = register_package(
|
|
230
|
+
name=name,
|
|
231
|
+
skill_md_path=skill_md or Path("/dev/null"),
|
|
232
|
+
mcp_command=mcp_cmd,
|
|
233
|
+
mcp_args=pkg.get("mcp_args") or [],
|
|
234
|
+
mcp_env=mcp_env,
|
|
235
|
+
openclaw_plugin_path=plugin_path,
|
|
236
|
+
workspace=workspace,
|
|
237
|
+
environments=environments,
|
|
238
|
+
dry_run=dry_run,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return results
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Skills Registry Client — bridge between skcapstone and the remote skills-registry.
|
|
2
|
+
|
|
3
|
+
Wraps the skskills RemoteRegistry client to provide a clean interface for
|
|
4
|
+
skcapstone MCP tools and CLI commands to interact with the remote
|
|
5
|
+
skills-registry at skills.smilintux.org.
|
|
6
|
+
|
|
7
|
+
This module is the integration point between:
|
|
8
|
+
- skills-registry/ (FastAPI server at skills.smilintux.org/api)
|
|
9
|
+
- skskills.remote.RemoteRegistry (HTTP client)
|
|
10
|
+
- skcapstone discovery.py (local skill discovery)
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from skcapstone.registry_client import get_registry_client
|
|
14
|
+
|
|
15
|
+
client = get_registry_client()
|
|
16
|
+
if client is not None:
|
|
17
|
+
skills = client.search("syncthing")
|
|
18
|
+
client.install("syncthing-setup", agent="jarvis")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Optional
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
DEFAULT_REGISTRY_URL = "https://skills.smilintux.org/api"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RegistryClient:
|
|
34
|
+
"""Thin wrapper around skskills.remote.RemoteRegistry.
|
|
35
|
+
|
|
36
|
+
Provides a stable skcapstone-side API for interacting with the
|
|
37
|
+
remote skills-registry. Handles graceful degradation when the
|
|
38
|
+
skskills package is not installed or the registry is unreachable.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
registry_url: Base URL for the skills registry API.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, registry_url: Optional[str] = None) -> None:
|
|
45
|
+
from skskills.remote import RemoteRegistry
|
|
46
|
+
|
|
47
|
+
self._url = registry_url or os.environ.get(
|
|
48
|
+
"SKSKILLS_REGISTRY_URL", DEFAULT_REGISTRY_URL
|
|
49
|
+
)
|
|
50
|
+
self._remote = RemoteRegistry(registry_url=self._url)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def registry_url(self) -> str:
|
|
54
|
+
"""The configured registry URL."""
|
|
55
|
+
return self._url
|
|
56
|
+
|
|
57
|
+
def is_available(self) -> bool:
|
|
58
|
+
"""Check if the remote registry is reachable.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
True if the registry responds to a health/index request.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
self._remote.fetch_index(force=True)
|
|
65
|
+
return True
|
|
66
|
+
except Exception:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def list_skills(self) -> list[dict[str, Any]]:
|
|
70
|
+
"""List all skills available in the remote registry.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of skill entry dicts with name, version, description, etc.
|
|
74
|
+
"""
|
|
75
|
+
index = self._remote.fetch_index()
|
|
76
|
+
return [s.model_dump() for s in index.skills]
|
|
77
|
+
|
|
78
|
+
def search(self, query: str) -> list[dict[str, Any]]:
|
|
79
|
+
"""Search remote skills by name, description, or tags.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
query: Case-insensitive search string.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
List of matching skill entry dicts.
|
|
86
|
+
"""
|
|
87
|
+
results = self._remote.search(query)
|
|
88
|
+
return [s.model_dump() for s in results]
|
|
89
|
+
|
|
90
|
+
def get_skill(self, name: str, version: Optional[str] = None) -> Optional[dict[str, Any]]:
|
|
91
|
+
"""Get info about a specific remote skill.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
name: Skill name.
|
|
95
|
+
version: Specific version (latest if None).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Skill entry dict or None if not found.
|
|
99
|
+
"""
|
|
100
|
+
entry = self._remote.get_skill_info(name, version)
|
|
101
|
+
return entry.model_dump() if entry else None
|
|
102
|
+
|
|
103
|
+
def install(
|
|
104
|
+
self,
|
|
105
|
+
name: str,
|
|
106
|
+
version: Optional[str] = None,
|
|
107
|
+
agent: str = "global",
|
|
108
|
+
force: bool = False,
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
"""Download and install a skill from the remote registry.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
name: Skill name to install.
|
|
114
|
+
version: Specific version (latest if None).
|
|
115
|
+
agent: Agent namespace for installation.
|
|
116
|
+
force: Overwrite existing installation.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Dict with installation metadata.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
FileNotFoundError: If the skill is not in the registry.
|
|
123
|
+
ValueError: If checksum verification fails.
|
|
124
|
+
"""
|
|
125
|
+
installed = self._remote.pull(name, version=version, agent=agent, force=force)
|
|
126
|
+
return {
|
|
127
|
+
"name": installed.manifest.name,
|
|
128
|
+
"version": installed.manifest.version,
|
|
129
|
+
"agent": installed.agent,
|
|
130
|
+
"install_path": installed.install_path,
|
|
131
|
+
"status": installed.status.value,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_registry_client(registry_url: Optional[str] = None) -> Optional[RegistryClient]:
|
|
136
|
+
"""Get a RegistryClient instance, or None if skskills is not installed.
|
|
137
|
+
|
|
138
|
+
This is the recommended entry point. It catches ImportError from
|
|
139
|
+
skskills and returns None so callers can degrade gracefully.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
registry_url: Override the default registry URL.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
RegistryClient or None if skskills is not available.
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
return RegistryClient(registry_url=registry_url)
|
|
149
|
+
except ImportError:
|
|
150
|
+
logger.debug("skskills not installed — remote registry unavailable")
|
|
151
|
+
return None
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Response Cache — TTL-based in-memory cache for LLM responses.
|
|
3
|
+
|
|
4
|
+
Caches responses keyed by (prompt_hash, model_name) with tier-dependent TTLs:
|
|
5
|
+
- FAST tier: 1 hour
|
|
6
|
+
- CODE tier: 24 hours
|
|
7
|
+
- All other tiers: 1 hour (conservative default)
|
|
8
|
+
|
|
9
|
+
Conversation messages (real-time peer exchanges with dynamic context) must be
|
|
10
|
+
excluded at the call site by passing ``skip_cache=True`` to LLMBridge.generate().
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import logging
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from skcapstone.blueprints.schema import ModelTier
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("skcapstone.response_cache")
|
|
24
|
+
|
|
25
|
+
# TTL constants (seconds)
|
|
26
|
+
_TTL_FAST: float = 3600.0 # 1 hour
|
|
27
|
+
_TTL_CODE: float = 86400.0 # 24 hours
|
|
28
|
+
_TTL_DEFAULT: float = 3600.0 # 1 hour fallback for other tiers
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _ttl_for_tier(tier: ModelTier) -> float:
|
|
32
|
+
"""Return the cache TTL in seconds for a given model tier.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
tier: The model tier used for the request.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
TTL in seconds: 3600 for FAST, 86400 for CODE, 3600 for all others.
|
|
39
|
+
"""
|
|
40
|
+
if tier == ModelTier.CODE:
|
|
41
|
+
return _TTL_CODE
|
|
42
|
+
return _TTL_FAST # FAST and all others default to 1 hour
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def hash_prompt(system_prompt: str, user_message: str) -> str:
|
|
46
|
+
"""Compute a deterministic SHA-256 hex digest for a prompt pair.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
system_prompt: The agent's system context string.
|
|
50
|
+
user_message: The incoming user message.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
64-character lowercase hex string.
|
|
54
|
+
"""
|
|
55
|
+
payload = f"{system_prompt}\x00{user_message}".encode("utf-8")
|
|
56
|
+
return hashlib.sha256(payload).hexdigest()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _CacheEntry:
|
|
60
|
+
"""Internal container for a cached response and its expiry timestamp."""
|
|
61
|
+
|
|
62
|
+
__slots__ = ("response", "expires_at")
|
|
63
|
+
|
|
64
|
+
def __init__(self, response: str, ttl: float) -> None:
|
|
65
|
+
self.response: str = response
|
|
66
|
+
self.expires_at: float = time.monotonic() + ttl
|
|
67
|
+
|
|
68
|
+
def is_alive(self) -> bool:
|
|
69
|
+
"""True if this entry has not yet expired."""
|
|
70
|
+
return time.monotonic() < self.expires_at
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ResponseCache:
|
|
74
|
+
"""Thread-safe in-memory LLM response cache with per-tier TTLs.
|
|
75
|
+
|
|
76
|
+
Entries are keyed by ``(prompt_hash, model_name)`` and expire automatically
|
|
77
|
+
based on the tier that produced them. A background sweep is *not* run;
|
|
78
|
+
expired entries are evicted lazily on read and during periodic ``evict()``
|
|
79
|
+
calls.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
max_size: Maximum number of entries to keep. Oldest entries are
|
|
83
|
+
dropped when the limit is reached. Defaults to 1024.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, max_size: int = 1024) -> None:
|
|
87
|
+
self._max_size = max_size
|
|
88
|
+
self._store: dict[tuple[str, str], _CacheEntry] = {}
|
|
89
|
+
self._lock = threading.Lock()
|
|
90
|
+
self._hits = 0
|
|
91
|
+
self._misses = 0
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
# Public API
|
|
95
|
+
# ------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def get(self, prompt_hash: str, model: str) -> Optional[str]:
|
|
98
|
+
"""Retrieve a cached response, returning None on miss or expiry.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
prompt_hash: SHA-256 hex digest from :func:`hash_prompt`.
|
|
102
|
+
model: Concrete model name (e.g. ``"llama3.2"``).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Cached response string, or ``None`` if not found / expired.
|
|
106
|
+
"""
|
|
107
|
+
key = (prompt_hash, model)
|
|
108
|
+
with self._lock:
|
|
109
|
+
entry = self._store.get(key)
|
|
110
|
+
if entry is None:
|
|
111
|
+
self._misses += 1
|
|
112
|
+
return None
|
|
113
|
+
if not entry.is_alive():
|
|
114
|
+
del self._store[key]
|
|
115
|
+
self._misses += 1
|
|
116
|
+
logger.debug("Cache miss (expired): model=%s", model)
|
|
117
|
+
return None
|
|
118
|
+
self._hits += 1
|
|
119
|
+
logger.debug("Cache hit: model=%s", model)
|
|
120
|
+
return entry.response
|
|
121
|
+
|
|
122
|
+
def put(self, prompt_hash: str, model: str, tier: ModelTier, response: str) -> None:
|
|
123
|
+
"""Store a response in the cache.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
prompt_hash: SHA-256 hex digest from :func:`hash_prompt`.
|
|
127
|
+
model: Concrete model name.
|
|
128
|
+
tier: Routing tier — determines the TTL.
|
|
129
|
+
response: LLM response text to cache.
|
|
130
|
+
"""
|
|
131
|
+
if not response:
|
|
132
|
+
return
|
|
133
|
+
ttl = _ttl_for_tier(tier)
|
|
134
|
+
key = (prompt_hash, model)
|
|
135
|
+
with self._lock:
|
|
136
|
+
self._store[key] = _CacheEntry(response, ttl)
|
|
137
|
+
if len(self._store) > self._max_size:
|
|
138
|
+
self._evict_locked()
|
|
139
|
+
logger.debug(
|
|
140
|
+
"Cached response: model=%s tier=%s ttl=%.0fs len=%d",
|
|
141
|
+
model, tier.value, ttl, len(response),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def evict(self) -> int:
|
|
145
|
+
"""Remove all expired entries and return the count removed.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Number of entries evicted.
|
|
149
|
+
"""
|
|
150
|
+
with self._lock:
|
|
151
|
+
return self._evict_locked()
|
|
152
|
+
|
|
153
|
+
def clear(self) -> None:
|
|
154
|
+
"""Remove all entries from the cache."""
|
|
155
|
+
with self._lock:
|
|
156
|
+
self._store.clear()
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def size(self) -> int:
|
|
160
|
+
"""Current number of entries (including not-yet-evicted expired ones)."""
|
|
161
|
+
with self._lock:
|
|
162
|
+
return len(self._store)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def stats(self) -> dict[str, int]:
|
|
166
|
+
"""Return cache statistics: hits, misses, current size."""
|
|
167
|
+
with self._lock:
|
|
168
|
+
return {
|
|
169
|
+
"hits": self._hits,
|
|
170
|
+
"misses": self._misses,
|
|
171
|
+
"size": len(self._store),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
# Internal helpers
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def _evict_locked(self) -> int:
|
|
179
|
+
"""Evict expired entries. Caller must hold ``self._lock``."""
|
|
180
|
+
dead = [k for k, v in self._store.items() if not v.is_alive()]
|
|
181
|
+
for k in dead:
|
|
182
|
+
del self._store[k]
|
|
183
|
+
if dead:
|
|
184
|
+
logger.debug("Evicted %d expired cache entries", len(dead))
|
|
185
|
+
|
|
186
|
+
# If still over limit after eviction, drop oldest by insertion order
|
|
187
|
+
overflow = len(self._store) - self._max_size
|
|
188
|
+
if overflow > 0:
|
|
189
|
+
keys_to_drop = list(self._store.keys())[:overflow]
|
|
190
|
+
for k in keys_to_drop:
|
|
191
|
+
del self._store[k]
|
|
192
|
+
logger.debug("Dropped %d entries to enforce max_size=%d", overflow, self._max_size)
|
|
193
|
+
|
|
194
|
+
return len(dead)
|