minder-cli 0.4.4__tar.gz → 0.4.6__tar.gz

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.
Files changed (156) hide show
  1. {minder_cli-0.4.4 → minder_cli-0.4.6}/PKG-INFO +1 -1
  2. {minder_cli-0.4.4 → minder_cli-0.4.6}/pyproject.toml +1 -1
  3. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/application/admin/dto.py +25 -0
  4. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/application/admin/use_cases.py +114 -0
  5. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/bootstrap/providers.py +13 -1
  6. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/bootstrap/transport.py +36 -0
  7. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/config.py +7 -6
  8. minder_cli-0.4.6/src/minder/context_compactor.py +197 -0
  9. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/executor.py +11 -0
  10. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/graph.py +9 -0
  11. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/nodes/__init__.py +4 -0
  12. minder_cli-0.4.6/src/minder/graph/nodes/clarification.py +164 -0
  13. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/nodes/planning.py +12 -1
  14. minder_cli-0.4.6/src/minder/graph/nodes/reflection.py +64 -0
  15. minder_cli-0.4.6/src/minder/learning/__init__.py +12 -0
  16. minder_cli-0.4.6/src/minder/learning/error_learner.py +70 -0
  17. minder_cli-0.4.6/src/minder/learning/pattern_extractor.py +44 -0
  18. minder_cli-0.4.6/src/minder/learning/quality_optimizer.py +105 -0
  19. minder_cli-0.4.6/src/minder/learning/skill_synthesizer.py +81 -0
  20. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/llm/litert.py +23 -16
  21. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/skill.py +3 -1
  22. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/observability/metrics.py +4 -1
  23. minder_cli-0.4.6/src/minder/presentation/cli/commands/agent.py +418 -0
  24. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/cli/main.py +2 -2
  25. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/cli/utils/common.py +1 -1
  26. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/admin/api.py +78 -14
  27. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/admin/memories.py +1 -1
  28. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/admin/runtime.py +60 -0
  29. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/admin/skills.py +1 -0
  30. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/server.py +28 -17
  31. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/interfaces.py +1 -0
  32. minder_cli-0.4.6/src/minder/store/mongodb/graph_store.py +497 -0
  33. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/mongodb/operational_store.py +11 -0
  34. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/relational.py +23 -1
  35. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/memory.py +62 -8
  36. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/query.py +35 -18
  37. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/registry.py +33 -1
  38. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/repo_scanner.py +1 -1
  39. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/session.py +48 -0
  40. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/skills.py +43 -12
  41. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/workflow.py +19 -29
  42. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/transport/base.py +14 -9
  43. minder_cli-0.4.4/src/minder/presentation/cli/commands/agent.py +0 -252
  44. {minder_cli-0.4.4 → minder_cli-0.4.6}/.gitignore +0 -0
  45. {minder_cli-0.4.4 → minder_cli-0.4.6}/LICENSE +0 -0
  46. {minder_cli-0.4.4 → minder_cli-0.4.6}/README-pypi.md +0 -0
  47. {minder_cli-0.4.4 → minder_cli-0.4.6}/README.md +0 -0
  48. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/__init__.py +0 -0
  49. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/api/routers/prompts.py +0 -0
  50. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/application/__init__.py +0 -0
  51. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/application/admin/__init__.py +0 -0
  52. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/application/admin/jobs.py +0 -0
  53. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/auth/__init__.py +0 -0
  54. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/auth/context.py +0 -0
  55. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/auth/middleware.py +0 -0
  56. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/auth/principal.py +0 -0
  57. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/auth/rate_limiter.py +0 -0
  58. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/auth/rbac.py +0 -0
  59. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/auth/service.py +0 -0
  60. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/bootstrap/__init__.py +0 -0
  61. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/cache/__init__.py +0 -0
  62. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/cache/providers.py +0 -0
  63. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/chunking/__init__.py +0 -0
  64. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/chunking/code_splitter.py +0 -0
  65. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/chunking/splitter.py +0 -0
  66. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/cli.py +0 -0
  67. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/continuity.py +0 -0
  68. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/dev.py +0 -0
  69. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/embedding/__init__.py +0 -0
  70. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/embedding/base.py +0 -0
  71. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/embedding/local.py +0 -0
  72. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/embedding/openai.py +0 -0
  73. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/__init__.py +0 -0
  74. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/edges.py +0 -0
  75. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/nodes/evaluator.py +0 -0
  76. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/nodes/guard.py +0 -0
  77. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/nodes/llm.py +0 -0
  78. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/nodes/reasoning.py +0 -0
  79. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/nodes/reranker.py +0 -0
  80. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/nodes/retriever.py +0 -0
  81. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/nodes/verification.py +0 -0
  82. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/nodes/workflow_planner.py +0 -0
  83. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/runtime.py +0 -0
  84. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/graph/state.py +0 -0
  85. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/llm/__init__.py +0 -0
  86. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/llm/base.py +0 -0
  87. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/llm/factory.py +0 -0
  88. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/llm/openai.py +0 -0
  89. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/__init__.py +0 -0
  90. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/base.py +0 -0
  91. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/client.py +0 -0
  92. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/document.py +0 -0
  93. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/error.py +0 -0
  94. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/graph.py +0 -0
  95. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/history.py +0 -0
  96. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/job.py +0 -0
  97. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/prompt.py +0 -0
  98. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/repository.py +0 -0
  99. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/rule.py +0 -0
  100. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/session.py +0 -0
  101. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/user.py +0 -0
  102. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/models/workflow.py +0 -0
  103. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/observability/__init__.py +0 -0
  104. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/observability/audit.py +0 -0
  105. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/observability/logging.py +0 -0
  106. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/observability/tracing.py +0 -0
  107. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/__init__.py +0 -0
  108. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/cli/__init__.py +0 -0
  109. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/cli/commands/auth.py +0 -0
  110. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/cli/commands/ide.py +0 -0
  111. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/cli/commands/mcp.py +0 -0
  112. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/cli/commands/sync.py +0 -0
  113. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/cli/commands/update.py +0 -0
  114. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/cli/utils/config.py +0 -0
  115. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/cli/utils/git.py +0 -0
  116. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/cli/utils/version.py +0 -0
  117. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/__init__.py +0 -0
  118. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/admin/__init__.py +0 -0
  119. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/admin/context.py +0 -0
  120. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/admin/dashboard.py +0 -0
  121. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/admin/jobs.py +0 -0
  122. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/admin/prompts.py +0 -0
  123. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/admin/routes.py +0 -0
  124. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/presentation/http/admin/search.py +0 -0
  125. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/prompts/__init__.py +0 -0
  126. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/prompts/formatter.py +0 -0
  127. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/resources/__init__.py +0 -0
  128. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/retrieval/__init__.py +0 -0
  129. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/retrieval/hybrid.py +0 -0
  130. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/retrieval/mmr.py +0 -0
  131. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/retrieval/multi_hop.py +0 -0
  132. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/runtime.py +0 -0
  133. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/__init__.py +0 -0
  134. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/document.py +0 -0
  135. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/error.py +0 -0
  136. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/feedback.py +0 -0
  137. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/graph.py +0 -0
  138. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/history.py +0 -0
  139. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/milvus/__init__.py +0 -0
  140. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/milvus/client.py +0 -0
  141. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/milvus/collections.py +0 -0
  142. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/milvus/vector_store.py +0 -0
  143. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/mongodb/__init__.py +0 -0
  144. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/mongodb/client.py +0 -0
  145. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/mongodb/indexes.py +0 -0
  146. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/repo_state.py +0 -0
  147. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/rule.py +0 -0
  148. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/store/vector.py +0 -0
  149. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/__init__.py +0 -0
  150. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/auth.py +0 -0
  151. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/graph.py +0 -0
  152. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/ingest.py +0 -0
  153. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/tools/search.py +0 -0
  154. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/transport/__init__.py +0 -0
  155. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/transport/sse.py +0 -0
  156. {minder_cli-0.4.4 → minder_cli-0.4.6}/src/minder/transport/stdio.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: minder-cli
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: Minder CLI is the command-line interface for the Minder self-hosted MCP platform.
5
5
  Project-URL: Homepage, https://github.com/hiimtrung/minder
