supermemory-agent 0.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. storage/adapters/__init__.py +0 -0
  2. storage/adapters/file.py +397 -0
  3. storage/adapters/postgres_qdrant_redis.py +32 -0
  4. storage/adapters/sqlite_chroma.py +95 -0
  5. supermemory_agent-0.2.3.dist-info/METADATA +170 -0
  6. supermemory_agent-0.2.3.dist-info/RECORD +54 -0
  7. supermemory_agent-0.2.3.dist-info/WHEEL +4 -0
  8. supermemory_agent-0.2.3.dist-info/entry_points.txt +2 -0
  9. supermemory_agent-0.2.3.dist-info/licenses/LICENSE +21 -0
  10. supermemory_mcp/__init__.py +5 -0
  11. supermemory_mcp/bridge.py +35 -0
  12. supermemory_mcp/handlers.py +772 -0
  13. supermemory_mcp/server.py +522 -0
  14. supermemory_mcp/text.py +16 -0
  15. uall/__init__.py +0 -0
  16. uall/analytics/service.py +35 -0
  17. uall/collector/__init__.py +0 -0
  18. uall/collector/service.py +100 -0
  19. uall/distillation/distiller.py +80 -0
  20. uall/evaluation/engine.py +38 -0
  21. uall/experiments/manager.py +83 -0
  22. uall/memory/__init__.py +0 -0
  23. uall/memory/confidence.py +36 -0
  24. uall/memory/freshness.py +28 -0
  25. uall/memory/graph.py +24 -0
  26. uall/memory/namespaces.py +40 -0
  27. uall/memory/policies.py +44 -0
  28. uall/memory/provenance.py +23 -0
  29. uall/memory/pruning.py +55 -0
  30. uall/memory/retrieval.py +98 -0
  31. uall/memory/ttl.py +22 -0
  32. uall/memory/validator.py +144 -0
  33. uall/optimization/optimizers.py +59 -0
  34. uall/promotion/queue.py +107 -0
  35. uall/recommendations/engine.py +66 -0
  36. uall/reflection/engine.py +72 -0
  37. uall/rollback/manager.py +49 -0
  38. uall/service.py +572 -0
  39. uall/skills/library.py +19 -0
  40. uall/telemetry/retrieval.py +40 -0
  41. uall_core/__init__.py +0 -0
  42. uall_core/ports/storage.py +71 -0
  43. uall_core/providers/heuristic.py +58 -0
  44. uall_core/providers/llm.py +6 -0
  45. uall_core/schemas/__init__.py +0 -0
  46. uall_core/schemas/common.py +73 -0
  47. uall_core/schemas/events.py +75 -0
  48. uall_core/schemas/graph.py +32 -0
  49. uall_core/schemas/lesson.py +109 -0
  50. uall_core/schemas/namespace.py +76 -0
  51. uall_python/__init__.py +3 -0
  52. uall_python/client.py +337 -0
  53. uall_server/__init__.py +0 -0
  54. uall_server/main.py +284 -0
