superlocalmemory 3.7.8 → 3.8.0
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 +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +69 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -1
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +94 -10
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +8 -1
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +183 -69
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +384 -56
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -75,13 +75,13 @@ def reset_engine():
|
|
|
75
75
|
|
|
76
76
|
# Register tools and resources -------------------------------------------------
|
|
77
77
|
#
|
|
78
|
-
# Essential-only default:
|
|
78
|
+
# Essential-only default: 34 base tools + 8 mesh tools = 42 registered
|
|
79
79
|
# when mesh is enabled. Set ``SLM_MCP_ALL_TOOLS=1`` to expose the full
|
|
80
80
|
# toolset. Rationale: IDEs cap at 50-100 tools total (Cursor,
|
|
81
81
|
# Antigravity, Windsurf) and a maximal SLM registration crowds out
|
|
82
82
|
# other MCP servers the user may have installed.
|
|
83
83
|
# Admin/diagnostics tools remain available via CLI (`slm <command>`).
|
|
84
|
-
# Set SLM_MCP_ALL_TOOLS=1 to enable all
|
|
84
|
+
# Set SLM_MCP_ALL_TOOLS=1 to enable all 84 tools (power users).
|
|
85
85
|
|
|
86
86
|
import os as _os_reg
|
|
87
87
|
|
|
@@ -107,6 +107,10 @@ _ESSENTIAL_TOOLS: set[str] = {
|
|
|
107
107
|
"evolve_skill", "skill_health", "skill_lineage",
|
|
108
108
|
# v3.6.11: Surface B Optimize tools (5)
|
|
109
109
|
"slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
|
|
110
|
+
# v3.8.0: bounded-loop tools (3) — CLI + /slm-loop command + MCP. Kept in
|
|
111
|
+
# the default exposure so any MCP client discovers gated loops, not just
|
|
112
|
+
# profile=code/full/power sessions.
|
|
113
|
+
"slm_loop_run", "slm_loop_history", "slm_loop_show",
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
# v3.4.4: Mesh tools — enabled if mesh_enabled in config or SLM_MCP_MESH_TOOLS=1
|
|
@@ -138,55 +142,21 @@ _user_allowlist_str = _os_reg.environ.get("SLM_MCP_TOOLS", "").strip()
|
|
|
138
142
|
|
|
139
143
|
# ---------------------------------------------------------------------------
|
|
140
144
|
# v3.6.14 WP-01: Named profile definitions
|
|
145
|
+
# Extracted to mcp/profiles.py (v3.8.0) — pure data, no side effects.
|
|
146
|
+
# All names re-exported here for backward compatibility with existing tests
|
|
147
|
+
# and any code that imports them from this module.
|
|
141
148
|
# ---------------------------------------------------------------------------
|
|
142
149
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"mesh_summary", "mesh_peers", "mesh_send", "mesh_inbox",
|
|
154
|
-
"mesh_state", "mesh_lock", "mesh_events", "mesh_status",
|
|
155
|
-
})
|
|
156
|
-
_PROFILE_FULL = frozenset({ # 30 base — EXPLICIT literal, NOT runtime _ESSENTIAL_TOOLS (OQ-2)
|
|
157
|
-
"remember", "recall", "search", "fetch", "list_recent", "delete_memory", "update_memory",
|
|
158
|
-
"get_status", "session_init", "observe", "close_session", "report_feedback", "forget",
|
|
159
|
-
"run_maintenance", "consolidate_cognitive", "get_soft_prompts", "set_mode", "report_outcome",
|
|
160
|
-
"log_tool_event", "get_assertions", "reinforce_assertion", "contradict_assertion",
|
|
161
|
-
"evolve_skill", "skill_health", "skill_lineage",
|
|
162
|
-
"slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
|
|
163
|
-
}) | _PROFILE_FULL_MESH # 38
|
|
164
|
-
_PROFILE_POWER = _PROFILE_FULL | frozenset({ # 50
|
|
165
|
-
"get_version", "get_mode", "health", "consistency_check", "recall_trace",
|
|
166
|
-
"get_lifecycle_status", "set_retention_policy", "compact_memories",
|
|
167
|
-
"get_behavioral_patterns", "audit_trail", "quantize", "get_retention_stats",
|
|
168
|
-
})
|
|
169
|
-
_PROFILE_MESH = _PROFILE_FULL_MESH # 8
|
|
170
|
-
|
|
171
|
-
_PROFILE_DEFINITIONS: dict[str, frozenset[str]] = {
|
|
172
|
-
"core": _PROFILE_CORE,
|
|
173
|
-
"code": _PROFILE_CODE,
|
|
174
|
-
"full": _PROFILE_FULL,
|
|
175
|
-
"power": _PROFILE_POWER,
|
|
176
|
-
"mesh": _PROFILE_MESH,
|
|
177
|
-
} # "whole" intentionally absent — maps to raw server (D-2 LOCKED)
|
|
178
|
-
|
|
179
|
-
# Compatibility aliases published by the v3.6 README. Keep these explicit so
|
|
180
|
-
# a stale client configuration has one deterministic meaning and emits a
|
|
181
|
-
# migration warning. Any other value is a configuration error (fail closed).
|
|
182
|
-
_PROFILE_ALIASES: dict[str, str] = {
|
|
183
|
-
"core14": "core",
|
|
184
|
-
"code20": "code",
|
|
185
|
-
"full38": "full",
|
|
186
|
-
"power50": "power",
|
|
187
|
-
"mesh8": "mesh",
|
|
188
|
-
"whole81": "whole",
|
|
189
|
-
}
|
|
150
|
+
from superlocalmemory.mcp.profiles import ( # noqa: E402 (after env setups above)
|
|
151
|
+
_PROFILE_CORE,
|
|
152
|
+
_PROFILE_CODE,
|
|
153
|
+
_PROFILE_FULL_MESH,
|
|
154
|
+
_PROFILE_FULL,
|
|
155
|
+
_PROFILE_POWER,
|
|
156
|
+
_PROFILE_MESH,
|
|
157
|
+
_PROFILE_DEFINITIONS,
|
|
158
|
+
_PROFILE_ALIASES,
|
|
159
|
+
)
|
|
190
160
|
|
|
191
161
|
_profile = _os_reg.environ.get("SLM_MCP_PROFILE", "").strip().lower()
|
|
192
162
|
|
|
@@ -291,6 +261,8 @@ register_learning_tools(_target, get_engine) # v3.4.7: Two-way learning tools
|
|
|
291
261
|
register_evolution_tools(_target, get_engine) # v3.4.11: Skill evolution tools
|
|
292
262
|
from superlocalmemory.mcp.tools_optimize import register_optimize_tools
|
|
293
263
|
register_optimize_tools(_target) # v3.6.11: Surface B Optimize tools (proxy-free)
|
|
264
|
+
from superlocalmemory.mcp.tools_loops import register_loop_tools
|
|
265
|
+
register_loop_tools(_target, get_engine) # v3.8.0: bounded-loop tools (CLI+command+MCP)
|
|
294
266
|
|
|
295
267
|
|
|
296
268
|
# V3.3.21: Eager engine warmup — start initializing BEFORE first tool call.
|
|
@@ -186,12 +186,6 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
186
186
|
relevance score is ≥ 0.70 (architectural decisions that remain
|
|
187
187
|
permanently relevant still surface). Default: 30.
|
|
188
188
|
Set to 0 to disable the age gate entirely.
|
|
189
|
-
|
|
190
|
-
Scoring: five candidate producers (semantic + BM25 + temporal +
|
|
191
|
-
spreading_activation + hopfield) feed RRF fusion; the entity graph then
|
|
192
|
-
applies an optional post-fusion score enhancement. Combined with
|
|
193
|
-
Ebbinghaus exponential recency decay and FSRS stability strengthening by
|
|
194
|
-
access frequency.
|
|
195
189
|
"""
|
|
196
190
|
try:
|
|
197
191
|
from superlocalmemory.hooks.rules_engine import RulesEngine
|
|
@@ -689,7 +683,10 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
689
683
|
try:
|
|
690
684
|
engine = get_engine()
|
|
691
685
|
db = engine.db
|
|
692
|
-
|
|
686
|
+
# Isolation: the tenant is ALWAYS the engine's active profile. The
|
|
687
|
+
# caller-supplied profile_id is ignored — honoring it let any MCP
|
|
688
|
+
# client read/pin another profile's facts by passing profile_id.
|
|
689
|
+
pid = engine.profile_id
|
|
693
690
|
|
|
694
691
|
if action == "pin":
|
|
695
692
|
if not fact_id:
|
|
@@ -14,10 +14,16 @@ All tools return {"success": bool, ...} envelope. Never raise.
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import logging
|
|
17
|
+
import os
|
|
17
18
|
import time
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
from typing import Any, Callable
|
|
20
21
|
|
|
22
|
+
from superlocalmemory.core.security_primitives import (
|
|
23
|
+
PathTraversalError,
|
|
24
|
+
safe_resolve,
|
|
25
|
+
)
|
|
26
|
+
|
|
21
27
|
logger = logging.getLogger(__name__)
|
|
22
28
|
|
|
23
29
|
# ---------------------------------------------------------------------------
|
|
@@ -118,7 +124,14 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
|
|
|
118
124
|
exclude_patterns: Comma-separated glob patterns to exclude.
|
|
119
125
|
"""
|
|
120
126
|
try:
|
|
121
|
-
|
|
127
|
+
# SEC: contain the untrusted repo_path under $HOME (same guard as
|
|
128
|
+
# update_code_graph) so repo_path="/" cannot ingest the whole
|
|
129
|
+
# filesystem into the code-graph DB.
|
|
130
|
+
from superlocalmemory.core.security_primitives import PathTraversalError
|
|
131
|
+
try:
|
|
132
|
+
repo = safe_resolve(Path.home(), repo_path)
|
|
133
|
+
except (PathTraversalError, OSError, ValueError) as exc:
|
|
134
|
+
return _error_response(f"Invalid repo_path: {exc}")
|
|
122
135
|
if not repo.exists():
|
|
123
136
|
return _error_response(
|
|
124
137
|
f"Repository path does not exist: {repo_path}"
|
|
@@ -250,6 +263,22 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
|
|
|
250
263
|
svc = _get_service()
|
|
251
264
|
db = svc.db
|
|
252
265
|
|
|
266
|
+
# SEC 3.7.9 (B1): contain untrusted repo_path under $HOME and confirm
|
|
267
|
+
# it is a real git repo BEFORE any git subprocess runs with it as cwd.
|
|
268
|
+
# Blocks RCE via attacker-controlled .git config/hooks (core.fsmonitor,
|
|
269
|
+
# core.hooksPath) that git would otherwise execute in a hostile repo.
|
|
270
|
+
if repo_path:
|
|
271
|
+
try:
|
|
272
|
+
repo = safe_resolve(Path.home(), repo_path)
|
|
273
|
+
except PathTraversalError as exc:
|
|
274
|
+
return _error_response(f"Invalid repo_path: {exc}")
|
|
275
|
+
if not (repo / ".git").exists():
|
|
276
|
+
return _error_response(
|
|
277
|
+
f"Not a git repository (no .git dir): {repo_path}"
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
repo = svc.config.repo_root
|
|
281
|
+
|
|
253
282
|
files_list = [
|
|
254
283
|
f.strip() for f in changed_files.split(",") if f.strip()
|
|
255
284
|
] if changed_files else []
|
|
@@ -258,11 +287,22 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
|
|
|
258
287
|
# Auto-detect via git
|
|
259
288
|
try:
|
|
260
289
|
import subprocess
|
|
261
|
-
repo
|
|
290
|
+
# repo is the safe_resolve-contained path computed above.
|
|
291
|
+
# Harden git: disable hooks + fsmonitor + system config so a
|
|
292
|
+
# hostile repo cannot execute code via the git invocation (B1).
|
|
262
293
|
result = subprocess.run(
|
|
263
|
-
[
|
|
294
|
+
[
|
|
295
|
+
"git", "-c", "core.hooksPath=/dev/null",
|
|
296
|
+
"-c", "core.fsmonitor=",
|
|
297
|
+
"diff", "--name-only", "HEAD~1", "HEAD",
|
|
298
|
+
],
|
|
264
299
|
capture_output=True, text=True, timeout=30,
|
|
265
300
|
cwd=str(repo),
|
|
301
|
+
env={
|
|
302
|
+
**os.environ,
|
|
303
|
+
"GIT_CONFIG_NOSYSTEM": "1",
|
|
304
|
+
"GIT_TERMINAL_PROMPT": "0",
|
|
305
|
+
},
|
|
266
306
|
)
|
|
267
307
|
files_list = [
|
|
268
308
|
f.strip() for f in result.stdout.strip().split("\n")
|
|
@@ -289,13 +329,19 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
|
|
|
289
329
|
config = svc.config
|
|
290
330
|
parser = CodeParser(config)
|
|
291
331
|
store = GraphStore(db)
|
|
292
|
-
repo
|
|
332
|
+
# repo already contained via safe_resolve above (B1).
|
|
293
333
|
|
|
294
334
|
nodes_before = db.get_node_count()
|
|
295
335
|
edges_before = db.get_edge_count()
|
|
296
336
|
|
|
297
337
|
for fp in files_list:
|
|
298
|
-
|
|
338
|
+
# fp comes from caller-supplied changed_files; contain it under
|
|
339
|
+
# the (already safe-resolved) repo so "../../.ssh/id_rsa" cannot
|
|
340
|
+
# be read into the graph.
|
|
341
|
+
try:
|
|
342
|
+
full = safe_resolve(repo, fp)
|
|
343
|
+
except Exception:
|
|
344
|
+
continue
|
|
299
345
|
if not full.exists():
|
|
300
346
|
store.remove_file(fp)
|
|
301
347
|
continue
|
|
@@ -284,10 +284,11 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
284
284
|
session_id: str = "", fast: bool = False,
|
|
285
285
|
include_global: bool | None = None,
|
|
286
286
|
include_shared: bool | None = None,
|
|
287
|
+
window: str = "",
|
|
287
288
|
) -> dict:
|
|
288
289
|
"""Search memories through hybrid retrieval, RRF fusion, and reranking.
|
|
289
290
|
|
|
290
|
-
|
|
291
|
+
Optional ``session_id`` threads through to the
|
|
291
292
|
engine's outcome-queue so PostToolUse / Stop hooks can attach
|
|
292
293
|
engagement signals to this recall. Claude Code should pass its
|
|
293
294
|
``CLAUDE_SESSION_ID``. Omitting it degrades to "no closed-loop
|
|
@@ -297,6 +298,11 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
297
298
|
scopes participate in retrieval. Leave them unset (``None``) to use the
|
|
298
299
|
configured default — shared memory is OPT-IN, so by default recall
|
|
299
300
|
returns only this profile's own facts. Pass ``True`` to opt in per call.
|
|
301
|
+
|
|
302
|
+
Time window: optional ``window`` restricts results to a event-time
|
|
303
|
+
range. Accepts a relative span (``"24h"``, ``"7d"``, ``"30d"``,
|
|
304
|
+
``"1y"``) or an explicit range (``"2026-07-01..2026-07-31"``). Empty =
|
|
305
|
+
no time filter.
|
|
300
306
|
"""
|
|
301
307
|
# v3.6.10: resolve "mcp_client" sentinel → URL path (HTTP) or env var (stdio)
|
|
302
308
|
if agent_id == "mcp_client":
|
|
@@ -353,6 +359,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
353
359
|
fast=bool(fast),
|
|
354
360
|
include_global=include_global,
|
|
355
361
|
include_shared=include_shared,
|
|
362
|
+
window=window or None,
|
|
356
363
|
)
|
|
357
364
|
if result.get("ok"):
|
|
358
365
|
# Record implicit feedback: every returned result is a recall_hit
|
|
@@ -98,11 +98,14 @@ def register_evolution_tools(server, get_engine: Callable) -> None:
|
|
|
98
98
|
engine = get_engine()
|
|
99
99
|
profile_id = engine.profile_id if engine else "default"
|
|
100
100
|
|
|
101
|
-
evolver._store.reset_cycle()
|
|
102
|
-
|
|
101
|
+
evolver._store.reset_cycle(profile_id)
|
|
102
|
+
# audit-10 fix: go through evolve_candidate so the manual/MCP path
|
|
103
|
+
# runs under a budget cycle and honours the LLM-call / wall-time /
|
|
104
|
+
# per-day caps (previously _process_candidate ran uncapped here).
|
|
105
|
+
outcome = evolver.evolve_candidate(candidate, profile_id)
|
|
103
106
|
|
|
104
107
|
# Fetch the latest record for this skill to return details
|
|
105
|
-
recent = evolver._store.get_skill_history(skill_name, limit=1)
|
|
108
|
+
recent = evolver._store.get_skill_history(skill_name, profile_id, limit=1)
|
|
106
109
|
record_info = {}
|
|
107
110
|
if recent:
|
|
108
111
|
r = recent[0]
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""SLM v3.8.0 — MCP bounded-loop tools.
|
|
6
|
+
|
|
7
|
+
Exposes the ``superlocalmemory.loops`` engine over MCP so an agent connected
|
|
8
|
+
via MCP — not only the ``slm loop`` CLI or the ``/slm-loop`` command — can run
|
|
9
|
+
a gated, bounded loop and inspect its durable ledger. Bounded loops therefore
|
|
10
|
+
ship on three surfaces: CLI, plugin command, and MCP.
|
|
11
|
+
|
|
12
|
+
The loop's one invariant holds identically here: an INDEPENDENT gate decides
|
|
13
|
+
when the loop is done, never the agent's own claim. Over MCP the gate is an SLM
|
|
14
|
+
*recall*: the loop converges the first lap a memory matching ``gate_query``
|
|
15
|
+
becomes retrievable with confidence (the "verification lives in memory" model
|
|
16
|
+
the engine is designed around). That makes ``slm_loop_run`` a safe, shell-free
|
|
17
|
+
multi-agent coordination primitive — one agent waits, under strict bounds, for
|
|
18
|
+
a memory another agent will write into shared SLM.
|
|
19
|
+
|
|
20
|
+
Three tools:
|
|
21
|
+
* ``slm_loop_run`` — run one bounded, gate-verified loop to a terminal
|
|
22
|
+
outcome. Blocks (polling the gate) until the gate
|
|
23
|
+
passes or a bound trips. Every lap is persisted to
|
|
24
|
+
SLM memory (tag ``loop:<name>``) and shows on the
|
|
25
|
+
dashboard.
|
|
26
|
+
* ``slm_loop_history`` — list recorded runs for a loop name (read-only).
|
|
27
|
+
* ``slm_loop_show`` — show every lap of one run (read-only).
|
|
28
|
+
|
|
29
|
+
Fail-open: every tool body returns a dict; internal errors surface as
|
|
30
|
+
``ok: False`` with a message, never a raised exception.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import asyncio
|
|
36
|
+
import logging
|
|
37
|
+
import time
|
|
38
|
+
from typing import Any, Callable
|
|
39
|
+
|
|
40
|
+
from mcp.types import ToolAnnotations
|
|
41
|
+
|
|
42
|
+
from superlocalmemory.loops import (
|
|
43
|
+
Bounds,
|
|
44
|
+
LapResult,
|
|
45
|
+
Verdict,
|
|
46
|
+
engine_backed_ledger,
|
|
47
|
+
run_bounded_loop,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger("slm.mcp.tools_loops")
|
|
51
|
+
|
|
52
|
+
# ─── Exported tool name list (used by server.py + tests) ─────────────────────
|
|
53
|
+
|
|
54
|
+
_LOOP_TOOL_NAMES = (
|
|
55
|
+
"slm_loop_run",
|
|
56
|
+
"slm_loop_history",
|
|
57
|
+
"slm_loop_show",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# ─── Hard caps (a loop tool must never hang the daemon or spin unbounded) ────
|
|
61
|
+
|
|
62
|
+
_MAX_ITERATIONS = 200
|
|
63
|
+
_MAX_WALLCLOCK_S = 120.0
|
|
64
|
+
_MIN_POLL_S = 0.25
|
|
65
|
+
_MAX_NAME_CHARS = 128
|
|
66
|
+
_MAX_QUERY_CHARS = 2000
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _top_score(resp: Any) -> float:
|
|
70
|
+
"""Highest result score in a RecallResponse (0.0 when there are none)."""
|
|
71
|
+
best = 0.0
|
|
72
|
+
for r in getattr(resp, "results", None) or []:
|
|
73
|
+
s = getattr(r, "score", None)
|
|
74
|
+
if s is None:
|
|
75
|
+
s = getattr(r, "relevance_score", 0.0) or 0.0
|
|
76
|
+
try:
|
|
77
|
+
best = max(best, float(s))
|
|
78
|
+
except (TypeError, ValueError):
|
|
79
|
+
continue
|
|
80
|
+
return best
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def register_loop_tools(server, get_engine: Callable) -> None:
|
|
84
|
+
"""Register the 3 bounded-loop tools on *server*.
|
|
85
|
+
|
|
86
|
+
*server* is duck-typed: must support the ``@server.tool()`` decorator.
|
|
87
|
+
Compatible with FastMCP, _FilteredServer, and the test mock server.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
@server.tool(annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False))
|
|
91
|
+
async def slm_loop_run(
|
|
92
|
+
name: str,
|
|
93
|
+
gate_query: str,
|
|
94
|
+
gate_min_score: float = 0.0,
|
|
95
|
+
max_iterations: int = 20,
|
|
96
|
+
max_wallclock_s: float = 15.0,
|
|
97
|
+
poll_interval_s: float = 1.0,
|
|
98
|
+
max_tokens: int = 0,
|
|
99
|
+
no_progress_window: int = 0,
|
|
100
|
+
) -> dict:
|
|
101
|
+
"""Run one bounded loop that finishes only when an INDEPENDENT gate passes.
|
|
102
|
+
|
|
103
|
+
The gate is an SLM recall of ``gate_query``: the loop converges on the
|
|
104
|
+
first lap that recall returns a confident match scoring at least
|
|
105
|
+
``gate_min_score``. The agent cannot end the loop by asserting it is
|
|
106
|
+
done — only the gate can. The call BLOCKS (polling every
|
|
107
|
+
``poll_interval_s``) until the gate passes or a bound trips, then
|
|
108
|
+
returns the outcome. Every lap is written to SLM memory (tag
|
|
109
|
+
``loop:<name>``) and is visible on the dashboard.
|
|
110
|
+
|
|
111
|
+
Use it to wait, under strict bounds, for a verification or coordination
|
|
112
|
+
condition to hold — e.g. for another agent to record a "build passed"
|
|
113
|
+
memory in shared SLM.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
name: Loop name, also the memory tag. 1–128 chars.
|
|
117
|
+
gate_query: Recall query the independent gate checks each lap.
|
|
118
|
+
gate_min_score: Minimum top-result score to pass (0.0 = any
|
|
119
|
+
confident hit above the evidence floor).
|
|
120
|
+
max_iterations: Hard cap on laps (1–200).
|
|
121
|
+
max_wallclock_s: Hard cap on wall-clock seconds (0 disables; capped at 120).
|
|
122
|
+
poll_interval_s: Seconds to wait between laps (minimum 0.25).
|
|
123
|
+
max_tokens: Optional token budget (0 disables).
|
|
124
|
+
no_progress_window: Halt after this many consecutive no-change laps
|
|
125
|
+
(0 disables). A pure watcher never "changes", so leave at 0
|
|
126
|
+
unless the runner reports progress.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
name = (name or "").strip()
|
|
130
|
+
if not name or len(name) > _MAX_NAME_CHARS:
|
|
131
|
+
return {"ok": False, "error": f"name must be 1–{_MAX_NAME_CHARS} chars"}
|
|
132
|
+
gate_query = (gate_query or "").strip()
|
|
133
|
+
if not gate_query or len(gate_query) > _MAX_QUERY_CHARS:
|
|
134
|
+
return {
|
|
135
|
+
"ok": False,
|
|
136
|
+
"error": f"gate_query must be 1–{_MAX_QUERY_CHARS} chars",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
iters = max(1, min(int(max_iterations), _MAX_ITERATIONS))
|
|
140
|
+
wall = (
|
|
141
|
+
min(float(max_wallclock_s), _MAX_WALLCLOCK_S)
|
|
142
|
+
if max_wallclock_s and float(max_wallclock_s) > 0
|
|
143
|
+
else None
|
|
144
|
+
)
|
|
145
|
+
poll = max(float(poll_interval_s), _MIN_POLL_S)
|
|
146
|
+
tok = int(max_tokens) if max_tokens and int(max_tokens) > 0 else None
|
|
147
|
+
# 0 = disabled (a pure watcher never "changes", so no-progress must
|
|
148
|
+
# be off by default or it would halt before the gate can pass).
|
|
149
|
+
npw = (
|
|
150
|
+
int(no_progress_window)
|
|
151
|
+
if no_progress_window and int(no_progress_window) > 0
|
|
152
|
+
else 0
|
|
153
|
+
)
|
|
154
|
+
min_score = float(gate_min_score)
|
|
155
|
+
|
|
156
|
+
engine = get_engine()
|
|
157
|
+
|
|
158
|
+
def gate(lap: int) -> Verdict:
|
|
159
|
+
resp = engine.recall(gate_query, limit=3, fast=True)
|
|
160
|
+
results = getattr(resp, "results", None) or []
|
|
161
|
+
floored = bool(getattr(resp, "no_confident_match", False))
|
|
162
|
+
top = _top_score(resp)
|
|
163
|
+
passed = bool(results) and not floored and top >= min_score
|
|
164
|
+
return Verdict(
|
|
165
|
+
passed,
|
|
166
|
+
f"recall '{gate_query[:48]}': hits={len(results)} "
|
|
167
|
+
f"top={top:.3f} floor={floored}",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def runner(lap: int) -> LapResult:
|
|
171
|
+
# Watcher lap: no work of our own. Give the gate condition time
|
|
172
|
+
# to become true (e.g. another agent writing a memory) between
|
|
173
|
+
# polls. Bounded by poll interval so the wall-clock / iteration
|
|
174
|
+
# caps stay meaningful. The first lap checks immediately.
|
|
175
|
+
if lap > 1:
|
|
176
|
+
time.sleep(poll)
|
|
177
|
+
return LapResult(changed=False, tokens=0)
|
|
178
|
+
|
|
179
|
+
ledger = engine_backed_ledger(engine)
|
|
180
|
+
|
|
181
|
+
# The loop blocks (sleeps between laps); run it off the event loop.
|
|
182
|
+
# The engine's per-call WAL connection model makes this thread-safe.
|
|
183
|
+
outcome = await asyncio.to_thread(
|
|
184
|
+
run_bounded_loop,
|
|
185
|
+
name,
|
|
186
|
+
bounds=Bounds(
|
|
187
|
+
max_iterations=iters,
|
|
188
|
+
max_tokens=tok,
|
|
189
|
+
max_wallclock_s=wall,
|
|
190
|
+
no_progress_window=npw,
|
|
191
|
+
),
|
|
192
|
+
runner=runner,
|
|
193
|
+
gate=gate,
|
|
194
|
+
ledger=ledger,
|
|
195
|
+
)
|
|
196
|
+
laps = await asyncio.to_thread(ledger.laps, outcome.run_id)
|
|
197
|
+
return {
|
|
198
|
+
"ok": True,
|
|
199
|
+
"status": outcome.status.value,
|
|
200
|
+
"reason": outcome.reason,
|
|
201
|
+
"passed": bool(outcome.ok),
|
|
202
|
+
"laps": outcome.laps,
|
|
203
|
+
"run_id": outcome.run_id,
|
|
204
|
+
"ledger": [
|
|
205
|
+
{
|
|
206
|
+
"lap": e.lap,
|
|
207
|
+
"decision": e.decision,
|
|
208
|
+
"passed": e.passed,
|
|
209
|
+
"detail": e.detail,
|
|
210
|
+
}
|
|
211
|
+
for e in laps
|
|
212
|
+
],
|
|
213
|
+
"note": (
|
|
214
|
+
"The gate — an independent SLM recall — decided this "
|
|
215
|
+
"outcome; the agent's own done-claim never terminates a loop."
|
|
216
|
+
),
|
|
217
|
+
}
|
|
218
|
+
except Exception as exc:
|
|
219
|
+
logger.exception("slm_loop_run failed (fail-open)")
|
|
220
|
+
return {"ok": False, "error": str(exc)}
|
|
221
|
+
|
|
222
|
+
@server.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
223
|
+
async def slm_loop_history(name: str, limit: int = 20) -> dict:
|
|
224
|
+
"""List recorded bounded-loop runs for a loop name, newest laps summarised.
|
|
225
|
+
|
|
226
|
+
Reads the durable SLM-backed ledger (the same rows ``slm loop history``
|
|
227
|
+
and the dashboard show). Read-only.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
name: Loop name to list runs for.
|
|
231
|
+
limit: Maximum runs to return (1–200).
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
name = (name or "").strip()
|
|
235
|
+
if not name:
|
|
236
|
+
return {"ok": False, "error": "name is required"}
|
|
237
|
+
lim = max(1, min(int(limit), 200))
|
|
238
|
+
engine = get_engine()
|
|
239
|
+
ledger = engine_backed_ledger(engine)
|
|
240
|
+
|
|
241
|
+
def _collect() -> list[dict]:
|
|
242
|
+
run_ids = ledger.runs(name)[:lim]
|
|
243
|
+
rows: list[dict] = []
|
|
244
|
+
for rid in run_ids:
|
|
245
|
+
laps = ledger.laps(rid)
|
|
246
|
+
last = laps[-1] if laps else None
|
|
247
|
+
rows.append(
|
|
248
|
+
{
|
|
249
|
+
"run_id": rid,
|
|
250
|
+
"laps": len(laps),
|
|
251
|
+
"final": last.decision if last else "unknown",
|
|
252
|
+
"ts": last.ts if last else "",
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
return rows
|
|
256
|
+
|
|
257
|
+
rows = await asyncio.to_thread(_collect)
|
|
258
|
+
return {"ok": True, "name": name, "count": len(rows), "runs": rows}
|
|
259
|
+
except Exception as exc:
|
|
260
|
+
logger.exception("slm_loop_history failed (fail-open)")
|
|
261
|
+
return {"ok": False, "error": str(exc)}
|
|
262
|
+
|
|
263
|
+
@server.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
264
|
+
async def slm_loop_show(run_id: str, limit: int = 200) -> dict:
|
|
265
|
+
"""Show every lap of one bounded-loop run, in order, from SLM memory.
|
|
266
|
+
|
|
267
|
+
Read-only. Each lap carries the gate verdict plus the agent's recorded
|
|
268
|
+
(advisory, never loop-terminating) done-claim.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
run_id: Run identifier returned by ``slm_loop_run``.
|
|
272
|
+
limit: Maximum laps to return (1–1000).
|
|
273
|
+
"""
|
|
274
|
+
try:
|
|
275
|
+
run_id = (run_id or "").strip()
|
|
276
|
+
if not run_id:
|
|
277
|
+
return {"ok": False, "error": "run_id is required"}
|
|
278
|
+
lim = max(1, min(int(limit), 1000))
|
|
279
|
+
engine = get_engine()
|
|
280
|
+
ledger = engine_backed_ledger(engine)
|
|
281
|
+
|
|
282
|
+
def _collect() -> list[dict]:
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
"lap": e.lap,
|
|
286
|
+
"ts": e.ts,
|
|
287
|
+
"decision": e.decision,
|
|
288
|
+
"passed": e.passed,
|
|
289
|
+
"detail": e.detail,
|
|
290
|
+
"agent_claimed_done": e.agent_claimed_done,
|
|
291
|
+
"tokens": e.budget.get("tokens", 0),
|
|
292
|
+
}
|
|
293
|
+
for e in ledger.laps(run_id)[:lim]
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
laps = await asyncio.to_thread(_collect)
|
|
297
|
+
return {"ok": True, "run_id": run_id, "count": len(laps), "laps": laps}
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
logger.exception("slm_loop_show failed (fail-open)")
|
|
300
|
+
return {"ok": False, "error": str(exc)}
|