6
6
  Project-URL: Repository, https://github.com/hiimtrung/minder
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "minder-cli"
7
- version = "0.4.4"
7
+ version = "0.4.6"
8
8
  description = "Minder CLI is the command-line interface for the Minder self-hosted MCP platform."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -72,6 +72,7 @@ class AuditEventPayload(TypedDict):
72
72
  resource_name: str | None # human-readable name for the resource
73
73
  outcome: str
74
74
  created_at: str | None
75
+ audit_metadata: dict | None
75
76
 
76
77
 
77
78
  class ClientListPayload(TypedDict):
@@ -129,6 +130,28 @@ class AdminSessionPayload(TypedDict):
129
130
  role: str
130
131
 
131
132
 
133
+ class SessionPayload(TypedDict):
134
+ id: str
135
+ user_id: str | None
136
+ client_id: str | None
137
+ name: str | None
138
+ repo_id: str | None
139
+ project_context: dict[str, Any]
140
+ active_skills: dict[str, Any]
141
+ state: dict[str, Any]
142
+ ttl: int
143
+ created_at: str
144
+ last_active: str
145
+
146
+
147
+ class SessionListPayload(TypedDict):
148
+ sessions: list[SessionPayload]
149
+
150
+
151
+ class SessionDetailPayload(TypedDict):
152
+ session: SessionPayload
153
+
154
+
132
155
  # ---------------------------------------------------------------------------