@@ -0,0 +1,522 @@
1
+ """SuperMemory MCP server — FastMCP with GitHub-compatible tools + MCP resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ ROOT = Path(__file__).resolve().parents[2]
13
+ sys.path.insert(0, str(ROOT))
14
+ sys.path.insert(0, str(ROOT / "packages"))
15
+ sys.path.insert(0, str(ROOT / "src"))
16
+
17
+ from supermemory_mcp.handlers import handle_tool, reset_service
18
+
19
+
20
+ def build_server(storage_root: str | None = None):
21
+ from mcp.server.fastmcp import FastMCP
22
+
23
+ data_dir = (
24
+ storage_root
25
+ or os.getenv("SUPERMEMORY_STORAGE_PATH")
26
+ or os.getenv("UALL_DATA_DIR")
27
+ or ".supermemory"
28
+ )
29
+ reset_service()
30
+ mcp = FastMCP("SuperMemory", json_response=True)
31
+
32
+ async def _call(name: str, arguments: dict | None = None) -> dict:
33
+ raw = await handle_tool(name, arguments or {}, data_dir=data_dir)
34
+ return json.loads(raw)
35
+
36
+ @mcp.tool(name="record_event")
37
+ async def record_event(
38
+ event_type: str,
39
+ summary: str,
40
+ workflow: str | None = None,
41
+ step: str | None = None,
42
+ tool: str | None = None,
43
+ agent: str | None = None,
44
+ domain: str | None = None,
45
+ language: str | None = None,
46
+ environment: str | None = None,
47
+ namespace: str = "global",
48
+ payload: dict[str, Any] | None = None,
49
+ ) -> dict[str, Any]:
50
+ """Record a bounded significant event. Full transcripts should not be sent here."""
51
+ return await _call(
52
+ "record_event",
53
+ {
54
+ "event_type": event_type,
55
+ "summary": summary,
56
+ "workflow": workflow,
57
+ "step": step,
58
+ "tool": tool,
59
+ "agent": agent,
60
+ "domain": domain,
61
+ "language": language,
62
+ "environment": environment,
63
+ "namespace": namespace,
64
+ "payload": payload,
65
+ },
66
+ )
67
+
68
+ @mcp.tool(name="record_failure")
69
+ async def record_failure(
70
+ summary: str,
71
+ workflow: str | None = None,
72
+ step: str | None = None,
73
+ tool: str | None = None,
74
+ agent: str | None = None,
75
+ domain: str | None = None,
76
+ language: str | None = None,
77
+ environment: str | None = None,
78
+ namespace: str = "global",
79
+ payload: dict[str, Any] | None = None,
80
+ ) -> dict[str, Any]:
81
+ """Record a bounded failure signal."""
82
+ return await _call(
83
+ "record_failure",
84
+ {
85
+ "summary": summary,
86
+ "workflow": workflow,
87
+ "step": step,
88
+ "tool": tool,
89
+ "agent": agent,
90
+ "domain": domain,
91
+ "language": language,
92
+ "environment": environment,
93
+ "namespace": namespace,
94
+ "payload": payload,
95
+ },
96
+ )
97
+
98
+ @mcp.tool(name="record_correction")
99
+ async def record_correction(
100
+ summary: str,
101
+ workflow: str | None = None,
102
+ step: str | None = None,
103
+ tool: str | None = None,
104
+ agent: str | None = None,
105
+ domain: str | None = None,
106
+ language: str | None = None,
107
+ environment: str | None = None,
108
+ namespace: str = "global",
109
+ payload: dict[str, Any] | None = None,
110
+ ) -> dict[str, Any]:
111
+ """Record a bounded correction signal."""
112
+ return await _call(
113
+ "record_correction",
114
+ {
115
+ "summary": summary,
116
+ "workflow": workflow,
117
+ "step": step,
118
+ "tool": tool,
119
+ "agent": agent,
120
+ "domain": domain,
121
+ "language": language,
122
+ "environment": environment,
123
+ "namespace": namespace,
124
+ "payload": payload,
125
+ },
126
+ )
127
+
128
+ @mcp.tool(name="reflect")
129
+ async def reflect(
130
+ event_ids: list[str],
131
+ suggestion: str | None = None,
132
+ lesson_text: str | None = None,
133
+ ) -> dict[str, Any]:
134
+ """Create a candidate lesson from evidence events."""
135
+ return await _call(
136
+ "reflect",
137
+ {"event_ids": event_ids, "suggestion": suggestion, "lesson_text": lesson_text},
138
+ )
139
+
140
+ @mcp.tool(name="validate")
141
+ async def validate(
142
+ reflection_id: str | None = None,
143
+ candidate_lesson: str | None = None,
144
+ event_ids: list[str] | None = None,
145
+ metadata: dict[str, Any] | None = None,
146
+ ) -> dict[str, Any]:
147
+ """Validate a lesson candidate and enqueue approved lessons for promotion."""
148
+ return await _call(
149
+ "validate",
150
+ {
151
+ "reflection_id": reflection_id,
152
+ "candidate_lesson": candidate_lesson,
153
+ "event_ids": event_ids,
154
+ "metadata": metadata,
155
+ },
156
+ )
157
+
158
+ @mcp.tool(name="process_promotions")
159
+ async def process_promotions(limit: int = 50) -> dict[str, Any]:
160
+ """Process pending validated lessons and promote passing items."""
161
+ return await _call("process_promotions", {"limit": limit})
162
+
163
+ @mcp.tool(name="retrieve")
164
+ async def retrieve(
165
+ query: str,
166
+ workflow: str | None = None,
167
+ step: str | None = None,
168
+ tool: str | None = None,
169
+ agent: str | None = None,
170
+ domain: str | None = None,
171
+ language: str | None = None,
172
+ environment: str | None = None,
173
+ namespace: str = "global",
174
+ top_k: int = 5,
175
+ max_tokens: int = 800,
176
+ ) -> dict[str, Any]:
177
+ """Retrieve policy-first, stage-aware lessons."""
178
+ return await _call(
179
+ "retrieve",
180
+ {
181
+ "query": query,
182
+ "workflow": workflow,
183
+ "step": step,
184
+ "tool": tool,
185
+ "agent": agent,
186
+ "domain": domain,
187
+ "language": language,
188
+ "environment": environment,
189
+ "namespace": namespace,
190
+ "top_k": top_k,
191
+ "max_tokens": max_tokens,
192
+ },
193
+ )
194
+
195
+ @mcp.tool(name="report_outcome")
196
+ async def report_outcome(
197
+ lesson_id: str,
198
+ used: bool,
199
+ accepted: bool,
200
+ improved: bool,
201
+ retrieval_id: str | None = None,
202
+ run_id: str | None = None,
203
+ ) -> dict[str, Any]:
204
+ """Report telemetry for a retrieved lesson."""
205
+ return await _call(
206
+ "report_outcome",
207
+ {
208
+ "lesson_id": lesson_id,
209
+ "used": used,
210
+ "accepted": accepted,
211
+ "improved": improved,
212
+ "retrieval_id": retrieval_id,
213
+ "run_id": run_id,
214
+ },
215
+ )
216
+
217
+ @mcp.tool(name="get_policies")
218
+ async def get_policies(namespace: str = "global") -> dict[str, Any]:
219
+ """Return active policies visible to a namespace."""
220
+ return await _call("get_policies", {"namespace": namespace})
221
+
222
+ @mcp.tool(name="add_policy")
223
+ async def add_policy(rule: str, namespace: str = "global", priority: int = 100) -> dict[str, Any]:
224
+ """Add a local policy rule."""
225
+ return await _call("add_policy", {"rule": rule, "namespace": namespace, "priority": priority})
226
+
227
+ @mcp.tool(name="add_skill")
228
+ async def add_skill(
229
+ name: str,
230
+ description: str,
231
+ steps: list[str],
232
+ workflow: str | None = None,
233
+ tools: list[str] | None = None,
234
+ namespace: str = "global",
235
+ version: str = "0.1.0",
236
+ metadata: dict[str, Any] | None = None,
237
+ ) -> dict[str, Any]:
238
+ """Add a reusable skill/workflow block."""
239
+ return await _call(
240
+ "add_skill",
241
+ {
242
+ "name": name,
243
+ "description": description,
244
+ "steps": steps,
245
+ "workflow": workflow,
246
+ "tools": tools,
247
+ "namespace": namespace,
248
+ "version": version,
249
+ "metadata": metadata,
250
+ },
251
+ )
252
+
253
+ @mcp.tool(name="search_skills")
254
+ async def search_skills(
255
+ query: str,
256
+ workflow: str | None = None,
257
+ namespace: str = "global",
258
+ top_k: int = 5,
259
+ ) -> dict[str, Any]:
260
+ """Search reusable skills visible to a namespace."""
261
+ return await _call(
262
+ "search_skills",
263
+ {"query": query, "workflow": workflow, "namespace": namespace, "top_k": top_k},
264
+ )
265
+
266
+ @mcp.tool(name="get_skill")
267
+ async def get_skill(skill_id: str) -> dict[str, Any]:
268
+ """Read a reusable skill by ID."""
269
+ return await _call("get_skill", {"skill_id": skill_id})
270
+
271
+ @mcp.tool(name="learn.run.start")
272
+ async def learn_run_start(
273
+ workflow_id: str,
274
+ step: str | None = None,
275
+ agents: list[str] | None = None,
276
+ namespace: str | None = None,
277
+ agent: str | None = None,
278
+ ) -> dict[str, Any]:
279
+ return await _call(
280
+ "learn.run.start",
281
+ {
282
+ "workflow_id": workflow_id,
283
+ "step": step,
284
+ "agents": agents or [],
285
+ "namespace": namespace,
286
+ "agent": agent,
287
+ },
288
+ )
289
+
290
+ @mcp.tool(name="learn.run.event")
291
+ async def learn_run_event(
292
+ run_id: str,
293
+ event_type: str,
294
+ snippet: str | None = None,
295
+ before: str | None = None,
296
+ after: str | None = None,
297
+ intent: str | None = None,
298
+ workflow: str | None = None,
299
+ step: str | None = None,
300
+ agent: str | None = None,
301
+ tags: list[str] | None = None,
302
+ ) -> dict[str, Any]:
303
+ return await _call(
304
+ "learn.run.event",
305
+ {
306
+ "run_id": run_id,
307
+ "event_type": event_type,
308
+ "snippet": snippet,
309
+ "before": before,
310
+ "after": after,
311
+ "intent": intent,
312
+ "workflow": workflow,
313
+ "step": step,
314
+ "agent": agent,
315
+ "tags": tags or [],
316
+ },
317
+ )
318
+
319
+ @mcp.tool(name="learn.run.end")
320
+ async def learn_run_end(
321
+ run_id: str,
322
+ success: bool,
323
+ lessons_used: list[str] | None = None,
324
+ ) -> dict[str, Any]:
325
+ return await _call(
326
+ "learn.run.end",
327
+ {"run_id": run_id, "success": success, "lessons_used": lessons_used or []},
328
+ )
329
+
330
+ @mcp.tool(name="learn.store")
331
+ async def learn_store(lesson_json: str) -> dict[str, Any]:
332
+ return await _call("learn.store", {"lesson_json": lesson_json})
333
+
334
+ @mcp.tool(name="learn.retrieve")
335
+ async def learn_retrieve(
336
+ query: str,
337
+ workflow: str | None = None,
338
+ step: str | None = None,
339
+ namespace: str | None = None,
340
+ max_tokens: int = 800,
341
+ ) -> dict[str, Any]:
342
+ return await _call(
343
+ "learn.retrieve",
344
+ {
345
+ "query": query,
346
+ "workflow": workflow,
347
+ "step": step,
348
+ "namespace": namespace,
349
+ "max_tokens": max_tokens,
350
+ },
351
+ )
352
+
353
+ @mcp.tool(name="learn.reflect")
354
+ async def learn_reflect(
355
+ event_ids: list[str] | None = None,
356
+ suggestion: str | None = None,
357
+ failure: str | None = None,
358
+ root_cause: str | None = None,
359
+ fix: str | None = None,
360
+ workflow: str | None = None,
361
+ step: str | None = None,
362
+ auto_promote: bool = False,
363
+ ) -> dict[str, Any]:
364
+ return await _call(
365
+ "learn.reflect",
366
+ {
367
+ "event_ids": event_ids or [],
368
+ "suggestion": suggestion,
369
+ "failure": failure,
370
+ "root_cause": root_cause,
371
+ "fix": fix,
372
+ "workflow": workflow,
373
+ "step": step,
374
+ "auto_promote": auto_promote,
375
+ },
376
+ )
377
+
378
+ @mcp.tool(name="learn.validate")
379
+ async def learn_validate(
380
+ failure: str,
381
+ fix: str,
382
+ root_cause: str | None = None,
383
+ event_ids: list[str] | None = None,
384
+ ) -> dict[str, Any]:
385
+ return await _call(
386
+ "learn.validate",
387
+ {
388
+ "failure": failure,
389
+ "fix": fix,
390
+ "root_cause": root_cause,
391
+ "event_ids": event_ids or [],
392
+ },
393
+ )
394
+
395
+ @mcp.tool(name="learn.evaluate")
396
+ async def learn_evaluate(run_id: str) -> dict[str, Any]:
397
+ return await _call("learn.evaluate", {"run_id": run_id})
398
+
399
+ @mcp.tool(name="learn.feedback")
400
+ async def learn_feedback(
401
+ rating: str,
402
+ comment: str | None = None,
403
+ run_id: str | None = None,
404
+ ) -> dict[str, Any]:
405
+ return await _call(
406
+ "learn.feedback",
407
+ {"rating": rating, "comment": comment, "run_id": run_id},
408
+ )
409
+
410
+ @mcp.tool(name="learn.improvements")
411
+ async def learn_improvements(
412
+ workflow_id: str | None = None,
413
+ agent_id: str | None = None,
414
+ ) -> dict[str, Any]:
415
+ return await _call(
416
+ "learn.improvements",
417
+ {"workflow_id": workflow_id, "agent_id": agent_id},
418
+ )
419
+
420
+ @mcp.tool(name="learn.analytics")
421
+ async def learn_analytics() -> dict[str, Any]:
422
+ return await _call("learn.analytics", {})
423
+
424
+ @mcp.tool(name="learn.policies")
425
+ async def learn_policies() -> dict[str, Any]:
426
+ return await _call("learn.policies", {})
427
+
428
+ @mcp.tool(name="learn.experiment")
429
+ async def learn_experiment(
430
+ resource_id: str,
431
+ variant_b: str,
432
+ traffic_split: float = 0.1,
433
+ ) -> dict[str, Any]:
434
+ return await _call(
435
+ "learn.experiment",
436
+ {"resource_id": resource_id, "variant_b": variant_b, "traffic_split": traffic_split},
437
+ )
438
+
439
+ @mcp.tool(name="learn.rollback")
440
+ async def learn_rollback(
441
+ resource_type: str,
442
+ resource_id: str,
443
+ target_version: str,
444
+ ) -> dict[str, Any]:
445
+ return await _call(
446
+ "learn.rollback",
447
+ {
448
+ "resource_type": resource_type,
449
+ "resource_id": resource_id,
450
+ "target_version": target_version,
451
+ },
452
+ )
453
+
454
+ @mcp.tool(name="learn.skills")
455
+ async def learn_skills(query: str) -> dict[str, Any]:
456
+ return await _call("learn.skills", {"query": query})
457
+
458
+ @mcp.tool(name="learn.telemetry")
459
+ async def learn_telemetry(
460
+ lesson_id: str,
461
+ run_id: str | None = None,
462
+ used: bool = False,
463
+ accepted: bool = False,
464
+ improved: bool | None = None,
465
+ telemetry_id: str | None = None,
466
+ ) -> dict[str, Any]:
467
+ return await _call(
468
+ "learn.telemetry",
469
+ {
470
+ "lesson_id": lesson_id,
471
+ "run_id": run_id,
472
+ "used": used,
473
+ "accepted": accepted,
474
+ "improved": improved,
475
+ "telemetry_id": telemetry_id,
476
+ },
477
+ )
478
+
479
+ @mcp.resource("supermemory://policies/active")
480
+ async def policies_resource() -> str:
481
+ """Active global policies."""
482
+ data = await _call("get_policies", {})
483
+ return json.dumps(data.get("policies", data), indent=2)
484
+
485
+ @mcp.resource("supermemory://lessons/{lesson_id}")
486
+ async def lesson_resource(lesson_id: str) -> str:
487
+ """Read a promoted lesson."""
488
+ from supermemory_mcp.handlers import get_service
489
+ svc = await get_service(data_dir)
490
+ lesson = await svc.get_lesson(lesson_id)
491
+ return json.dumps(lesson.model_dump(mode="json") if lesson else {"error": "not_found"}, indent=2)
492
+
493
+ @mcp.resource("supermemory://memory/{lesson_id}/provenance")
494
+ async def provenance_resource(lesson_id: str) -> str:
495
+ """Read a lesson provenance chain."""
496
+ from supermemory_mcp.handlers import get_service
497
+ svc = await get_service(data_dir)
498
+ prov = await svc.get_provenance(lesson_id)
499
+ return json.dumps(prov or {"error": "not_found"}, indent=2)
500
+
501
+ @mcp.resource("supermemory://skills/{skill_id}")
502
+ async def skill_resource(skill_id: str) -> str:
503
+ """Read a reusable skill."""
504
+ data = await _call("get_skill", {"skill_id": skill_id})
505
+ return json.dumps(data, indent=2)
506
+
507
+ return mcp
508
+
509
+
510
+ def main() -> None:
511
+ parser = argparse.ArgumentParser(description="Run the SuperMemory MCP server.")
512
+ parser.add_argument(
513
+ "--storage",
514
+ default=os.getenv("SUPERMEMORY_STORAGE_PATH") or os.getenv("UALL_DATA_DIR") or ".supermemory",
515
+ )
516
+ parser.add_argument("--transport", choices=["stdio", "streamable-http"], default="stdio")
517
+ args = parser.parse_args()
518
+ build_server(args.storage).run(transport=args.transport)
519
+
520
+
521
+ if __name__ == "__main__":
522
+ main()
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+
7
+ def bounded_text(value: Any, limit: int) -> str:
8
+ if isinstance(value, dict):
9
+ text = json.dumps(value, default=str)
10
+ else:
11
+ text = str(value)
12
+ return text[:limit]
13
+
14
+
15
+ def merge_text(parts: list[str], limit: int) -> str:
16
+ return bounded_text(" ".join(part for part in parts if part), limit)
uall/__init__.py ADDED
File without changes
@@ -0,0 +1,35 @@
1
+ from uall_core.ports.storage import StoragePort
2
+ from uall.evaluation.engine import EvaluationEngine
3
+ from uall.recommendations.engine import PatternDetector
4
+
5
+
6
+ class AnalyticsService:
7
+ def __init__(self, storage: StoragePort):
8
+ self.storage = storage
9
+ self.evaluator = EvaluationEngine(storage)
10
+
11
+ async def get_analytics(self) -> dict:
12
+ runs = await self.storage.list_runs()
13
+ lessons = await self.storage.list_lessons("active")
14
+ successes = sum(1 for r in runs if r.get("success"))
15
+ total = len(runs) or 1
16
+ return {
17
+ "total_runs": len(runs),
18
+ "success_rate": round(successes / total, 3),
19
+ "active_lessons": len(lessons),
20
+ "avg_lesson_confidence": round(
21
+ sum(l.confidence.overall for l in lessons) / max(len(lessons), 1), 3
22
+ ),
23
+ "memory_health": "good" if len(lessons) < 1000 else "review_pruning",
24
+ }
25
+
26
+ async def workflow_health(self, workflow_id: str) -> dict:
27
+ from uall.optimization.optimizers import WorkflowOptimizer
28
+
29
+ opt = WorkflowOptimizer(self.storage)
30
+ return await opt.analyze(workflow_id)
31
+
32
+ async def top_failures(self, limit: int = 10) -> list[dict]:
33
+ detector = PatternDetector(self.storage)
34
+ patterns = await detector.detect_patterns()
35
+ return patterns[:limit]
File without changes
@@ -0,0 +1,100 @@
1
+ import os
2
+ import uuid
3
+ from datetime import datetime
4
+
5
+ from uall_core.ports.storage import StoragePort
6
+ from uall_core.providers.heuristic import HeuristicLLMProvider
7
+ from uall_core.schemas.events import Event, EventType, Feedback, RunEnd, RunStart, StageMetadata
8
+ from uall_core.schemas.lesson import CandidateLesson
9
+
10
+
11
+ class EventCollector:
12
+ def __init__(self, storage: StoragePort):
13
+ self.storage = storage
14
+
15
+ async def start_run(self, data: RunStart) -> dict:
16
+ await self.storage.save_run_start(data)
17
+ event = Event(
18
+ event_id=f"evt_{uuid.uuid4().hex[:8]}",
19
+ event_type=EventType.WORKFLOW_START,
20
+ run_id=data.run_id,
21
+ stage=data.stage,
22
+ payload={"workflow_id": data.workflow_id, "agents": data.agents},
23
+ )
24
+ await self.storage.save_event(event)
25
+ return {"run_id": data.run_id, "status": "started"}
26
+
27
+ async def record_event(self, event: Event) -> dict:
28
+ await self.storage.save_event(event)
29
+ return {"event_id": event.event_id, "recorded": True}
30
+
31
+ async def end_run(self, data: RunEnd) -> dict:
32
+ await self.storage.save_run_end(data)
33
+ event = Event(
34
+ event_id=f"evt_{uuid.uuid4().hex[:8]}",
35
+ event_type=EventType.WORKFLOW_END,
36
+ run_id=data.run_id,
37
+ payload={"success": data.success, "metrics": data.metrics},
38
+ )
39
+ await self.storage.save_event(event)
40
+ return {"run_id": data.run_id, "status": "ended", "success": data.success}
41
+
42
+ async def record_feedback(self, feedback: Feedback) -> dict:
43
+ fid = await self.storage.save_feedback(feedback)
44
+ return {"feedback_id": fid}
45
+
46
+ def event_from_failure(
47
+ self,
48
+ run_id: str,
49
+ snippet: str,
50
+ stage: StageMetadata,
51
+ tags: list[str] | None = None,
52
+ ) -> Event:
53
+ return Event(
54
+ event_id=f"failure_{uuid.uuid4().hex[:8]}",
55
+ event_type=EventType.FAILURE,
56
+ run_id=run_id,
57
+ stage=stage,
58
+ tags=tags or [],
59
+ payload={"snippet": snippet[:500]},
60
+ )
61
+
62
+ def event_from_correction(
63
+ self,
64
+ run_id: str,
65
+ before: str,
66
+ after: str,
67
+ intent: str,
68
+ stage: StageMetadata,
69
+ ) -> Event:
70
+ return Event(
71
+ event_id=f"correction_{uuid.uuid4().hex[:8]}",
72
+ event_type=EventType.CORRECTION,
73
+ run_id=run_id,
74
+ stage=stage,
75
+ payload={"before": before[:300], "after": after[:300], "intent": intent},
76
+ )
77
+
78
+ async def get_candidates_from_run(self, run_id: str) -> list[CandidateLesson]:
79
+ run = await self.storage.get_run(run_id)
80
+ if not run:
81
+ return []
82
+ candidates = []
83
+ for evt in run.get("events", []):
84
+ if evt.get("event_type") in ("failure", "correction", "suggestion"):
85
+ stage = StageMetadata(**evt.get("stage", {}))
86
+ payload = evt.get("payload", {})
87
+ snippet = payload.get("snippet") or payload.get("intent") or ""
88
+ candidates.append(
89
+ CandidateLesson(
90
+ reflection_id="",
91
+ failure=snippet,
92
+ root_cause=payload.get("intent", ""),
93
+ fix=payload.get("after", snippet),
94
+ stage=stage,
95
+ run_id=run_id,
96
+ event_ids=[evt.get("event_id", "")],
97
+ evidence_payload=payload,
98
+ )
99
+ )
100
+ return candidates