133
156
  # User management
134
157
  # ---------------------------------------------------------------------------
@@ -198,6 +221,7 @@ class RepositoryPayload(TypedDict):
198
221
  remote_url: str | None
199
222
  default_branch: str | None
200
223
  tracked_branches: list[str]
224
+ workflow_id: str | None
201
225
  workflow_name: str | None
202
226
  workflow_state: str | None
203
227
  current_step: str | None
@@ -260,6 +284,7 @@ class UpdateRepositoryPayload(TypedDict, total=False):
260
284
  remote_url: str | None
261
285
  default_branch: str | None
262
286
  path: str
287
+ workflow_id: str | None
263
288
 
264
289
 
265
290
  class DeleteRepositoryPayload(TypedDict):
@@ -6,6 +6,7 @@ from collections import Counter
6
6
  from datetime import UTC, datetime
7
7
  from pathlib import Path
8
8
  from typing import Any
9
+
9
10
  from urllib.parse import urlsplit
10
11
 
11
12
  from minder.application.admin.dto import (
@@ -49,13 +50,19 @@ from minder.application.admin.dto import (
49
50
  WorkflowListPayload,
50
51
  WorkflowPayload,
51
52
  WorkflowStepPayload,
53
+ SessionPayload,
54
+ SessionListPayload,
55
+ SessionDetailPayload,
52
56
  )
53
57
  from minder.auth.service import AuthService
54
58
  from minder.config import MinderConfig
59
+ from minder.observability.audit import AuditEmitter
55
60
  from minder.store.interfaces import IGraphRepository, IOperationalStore
56
61
  from minder.tools.graph import GraphTools
57
62
  from minder.tools.registry import SCOPEABLE_TOOLS
58
63
 
64
+ _UNSET: Any = object() # sentinel for optional update fields
65
+
59
66
  DASHBOARD_TOOL_SCOPE_OPTIONS = [tool.name for tool in SCOPEABLE_TOOLS]
60
67
 
61
68
  DASHBOARD_TOOL_SCOPE_PRESETS: dict[str, list[str]] = {
@@ -96,6 +103,7 @@ class AdminConsoleUseCases:
96
103
  self._config = config
97
104
  self._graph_store = graph_store
98
105
  self._graph_tools = GraphTools(graph_store, store)
106
+ self._audit = AuditEmitter(store)
99
107
 
100
108
  async def has_admin_users(self) -> bool:
101
109
  return await self._auth_service.has_admin_users()
@@ -413,6 +421,7 @@ class AdminConsoleUseCases:
413
421
  "resource_name": None,
414
422
  "outcome": event.outcome,
415
423
  "created_at": event.created_at.isoformat() if event.created_at else None,
424
+ "audit_metadata": getattr(event, "audit_metadata", None),
416
425
  }
417
426
 
418
427
  async def serialize_audit_event_enriched(self, event: Any) -> AuditEventPayload:
@@ -481,6 +490,17 @@ class AdminConsoleUseCases:
481
490
  role=role,
482
491
  password=password,
483
492
  )
493
+ await self._audit.emit(
494
+ actor_type="admin",
495
+ actor_id=str(user.id), # in this context, the new user IS the actor if it's initial setup?
496
+ # actually, if an admin creates a user, we should know WHO did it.
497
+ # but create_user use case doesn't take actor_id yet.
498
+ event_type="user.created",
499
+ resource_type="user",
500
+ resource_id=str(user.id),
501
+ outcome="success",
502
+ metadata={"username": username, "role": role},
503
+ )
484
504
  return {"user": self.serialize_user(user), "api_key": api_key}
485
505
 
486
506
  async def get_user_detail(self, user_id: uuid.UUID) -> UserDetailPayload:
@@ -514,6 +534,16 @@ class AdminConsoleUseCases:
514
534
  updated = await self._store.update_user(user_id, **kwargs)
515
535
  if updated is None:
516
536
  raise LookupError(f"User {user_id} not found")
537
+
538
+ await self._audit.emit(
539
+ actor_type="admin",
540
+ actor_id=str(user_id), # same issue: no actor_id passed to use case
541
+ event_type="user.updated",
542
+ resource_type="user",
543
+ resource_id=str(user_id),
544
+ outcome="success",
545
+ metadata={"fields_changed": list(kwargs.keys())},
546
+ )
517
547
  return await self.get_user_detail(user_id)
518
548
 
519
549
  async def deactivate_user(self, user_id: uuid.UUID) -> UserDetailPayload:
@@ -623,6 +653,78 @@ class AdminConsoleUseCases:
623
653
  ),
624
654
  }
625
655
 
656
+ # ------------------------------------------------------------------
657
+ # Session management
658
+ # ------------------------------------------------------------------
659
+
660
+ async def list_sessions(self) -> SessionListPayload:
661
+ sessions = await self._store.list_sessions()
662
+ return {"sessions": [self.serialize_session(s) for s in sessions]}
663
+
664
+ async def get_session_detail(self, session_id: uuid.UUID) -> SessionDetailPayload:
665
+ session = await self._store.get_session_by_id(session_id)
666
+ if session is None:
667
+ raise LookupError(f"Session {session_id} not found")
668
+ return {"session": self.serialize_session(session)}
669
+
670
+ async def update_session(
671
+ self,
672
+ session_id: uuid.UUID,
673
+ *,
674
+ state: dict[str, Any] | None = None,
675
+ active_skills: dict[str, Any] | None = None,
676
+ project_context: dict[str, Any] | None = None,
677
+ ) -> SessionPayload:
678
+ existing = await self._store.get_session_by_id(session_id)
679
+ if existing is None:
680
+ raise LookupError(f"Session {session_id} not found")
681
+ updates: dict[str, Any] = {}
682
+ if state is not None:
683
+ updates["state"] = state
684
+ if active_skills is not None:
685
+ updates["active_skills"] = active_skills
686
+ if project_context is not None:
687
+ updates["project_context"] = project_context
688
+ if not updates:
689
+ return self.serialize_session(existing)
690
+ from datetime import UTC, datetime
691
+ updates["last_active"] = datetime.now(UTC)
692
+ updated = await self._store.update_session(session_id, **updates)
693
+ if updated is None:
694
+ raise LookupError(f"Session {session_id} not found")
695
+ return self.serialize_session(updated)
696
+
697
+ async def delete_session(self, session_id: uuid.UUID) -> dict[str, bool]:
698
+ existing = await self._store.get_session_by_id(session_id)
699
+ if existing is None:
700
+ raise LookupError(f"Session {session_id} not found")
701
+ await self._store.delete_session(session_id)
702
+ return {"deleted": True}
703
+
704
+ @staticmethod
705
+ def serialize_session(session: Any) -> SessionPayload:
706
+ return {
707
+ "id": str(session.id),
708
+ "user_id": str(session.user_id) if session.user_id else None,
709
+ "client_id": str(session.client_id) if session.client_id else None,
710
+ "name": session.name,
711
+ "repo_id": str(session.repo_id) if session.repo_id else None,
712
+ "project_context": dict(getattr(session, "project_context", {}) or {}),
713
+ "active_skills": dict(getattr(session, "active_skills", {}) or {}),
714
+ "state": dict(getattr(session, "state", {}) or {}),
715
+ "ttl": int(getattr(session, "ttl", 86400) or 86400),
716
+ "created_at": (
717
+ session.created_at.isoformat()
718
+ if getattr(session, "created_at", None)
719
+ else ""
720
+ ),
721
+ "last_active": (
722
+ session.last_active.isoformat()
723
+ if getattr(session, "last_active", None)
724
+ else ""
725
+ ),
726
+ }
727
+
626
728
  # ------------------------------------------------------------------
627
729
  # Repository management
628
730
  # ------------------------------------------------------------------
@@ -661,6 +763,7 @@ class AdminConsoleUseCases:
661
763
  remote_url: str | None = None,
662
764
  default_branch: str | None = None,
663
765
  path: str | None = None,
766
+ workflow_id: Any = _UNSET,
664
767
  ) -> RepositoryDetailPayload:
665
768
  repository = await self._store.get_repository_by_id(repo_id)
666
769
  if repository is None:
@@ -687,6 +790,15 @@ class AdminConsoleUseCases:
687
790
  if not normalized_path:
688
791
  raise ValueError("Repository path is required")
689
792
  updates["state_path"] = normalized_path
793
+ if workflow_id is not _UNSET:
794
+ if workflow_id is None or str(workflow_id).strip() == "":
795
+ updates["workflow_id"] = None
796
+ else:
797
+ wf_id = uuid.UUID(str(workflow_id))
798
+ workflow = await self._store.get_workflow_by_id(wf_id)
799
+ if workflow is None:
800
+ raise LookupError(f"Workflow {wf_id} not found")
801
+ updates["workflow_id"] = str(wf_id)
690
802
 
691
803
  updated = await self._store.update_repository(repo_id, **updates)
692
804
  if updated is None:
@@ -772,6 +884,7 @@ class AdminConsoleUseCases:
772
884
  tracked: list[str] = (
773
885
  list(raw_branches) if isinstance(raw_branches, list) else []
774
886
  )
887
+ raw_workflow_id = getattr(repo, "workflow_id", None)
775
888
  return {
776
889
  "id": str(repo.id),
777
890
  "name": getattr(repo, "repo_name", getattr(repo, "name", "")),
@@ -779,6 +892,7 @@ class AdminConsoleUseCases:
779
892
  "remote_url": _normalize_repository_remote(getattr(repo, "repo_url", None)),
780
893
  "default_branch": getattr(repo, "default_branch", None),
781
894
  "tracked_branches": tracked,
895
+ "workflow_id": str(raw_workflow_id) if raw_workflow_id else None,
782
896
  "workflow_name": getattr(state, "workflow_name", None) if state else None,
783
897
  "workflow_state": getattr(state, "state", None) if state else None,
784
898
  "current_step": getattr(state, "current_step", None) if state else None,
@@ -87,6 +87,18 @@ def build_graph_store(config: MinderConfig) -> IGraphRepository | None:
87
87
  if provider == "mongodb":
88
88
  provider = "sqlite"
89
89
 
90
+ if provider == "mongodb":
91
+ from minder.store.mongodb.client import MongoClient
92
+ from minder.store.mongodb.graph_store import MongoGraphStore
93
+
94
+ client = MongoClient(
95
+ uri=config.mongodb.uri,
96
+ database=config.mongodb.database,
97
+ min_pool_size=config.mongodb.min_pool_size,
98
+ max_pool_size=config.mongodb.max_pool_size,
99
+ )
100
+ return MongoGraphStore(client) # type: ignore[return-value]
101
+
90
102
  if provider in ("sqlite", "postgresql"):
91
103
  from minder.store.graph import KnowledgeGraphStore
92
104
 
@@ -105,5 +117,5 @@ def build_graph_store(config: MinderConfig) -> IGraphRepository | None:
105
117
 
106
118
  raise ValueError(
107
119
  f"Unsupported graph_store.provider '{provider}'. "
108
- "Supported: 'auto', 'sqlite', 'postgresql'."
120
+ "Supported: 'auto', 'mongodb', 'sqlite', 'postgresql'."
109
121
  )
@@ -274,6 +274,12 @@ def build_transport(
274
274
  open_files=open_files,
275
275
  )
276
276
 
277
+ async def minder_session_summarize(
278
+ *, user=None, session_id: str
279
+ ) -> dict[str, Any]: # noqa: ANN001
280
+ del user
281
+ return await session_tools.minder_session_summarize(uuid.UUID(session_id))
282
+
277
283
  async def minder_session_cleanup(
278
284
  *,
279
285
  user=None,
@@ -368,6 +374,22 @@ def build_transport(
368
374
  del user
369
375
  return await memory_tools.minder_memory_list()
370
376
 
377
+ async def minder_memory_update(
378
+ *,
379
+ user=None,
380
+ memory_id: str,
381
+ title: str | None = None,
382
+ content: str | None = None,
383
+ tags: list[str] | None = None,
384
+ ) -> dict[str, Any]: # noqa: ANN001
385
+ del user
386
+ return await memory_tools.minder_memory_update(
387
+ memory_id,
388
+ title=title,
389
+ content=content,
390
+ tags=tags,
391
+ )
392
+
371
393
  async def minder_memory_delete(
372
394
  *, user=None, skill_id: str
373
395
  ) -> dict[str, bool]: # noqa: ANN001
@@ -456,6 +478,7 @@ def build_transport(
456
478
  artifact_types: list[str] | None = None,
457
479
  provenance: str | None = None,
458
480
  quality_score: float | None = None,
481
+ deprecated: bool | None = None,
459
482
  ) -> dict[str, Any]: # noqa: ANN001
460
483
  del user
461
484
  return await skill_tools.minder_skill_update(
@@ -468,6 +491,7 @@ def build_transport(
468
491
  artifact_types=artifact_types,
469
492
  provenance=provenance,
470
493
  quality_score=quality_score,
494
+ deprecated=deprecated,
471
495
  )
472
496
 
473
497
  async def minder_skill_delete(
@@ -669,6 +693,12 @@ def build_transport(
669
693
  require_auth=True,
670
694
  description=TOOL_DESCRIPTIONS["minder_session_context"],
671
695
  )
696
+ transport.register_tool(
697
+ "minder_session_summarize",
698
+ minder_session_summarize,
699
+ require_auth=True,
700
+ description=TOOL_DESCRIPTIONS["minder_session_summarize"],
701
+ )
672
702
  transport.register_tool(
673
703
  "minder_session_cleanup",
674
704
  minder_session_cleanup,
@@ -717,6 +747,12 @@ def build_transport(
717
747
  require_auth=True,
718
748
  description=TOOL_DESCRIPTIONS["minder_memory_list"],
719
749
  )
750
+ transport.register_tool(
751
+ "minder_memory_update",
752
+ minder_memory_update,
753
+ require_auth=True,
754
+ description=TOOL_DESCRIPTIONS["minder_memory_update"],
755
+ )
720
756
  transport.register_tool(
721
757
  "minder_memory_delete",
722
758
  minder_memory_delete,
@@ -46,7 +46,7 @@ class LLMConfig(BaseModel):
46
46
  litert_model_path: str = "~/.minder/models/gemma-4-E2B-it.litertlm"
47
47
  litert_backend: str = "auto" # "auto" (GPU on Mac, CPU elsewhere) | "cpu" | "gpu"
48
48
  litert_cache_dir: str = "~/.minder/cache/litert"
49
- context_length: int = 32768
49
+ context_length: int = 16384
50
50
  temperature: float = 0.1
51
51
  openai_api_key: Optional[str] = None
52
52
  openai_model: str = "gpt-4o-mini"
@@ -67,16 +67,17 @@ class RelationalStoreConfig(BaseModel):
67
67
 
68
68
  class GraphStoreConfig(BaseModel):
69
69
  enabled: bool = True
70
- provider: str = "auto" # "auto" | "sqlite" | "postgresql"
71
- db_path: str = "~/.minder/data/graph.db" # used by sqlite
72
- uri: str = "postgresql+asyncpg://localhost/minder_graph" # used by postgresql
70
+ provider: str = "auto" # "auto" | "mongodb" | "sqlite" | "postgresql"
71
+ # auto: mirrors relational_store.provider (mongodb mongodb, sqlite → sqlite, postgresql → postgresql)
72
+ db_path: str = "~/.minder/data/graph.db" # sqlite only
73
+ uri: str = "postgresql+asyncpg://localhost/minder_graph" # postgresql only
73
74
 
74
75
 
75
76
  class MongoDBConfig(BaseModel):
76
77
  uri: str = "mongodb://localhost:27017"
77
78
  database: str = "minder"
78
- min_pool_size: int = 5
79
- max_pool_size: int = 50
79
+ min_pool_size: int = 2
80
+ max_pool_size: int = 10
80
81
 
81
82
 
82
83
  class RedisConfig(BaseModel):
@@ -0,0 +1,197 @@
1
+ """
2
+ Compacts chat history to fit within the LLM context window.
3
+
4
+ Strategy (HistoryCompactor):
5
+ - Always keep the `keep_recent` most recent messages verbatim (recency bias).
6
+ - If older messages still fit in the remaining budget, keep them all.
7
+ - Otherwise drop oldest messages and prepend a single notice message so the
8
+ LLM knows part of the history was omitted.
9
+
10
+ No LLM call is made here — this is a pure sliding-window approach so it adds
11
+ zero latency on every query.
12
+
13
+ P7-T06 — SummarizingCompactor extends the base with an optional LLM
14
+ summarization step: dropped messages are condensed into a single summary
15
+ message instead of a terse omission notice.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ from collections.abc import Callable
22
+ from typing import Any
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Approximate chars-per-token for chat content (UTF-8 prose / code mix).
27
+ _CHARS_PER_TOKEN = 3
28
+
29
+
30
+ class HistoryCompactor:
31
+ """Trims chat_history to fit within a fraction of the LLM context window."""
32
+
33
+ def __init__(
34
+ self,
35
+ *,
36
+ keep_recent: int = 6,
37
+ history_budget_ratio: float = 0.40,
38
+ ) -> None:
39
+ # keep_recent: number of latest messages always preserved verbatim.
40
+ # history_budget_ratio: fraction of context_length tokens available for history.
41
+ self._keep_recent = keep_recent
42
+ self._history_budget_ratio = history_budget_ratio
43
+
44
+ def compact(
45
+ self,
46
+ history: list[dict[str, Any]],
47
+ *,
48
+ context_length: int,
49
+ ) -> list[dict[str, Any]]:
50
+ """Return a (possibly shorter) history list that fits within the budget.
51
+
52
+ If messages are dropped a synthetic notice is prepended so the model
53
+ knows earlier context exists but was truncated.
54
+ """
55
+ if not history:
56
+ return []
57
+
58
+ budget_chars = int(context_length * self._history_budget_ratio) * _CHARS_PER_TOKEN
59
+ total_chars = sum(len(str(m.get("content", ""))) for m in history)
60
+
61
+ if total_chars <= budget_chars:
62
+ return history
63
+
64
+ # Split into recent (always kept) and older (candidates for dropping).
65
+ if len(history) > self._keep_recent:
66
+ recent = history[-self._keep_recent :]
67
+ older = history[: -self._keep_recent]
68
+ else:
69
+ recent = list(history)
70
+ older = []
71
+
72
+ recent_chars = sum(len(str(m.get("content", ""))) for m in recent)
73
+
74
+ # If even the recent slice alone exceeds the budget, truncate from the
75
+ # oldest message in that slice (keeping tail content within each message).
76
+ if recent_chars > budget_chars:
77
+ recent = self._fit_to_budget(recent, budget_chars)
78
+ dropped_count = len(history) - len(recent)
79
+ logger.warning(
80
+ "chat_history compacted: dropped %d messages (context_length=%d)",
81
+ dropped_count,
82
+ context_length,
83
+ )
84
+ if dropped_count > 0:
85
+ return [_notice(dropped_count)] + recent
86
+ return recent
87
+
88
+ # Fit as many older messages as possible within leftover budget.
89
+ older_budget = budget_chars - recent_chars
90
+ kept_older = self._fit_to_budget(older, older_budget)
91
+ dropped_count = len(older) - len(kept_older)
92
+
93
+ if dropped_count > 0:
94
+ logger.info(
95
+ "chat_history compacted: dropped %d older messages (context_length=%d)",
96
+ dropped_count,
97
+ context_length,
98
+ )
99
+ return [_notice(dropped_count)] + kept_older + recent
100
+
101
+ return kept_older + recent
102
+
103
+ # ------------------------------------------------------------------
104
+ # Internal helpers
105
+ # ------------------------------------------------------------------
106
+
107
+ @staticmethod
108
+ def _fit_to_budget(
109
+ messages: list[dict[str, Any]],
110
+ budget_chars: int,
111
+ ) -> list[dict[str, Any]]:
112
+ """Return the maximal tail of *messages* whose total chars ≤ budget_chars."""
113
+ kept: list[dict[str, Any]] = []
114
+ remaining = budget_chars
115
+ for msg in reversed(messages):
116
+ content = str(msg.get("content", ""))
117
+ if remaining <= 0:
118
+ break
119
+ if len(content) > remaining:
120
+ # Truncate the message to fit, keeping the tail (most recent content).
121
+ truncated = {**msg, "content": "…" + content[-(remaining - 1) :]}
122
+ kept.insert(0, truncated)
123
+ remaining = 0
124
+ else:
125
+ kept.insert(0, msg)
126
+ remaining -= len(content)
127
+ return kept
128
+
129
+
130
+ def _notice(dropped_count: int) -> dict[str, Any]:
131
+ return {
132
+ "role": "user",
133
+ "content": (
134
+ f"[{dropped_count} earlier message(s) omitted — context window limit reached]"
135
+ ),
136
+ }
137
+
138
+
139
+ class SummarizingCompactor(HistoryCompactor):
140
+ """P7-T06 — Compactor that replaces omission notices with LLM summaries.
141
+
142
+ The `summarizer` callable receives the list of dropped messages and
143
+ returns a short summary string. If the callable raises or returns an
144
+ empty string, the base-class omission notice is used as a fallback.
145
+
146
+ Because summarization adds latency, only activate this when an LLM is
147
+ available and the history budget is genuinely exceeded.
148
+ """
149
+
150
+ def __init__(
151
+ self,
152
+ summarizer: Callable[[list[dict[str, Any]]], str],
153
+ *,
154
+ keep_recent: int = 6,
155
+ history_budget_ratio: float = 0.40,
156
+ ) -> None:
157
+ super().__init__(
158
+ keep_recent=keep_recent,
159
+ history_budget_ratio=history_budget_ratio,
160
+ )
161
+ self._summarizer = summarizer
162
+
163
+ def compact(
164
+ self,
165
+ history: list[dict[str, Any]],
166
+ *,
167
+ context_length: int,
168
+ ) -> list[dict[str, Any]]:
169
+ budget_chars = int(context_length * self._history_budget_ratio) * _CHARS_PER_TOKEN
170
+ total_chars = sum(len(str(m.get("content", ""))) for m in history)
171
+
172
+ if total_chars <= budget_chars:
173
+ return history
174
+
175
+ if len(history) > self._keep_recent:
176
+ recent = history[-self._keep_recent :]
177
+ dropped = history[: -self._keep_recent]
178
+ else:
179
+ return super().compact(history, context_length=context_length)
180
+
181
+ summary_message = self._summarize(dropped)
182
+ recent_chars = sum(len(str(m.get("content", ""))) for m in recent)
183
+ summary_chars = len(str(summary_message.get("content", "")))
184
+
185
+ if recent_chars + summary_chars <= budget_chars:
186
+ return [summary_message] + recent
187
+
188
+ return super().compact(history, context_length=context_length)
189
+
190
+ def _summarize(self, dropped: list[dict[str, Any]]) -> dict[str, Any]:
191
+ try:
192
+ text = self._summarizer(dropped)
193
+ if text and text.strip():
194
+ return {"role": "user", "content": f"[Earlier context summary: {text.strip()}]"}
195
+ except Exception as exc:
196
+ logger.debug("SummarizingCompactor summarizer failed: %s", exc)
197
+ return _notice(len(dropped))
@@ -4,12 +4,14 @@ from dataclasses import dataclass, field
4
4
 
5
5
  from minder.graph.edges import determine_next_edge
6
6
  from minder.graph.nodes import (
7
+ ClarificationNode,
7
8
  EvaluatorNode,
8
9
  GuardNode,
9
10
  LLMNode,
10
11
  PlanningNode,
11
12
  ReasoningNode,
12
13
  RerankerNode,
14
+ ReflectionNode,
13
15
  RetrieverNode,
14
16
  VerificationNode,
15
17
  WorkflowPlannerNode,
@@ -22,6 +24,7 @@ from minder.graph.state import GraphState
22
24
  class GraphNodes:
23
25
  workflow_planner: WorkflowPlannerNode
24
26
  planning: PlanningNode
27
+ clarification: ClarificationNode
25
28
  retriever: RetrieverNode
26
29
  reasoning: ReasoningNode
27
30
  llm: LLMNode
@@ -29,6 +32,7 @@ class GraphNodes:
29
32
  verification: VerificationNode
30
33
  evaluator: EvaluatorNode
31
34
  reranker: RerankerNode | None = field(default=None)
35
+ reflection: ReflectionNode | None = field(default=None)
32
36
 
33
37
 
34
38
  class InternalGraphExecutor:
@@ -41,6 +45,9 @@ class InternalGraphExecutor:
41
45
  state.metadata["orchestration_runtime"] = "internal"
42
46
  state = await self._nodes.workflow_planner.run(state)
43
47
  state = self._nodes.planning.run(state)
48
+ state = self._nodes.clarification.run(state)
49
+ if state.metadata.get("needs_clarification"):
50
+ return state
44
51
  state = await self._nodes.retriever.run(state)
45
52
  if self._nodes.reranker is not None:
46
53
  state = await self._nodes.reranker.run(state)
@@ -88,6 +95,10 @@ class InternalGraphExecutor:
88
95
 
89
96
  state = self._nodes.evaluator.run(state)
90
97
  state.metadata["edge"] = determine_next_edge(state)
98
+
99
+ if self._nodes.reflection is not None:
100
+ state = await self._nodes.reflection.run(state)
101
+
91
102
  return state
92
103
 
93
104