ltcai 7.3.0 → 7.4.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/README.md CHANGED
@@ -200,32 +200,33 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
200
200
 
201
201
  ## Current Release
202
202
 
203
- The current release is **7.3.0 Runtime Contract & Retrieval Quality**:
204
-
205
- - Single-agent and multi-agent execution now share a serializable
206
- `agent-run-contract/v1` payload for run identity, mode, status, roles,
207
- timeline, retries, artifacts, and terminal state.
208
- - Agent runtime API responses include the shared contract, keeping real and
209
- simulated run history distinguishable as runtime extraction continues.
210
- - Brain quality evaluation now includes deterministic hybrid recall/ranking
211
- regression thresholds, not only "some recall exists" checks.
212
- - The 7.2.0 preview/diagnostics work remains the admin trust surface while
213
- 7.3.0 tightens the underlying run and retrieval contracts.
214
- - The roadmap focus is durability over model choice: knowledge, run evidence,
215
- and retrieval quality become measurable product assets.
216
-
217
- Expected artifacts for 7.3.0 release must use exact filenames:
218
-
219
- - `dist/ltcai-7.3.0-py3-none-any.whl`
220
- - `dist/ltcai-7.3.0.tar.gz`
221
- - `ltcai-7.3.0.tgz`
222
- - `dist/ltcai-7.3.0.vsix`
223
- - `src-tauri/target/release/bundle/dmg/Lattice AI_7.3.0_aarch64.dmg`
203
+ The current release is **7.4.0 Runtime Contract Convergence & Corpus Retrieval**:
204
+
205
+ - Agent runs, workflow runs, audit events, and realtime events now carry the
206
+ `agent-run-contract/v1` family envelope, so replay, admin review, and live
207
+ feeds can inspect run identity, runtime, status, timeline, and artifacts
208
+ through one contract shape.
209
+ - Persisted queued/running/terminal agent and workflow rows refresh their
210
+ contract on every state transition, including cancellation and interruption.
211
+ - Audit rows keep their existing admin fields while adding redacted contract
212
+ metadata; realtime SSE events keep `area`, `event_type`, `workspace_id`, and
213
+ `payload` while adding the same contract family.
214
+ - Brain quality evaluation now runs a real KnowledgeGraphStore + SearchService
215
+ corpus fixture with judged queries, recall, precision, NDCG, and must-include
216
+ hit-rate thresholds.
217
+
218
+ Expected artifacts for 7.4.0 release must use exact filenames:
219
+
220
+ - `dist/ltcai-7.4.0-py3-none-any.whl`
221
+ - `dist/ltcai-7.4.0.tar.gz`
222
+ - `ltcai-7.4.0.tgz`
223
+ - `dist/ltcai-7.4.0.vsix`
224
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_7.4.0_aarch64.dmg`
224
225
 
225
226
  Do not upload `dist/*`. Package registry publishing remains owner-run.
226
227
 
227
228
  See [docs/ROADMAP_RECOMMENDATIONS.md](docs/ROADMAP_RECOMMENDATIONS.md) for the
228
- strategic roadmap slice applied in 7.3.0 and the follow-up tracks.
229
+ strategic roadmap slices applied through 7.4.0 and the follow-up tracks.
229
230
 
230
231
  ## Known Limitations
231
232
 
@@ -242,6 +243,7 @@ strategic roadmap slice applied in 7.3.0 and the follow-up tracks.
242
243
 
243
244
  | Version | Theme |
244
245
  | --- | --- |
246
+ | 7.4.0 | Runtime Contract Convergence & Corpus Retrieval: agent/workflow/audit/realtime records share the agent-run-contract/v1 family, and retrieval quality gates run against a real corpus-scale fixture |
245
247
  | 7.3.0 | Runtime Contract & Retrieval Quality: shared agent-run contract across runtimes plus deterministic hybrid recall/ranking regression gates |
246
248
  | 7.2.0 | Runtime Trust Baseline: agent run preview/readiness, simulation-mode guardrails, live ToolRegistry manifest/diagnostics, and tests for dispatch/governance/catalog drift |
247
249
  | 7.1.0 | Brain Usability Completion: clearer first-run onboarding, ingestion progress/emergence, richer graph controls, inline answer proof, workspace/profile/admin discovery, empty/error/consent feedback, and VS Code sync status |
package/docs/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ The top entry is either the current unreleased main-branch work or the current
4
4
  release line. Older entries are historical and may describe behavior as it
5
5
  existed at that release.
6
6
 
7
+ ## [7.4.0] - 2026-06-20
8
+
9
+ > Runtime Contract Convergence & Corpus Retrieval. Completes the
10
+ > agent-run-contract/v1 family across run storage, workflow execution, audit
11
+ > events, realtime events, and a real corpus-scale retrieval quality gate.
12
+
13
+ ### Added
14
+ - Persisted agent and workflow run rows now carry refreshed contract metadata
15
+ for queued, running, terminal, cancelled, and interrupted states.
16
+ - Workflow engine results, replay payloads, audit log events, and realtime SSE
17
+ feed events now expose the same `agent-run-contract/v1` family envelope while
18
+ preserving existing top-level compatibility fields.
19
+ - Corpus-scale retrieval fixture with 30+ documents, judged queries,
20
+ must-include expectations, and thresholds for recall, precision, NDCG, and
21
+ hit rate.
22
+ - `scripts/brain_quality_eval.py` now exercises the real local
23
+ `KnowledgeGraphStore` + `SearchService` hybrid retrieval path before scoring.
24
+
25
+ ### Changed
26
+ - `RetrievalBenchmarkRunner` reports dynamic metric aliases for the selected
27
+ `top_k` and a `must_include_hit_rate`.
28
+ - Package/runtime/static metadata is synchronized to 7.4.0; package publish and
29
+ deployment remain owner-run only.
30
+
7
31
  ## [7.3.0] - 2026-06-20
8
32
 
9
33
  > Runtime Contract & Retrieval Quality. Turns the next AgentRuntime extraction
@@ -16,12 +16,25 @@ small release-sized work. The operating principle stays unchanged:
16
16
  - Security and trust: run contracts distinguish runtime type and execution mode
17
17
  so simulated output is not presented as real execution.
18
18
 
19
+ ## 7.4.0 Applied Slice
20
+
21
+ 7.4.0 completes the next roadmap slice without deferring it:
22
+
23
+ - Runtime convergence: agent runs, workflow runs, audit events, and realtime
24
+ events all expose the `agent-run-contract/v1` family envelope while keeping
25
+ legacy top-level fields for compatibility.
26
+ - Trust and operations: persisted run rows refresh their contract through
27
+ queued, running, terminal, cancelled, and interrupted states; audit events are
28
+ contracted only after secret redaction.
29
+ - Retrieval quality and scale: the CI quality gate now seeds a real local
30
+ Knowledge Graph corpus and scores SearchService hybrid retrieval with judged
31
+ queries, recall, precision, NDCG, and must-include hit-rate thresholds.
32
+
19
33
  ## Near-Term Tracks
20
34
 
21
35
  1. Retrieval quality and scale
22
- - Expand fixture datasets for hybrid search.
23
- - Add latency budgets for large corpora.
24
- - Track recall, precision, and ranking regressions in CI.
36
+ - Add latency budgets for larger corpora.
37
+ - Track per-channel keyword/vector/graph diagnostics in CI.
25
38
  - Tune semantic/graph/keyword fusion by query class.
26
39
 
27
40
  2. Incremental ingestion
@@ -30,8 +43,7 @@ small release-sized work. The operating principle stays unchanged:
30
43
  - Surface conflict resolution for contradictory memories.
31
44
 
32
45
  3. Runtime convergence
33
- - Use `agent-run-contract/v1` across single-agent, multi-agent, workflows,
34
- audit logs, and UI event streams.
46
+ - Drive UI run/replay/admin views from the shared contract family.
35
47
  - Keep simulation mode explicit and never record it as product success.
36
48
  - Route tools through explicit registry/governance contracts.
37
49
 
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "7.3.0"
29
+ __version__ = "7.4.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -285,6 +285,18 @@ class StructuredContextAssembler:
285
285
  # -----------------------------
286
286
  # 6. Retrieval Benchmark Fixture Runner
287
287
  # -----------------------------
288
+ def _relevance_grades(relevant: Any) -> Dict[str, float]:
289
+ """Normalize a ``relevant`` spec into a ``{doc_id: grade}`` map.
290
+
291
+ Accepts a graded dict (``{"doc": 3}``) or a binary list/set/tuple (grade 1
292
+ for every id), so judged fixtures can express graded relevance for nDCG
293
+ while older binary fixtures keep working unchanged.
294
+ """
295
+ if isinstance(relevant, dict):
296
+ return {str(k): float(v) for k, v in relevant.items()}
297
+ return {str(doc_id): 1.0 for doc_id in (relevant or [])}
298
+
299
+
288
300
  class RetrievalBenchmarkRunner:
289
301
  def __init__(self):
290
302
  self.results: List[Dict] = []
@@ -297,25 +309,36 @@ class RetrievalBenchmarkRunner:
297
309
  precisions = []
298
310
  ndcgs = []
299
311
  for query in judged:
300
- relevant = set(query.get("relevant") or [])
312
+ grades = _relevance_grades(query.get("relevant"))
313
+ relevant = set(grades)
301
314
  retrieved = list(query.get("retrieved") or [])[:top_k]
302
315
  if not relevant:
303
316
  continue
304
317
  hits = [doc_id for doc_id in retrieved if doc_id in relevant]
305
318
  recalls.append(len(hits) / len(relevant))
306
319
  precisions.append(len(hits) / max(1, len(retrieved)))
320
+ # Graded DCG: each retrieved doc contributes its relevance grade
321
+ # discounted by log2(rank + 2). IDCG ranks the highest grades
322
+ # first, so nDCG rewards putting the *most* relevant docs on top.
323
+ # Binary fixtures fall back to grade 1 via _relevance_grades.
307
324
  dcg = sum(
308
- 1.0 / math.log2(rank + 2)
325
+ grades.get(doc_id, 0.0) / math.log2(rank + 2)
309
326
  for rank, doc_id in enumerate(retrieved)
310
- if doc_id in relevant
311
327
  )
312
- ideal = sum(1.0 / math.log2(rank + 2) for rank in range(min(len(relevant), top_k)))
328
+ ideal_grades = sorted(grades.values(), reverse=True)[:top_k]
329
+ ideal = sum(g / math.log2(rank + 2) for rank, g in enumerate(ideal_grades))
313
330
  ndcgs.append(dcg / ideal if ideal else 0.0)
314
331
  recall = sum(recalls) / len(recalls) if recalls else 0.0
315
332
  precision = sum(precisions) / len(precisions) if precisions else 0.0
316
333
  ndcg = sum(ndcgs) / len(ndcgs) if ndcgs else 0.0
317
334
  else:
318
335
  recall = precision = ndcg = 0.0
336
+ must_include = [
337
+ bool(set(query.get("must_include") or []).intersection(set(list(query.get("retrieved") or [])[:top_k])))
338
+ for query in judged
339
+ if query.get("must_include")
340
+ ]
341
+ must_include_hit_rate = sum(1 for hit in must_include if hit) / len(must_include) if must_include else 1.0
319
342
  metrics = {
320
343
  "fixture": fixture_name,
321
344
  "queries": len(queries),
@@ -323,6 +346,10 @@ class RetrievalBenchmarkRunner:
323
346
  "recall@5": round(recall, 4),
324
347
  "precision@5": round(precision, 4),
325
348
  "ndcg@5": round(ndcg, 4),
349
+ f"recall@{top_k}": round(recall, 4),
350
+ f"precision@{top_k}": round(precision, 4),
351
+ f"ndcg@{top_k}": round(ndcg, 4),
352
+ "must_include_hit_rate": round(must_include_hit_rate, 4),
326
353
  "top_k": top_k,
327
354
  "judged": len(judged),
328
355
  }
@@ -0,0 +1,56 @@
1
+ """Corpus-scale retrieval fixture for the 7.4.0 Brain quality gate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ FIXTURE_NAME = "7.4.0-corpus-scale-hybrid-retrieval"
6
+ TOP_K = 5
7
+
8
+ DOCUMENTS = [
9
+ {"id": "doc:agent-runtime-contract", "type": "Decision", "title": "Agent runtime contract", "content": "Workflow run, agent run, audit event, and realtime event records must carry the agent-run-contract/v1 family envelope with run id, runtime, status, timeline, and artifacts."},
10
+ {"id": "doc:workflow-observability", "type": "Document", "title": "Workflow observability", "content": "Workflow Designer persists queued, running, awaiting approval, cancelled, interrupted, partial, failed, and ok runs with replayable timeline events."},
11
+ {"id": "doc:audit-trust", "type": "Security", "title": "Audit trust", "content": "Audit events are append-only, redacted before persistence, and linked to execution contracts for admin review without leaking secrets."},
12
+ {"id": "doc:realtime-feed", "type": "Document", "title": "Realtime execution feed", "content": "The realtime SSE bus publishes workspace, agent, workflow, and execution events with workspace scope, event type, payload, sequence, and contract metadata."},
13
+ {"id": "doc:retrieval-benchmark", "type": "Benchmark", "title": "Retrieval benchmark strategy", "content": "Hybrid retrieval quality must be checked with corpus-scale fixtures, judged queries, recall, precision, ndcg, and must-include hit rate."},
14
+ {"id": "doc:hybrid-search", "type": "Architecture", "title": "Hybrid search", "content": "SearchService fuses keyword, vector, and graph retrieval channels with weighted scores and scoped workspace filtering."},
15
+ {"id": "doc:knowledge-graph", "type": "Architecture", "title": "Knowledge graph stabilization", "content": "The Knowledge Graph preserves legacy read compatibility, non-destructive migrations, vector indexing, relationship traversal, and provenance."},
16
+ {"id": "doc:local-first", "type": "Strategy", "title": "Local-first product wedge", "content": "Lattice AI is local-first: durable knowledge, private workspace memory, local LLM routing, and model-independent context are the product wedge."},
17
+ {"id": "doc:brain-proof", "type": "UX", "title": "Brain proof and citations", "content": "Brain proof shows durable recall, source citation, graph counts, vector counts, and model continuity evidence in chat."},
18
+ {"id": "doc:onboarding", "type": "UX", "title": "Five minute onboarding loop", "content": "The first loop asks users to add a source, ask a question, see Brain proof, and understand expected installation time."},
19
+ {"id": "doc:vscode-sync", "type": "Integration", "title": "VS Code extension state", "content": "VS Code extension integration shows app connection state, indexing status, command routing, and extension-to-app sync health."},
20
+ {"id": "doc:workspace-admin", "type": "Workspace", "title": "Workspace profile admin discovery", "content": "Personal workspace, organization workspace, profile, admin, members, roles, permissions, and security surfaces must be discoverable."},
21
+ {"id": "doc:empty-error-consent", "type": "UX", "title": "Empty error consent states", "content": "Empty states, error states, consent prompts, retries, and privacy feedback should be explicit across ingestion, graph, chat, and workspace flows."},
22
+ {"id": "doc:tool-registry", "type": "Architecture", "title": "Tool registry separation", "content": "ToolRegistry separates tool definitions, permissions, dispatch, audit, and UI discovery from AgentRuntime orchestration."},
23
+ {"id": "doc:config-centralization", "type": "Architecture", "title": "Config centralization", "content": "Configuration should be centralized into explicit objects instead of hidden globals so runtime, tests, and packaging are predictable."},
24
+ {"id": "doc:server-decomposition", "type": "Architecture", "title": "Server decomposition", "content": "Server decomposition moves routers, services, runtime context, and persistence seams into focused modules with dependency injection."},
25
+ {"id": "doc:incremental-ingestion", "type": "Pipeline", "title": "Incremental ingestion", "content": "Background indexing needs duplicate detection, conflict resolution, incremental updates, and merge behavior for large personal corpora."},
26
+ {"id": "doc:vector-index-evolution", "type": "Pipeline", "title": "Vector index evolution", "content": "sqlite-vec migration, HNSW evaluation, query caching, embedding caching, and retrieval caching support larger corpora."},
27
+ {"id": "doc:security-zero-trust", "type": "Security", "title": "Zero trust defaults", "content": "Zero-trust defaults include audit logging, secret redaction, dependency vulnerability monitoring, and automated security scanning."},
28
+ {"id": "doc:agentic-hitl", "type": "Workflow", "title": "Agentic human review", "content": "Agentic workflows need human-in-the-loop review, approval gates, rollback paths, review queues, and auditable decisions."},
29
+ {"id": "doc:temporal-reasoning", "type": "Brain", "title": "Temporal reasoning", "content": "Temporal graph states, historical memory, contradiction detection, synthesis, and recommendations make the Brain proactive."},
30
+ {"id": "doc:multimodal-memory", "type": "Brain", "title": "Multimodal memory", "content": "Images, audio, video, documents, and chat should ingest into one durable Brain with retrieval and citation proof."},
31
+ {"id": "doc:developer-api", "type": "API", "title": "Developer API", "content": "A secure developer API exposes third-party access to workspace knowledge, tools, runs, contracts, and audit trails."},
32
+ {"id": "doc:team-brains", "type": "Business", "title": "Team Brains", "content": "Team Brains, encrypted sharing, premium cloud sync, hosted models, plugin marketplace, and public benchmarks support monetization."},
33
+ {"id": "doc:docker-tauri", "type": "Deployment", "title": "Deployment operations", "content": "Docker Compose deployment, one-click installation, Tauri auto-updates, rollback support, and exact release artifacts improve operations."},
34
+ {"id": "doc:model-routing", "type": "Runtime", "title": "Model orchestration", "content": "Ollama, LM Studio, MLX, and cloud models need automatic fallback, routing, and replaceable model boundaries."},
35
+ {"id": "doc:strict-quality", "type": "Quality", "title": "Strict code quality", "content": "Async-first architecture, dependency injection, centralized exceptions, mypy or pyright, TypeScript strict mode, E2E tests, and property tests reduce regressions."},
36
+ {"id": "distractor:theme-editor", "type": "Distractor", "title": "Theme editor", "content": "Color palettes, typography, and sidebar icon sizing are useful but unrelated to retrieval contract benchmarks."},
37
+ {"id": "distractor:calendar", "type": "Distractor", "title": "Calendar reminders", "content": "Calendar reminders and meeting agenda formatting are integrations but not the runtime event contract path."},
38
+ {"id": "distractor:billing", "type": "Distractor", "title": "Billing invoices", "content": "Invoices, payment receipts, and subscription emails can be stored but are not the Brain quality gate."},
39
+ {"id": "distractor:mobile-shell", "type": "Distractor", "title": "Mobile shell", "content": "A mobile companion can improve access but should not replace local-first desktop retrieval and run observability."},
40
+ {"id": "distractor:marketing", "type": "Distractor", "title": "Marketing landing page", "content": "Landing pages and hero copy do not prove durable knowledge or agent runtime correctness."},
41
+ ]
42
+
43
+ QUERIES = [
44
+ {"query": "agent workflow audit realtime contract run status timeline", "relevant": {"doc:agent-runtime-contract": 3, "doc:workflow-observability": 2, "doc:audit-trust": 2, "doc:realtime-feed": 2}, "must_include": ["doc:agent-runtime-contract"]},
45
+ {"query": "workflow queued running cancelled interrupted replay approval", "relevant": {"doc:workflow-observability": 3, "doc:agentic-hitl": 2}, "must_include": ["doc:workflow-observability"]},
46
+ {"query": "audit log redacted secrets admin review contract", "relevant": {"doc:audit-trust": 3, "doc:security-zero-trust": 2}, "must_include": ["doc:audit-trust"]},
47
+ {"query": "SSE realtime workspace event payload sequence contract", "relevant": {"doc:realtime-feed": 3, "doc:agent-runtime-contract": 2}, "must_include": ["doc:realtime-feed"]},
48
+ {"query": "hybrid retrieval keyword vector graph weighted scoped search", "relevant": {"doc:hybrid-search": 3, "doc:knowledge-graph": 2, "doc:retrieval-benchmark": 2}, "must_include": ["doc:hybrid-search"]},
49
+ {"query": "corpus scale recall precision ndcg judged queries", "relevant": {"doc:retrieval-benchmark": 3, "doc:hybrid-search": 2}, "must_include": ["doc:retrieval-benchmark"]},
50
+ {"query": "local first model independent durable memory brain proof citation", "relevant": {"doc:local-first": 3, "doc:brain-proof": 2}, "must_include": ["doc:local-first"]},
51
+ {"query": "incremental indexing duplicate conflict merge background ingestion", "relevant": {"doc:incremental-ingestion": 3, "doc:vector-index-evolution": 2}, "must_include": ["doc:incremental-ingestion"]},
52
+ {"query": "workspace admin profile organization members roles permissions", "relevant": {"doc:workspace-admin": 3, "doc:security-zero-trust": 2}, "must_include": ["doc:workspace-admin"]},
53
+ {"query": "tool registry separation dependency injection runtime context", "relevant": {"doc:tool-registry": 3, "doc:server-decomposition": 2, "doc:config-centralization": 2}, "must_include": ["doc:tool-registry"]},
54
+ {"query": "human in the loop approval review queue rollback", "relevant": {"doc:agentic-hitl": 3, "doc:workflow-observability": 2}, "must_include": ["doc:agentic-hitl"]},
55
+ {"query": "model routing ollama lm studio mlx cloud fallback replaceable", "relevant": {"doc:model-routing": 3, "doc:local-first": 2}, "must_include": ["doc:model-routing"]},
56
+ ]
@@ -4,6 +4,26 @@ These contracts are deliberately small and serializable. The single-agent
4
4
  state machine and the multi-agent facade can evolve independently, but callers
5
5
  should see the same run identity, mode, status, role, timeline and artifact
6
6
  shape across both paths.
7
+
8
+ Contract family (7.4.0)
9
+ -----------------------
10
+ Four observability surfaces — agent runs, workflow runs, audit events, and
11
+ realtime events — now share one **contract family**, ``agent-run-contract/v1``.
12
+ Every record emitted by any of these surfaces carries the same envelope keys so
13
+ a single consumer (timeline UI, exporter, replay tooling) can treat them
14
+ uniformly:
15
+
16
+ * ``family`` — always :data:`CONTRACT_FAMILY` (``agent-run-contract/v1``).
17
+ * ``schema_version`` — the per-surface schema string (see ``*_SCHEMA`` below).
18
+ * ``kind`` — one of :data:`CONTRACT_KINDS`.
19
+ * ``id`` — the record identity (run id / event id), may be ``None``.
20
+ * ``status`` — a normalized lifecycle status string.
21
+ * ``timestamp`` — ISO-8601 second-precision emit/observe time.
22
+
23
+ Each surface keeps its own rich, surface-specific keys *in addition* to the
24
+ envelope — the envelope is purely additive, so existing consumers are never
25
+ broken. :func:`stamp_contract` is the single place that writes the envelope;
26
+ :func:`is_contract_member` validates membership for tests and importers.
7
27
  """
8
28
 
9
29
  from __future__ import annotations
@@ -17,6 +37,71 @@ def runtime_timestamp() -> str:
17
37
  return datetime.now().isoformat(timespec="seconds")
18
38
 
19
39
 
40
+ # ── Contract family identity ────────────────────────────────────────────────
41
+ # A single family string ties every observability surface together. Per-surface
42
+ # schema versions live *under* the family so each surface can rev independently
43
+ # without forking the family contract consumers depend on.
44
+ CONTRACT_FAMILY = "agent-run-contract/v1"
45
+
46
+ AGENT_RUN_SCHEMA = "agent-run-contract/v1"
47
+ WORKFLOW_RUN_SCHEMA = "workflow-run-contract/v1"
48
+ AUDIT_EVENT_SCHEMA = "audit-event-contract/v1"
49
+ REALTIME_EVENT_SCHEMA = "realtime-event-contract/v1"
50
+
51
+ CONTRACT_KINDS = ("agent_run", "workflow_run", "audit_event", "realtime_event")
52
+
53
+ # The envelope keys every family member is guaranteed to expose.
54
+ CONTRACT_ENVELOPE_KEYS = ("family", "schema_version", "kind", "id", "status", "timestamp")
55
+
56
+ _SCHEMA_FOR_KIND = {
57
+ "agent_run": AGENT_RUN_SCHEMA,
58
+ "workflow_run": WORKFLOW_RUN_SCHEMA,
59
+ "audit_event": AUDIT_EVENT_SCHEMA,
60
+ "realtime_event": REALTIME_EVENT_SCHEMA,
61
+ }
62
+
63
+
64
+ def stamp_contract(
65
+ body: Dict[str, Any],
66
+ *,
67
+ kind: str,
68
+ identity: Optional[str],
69
+ status: str,
70
+ timestamp: Optional[str] = None,
71
+ ) -> Dict[str, Any]:
72
+ """Return ``body`` with the ``agent-run-contract/v1`` envelope merged in.
73
+
74
+ Additive and idempotent: existing keys in ``body`` win for everything except
75
+ the six envelope keys, which are authoritative. ``kind`` must be one of
76
+ :data:`CONTRACT_KINDS`; the schema version is derived from it so callers
77
+ cannot mismatch ``kind`` and ``schema_version``.
78
+ """
79
+ if kind not in _SCHEMA_FOR_KIND:
80
+ raise ValueError(f"unknown contract kind: {kind!r}")
81
+ enveloped = dict(body)
82
+ enveloped["family"] = CONTRACT_FAMILY
83
+ enveloped["schema_version"] = _SCHEMA_FOR_KIND[kind]
84
+ enveloped["kind"] = kind
85
+ enveloped["id"] = identity
86
+ enveloped["status"] = status
87
+ enveloped["timestamp"] = timestamp or body.get("timestamp") or runtime_timestamp()
88
+ return enveloped
89
+
90
+
91
+ def is_contract_member(record: Any) -> bool:
92
+ """True if ``record`` carries a valid ``agent-run-contract/v1`` envelope."""
93
+ if not isinstance(record, dict):
94
+ return False
95
+ if record.get("family") != CONTRACT_FAMILY:
96
+ return False
97
+ kind = record.get("kind")
98
+ if kind not in _SCHEMA_FOR_KIND:
99
+ return False
100
+ if record.get("schema_version") != _SCHEMA_FOR_KIND[kind]:
101
+ return False
102
+ return all(key in record for key in CONTRACT_ENVELOPE_KEYS)
103
+
104
+
20
105
  @dataclass(frozen=True)
21
106
  class AgentRunContract:
22
107
  run_id: Optional[str]
@@ -38,13 +123,11 @@ class AgentRunContract:
38
123
  return self.status in {"ok", "retried_ok", "failed", "rejected", "cancelled", "interrupted", "partial", "done"}
39
124
 
40
125
  def as_dict(self) -> Dict[str, Any]:
41
- return {
42
- "schema_version": self.schema_version,
126
+ body = {
43
127
  "run_id": self.run_id,
44
128
  "agent_id": self.agent_id,
45
129
  "runtime": self.runtime,
46
130
  "mode": self.mode,
47
- "status": self.status,
48
131
  "goal": self.goal,
49
132
  "roles": list(self.roles),
50
133
  "current_role": self.current_role,
@@ -54,6 +137,12 @@ class AgentRunContract:
54
137
  "blocking_reasons": list(self.blocking_reasons),
55
138
  "is_terminal": self.is_terminal,
56
139
  }
140
+ return stamp_contract(
141
+ body,
142
+ kind="agent_run",
143
+ identity=self.run_id,
144
+ status=self.status,
145
+ )
57
146
 
58
147
 
59
148
  def multi_agent_contract(
@@ -80,6 +169,41 @@ def multi_agent_contract(
80
169
  ).as_dict()
81
170
 
82
171
 
172
+ def run_record_contract(run: Dict[str, Any], *, runtime: str = "multi_agent") -> Dict[str, Any]:
173
+ """Build the family contract from a persisted agent run row."""
174
+ run = dict(run or {})
175
+ timeline = list(run.get("timeline") or [])
176
+ retry_history = list(run.get("retry_history") or [])
177
+ roles = list(run.get("roles_run") or run.get("requested_roles") or [])
178
+ if not roles:
179
+ roles = [
180
+ str(item.get("role"))
181
+ for item in timeline
182
+ if isinstance(item, dict) and item.get("role")
183
+ ]
184
+ return AgentRunContract(
185
+ run_id=run.get("id") or run.get("run_id"),
186
+ agent_id=str(run.get("agent_id") or "agent:executor"),
187
+ runtime=runtime,
188
+ mode=str(run.get("mode") or "simulation"),
189
+ status=str(run.get("status") or "unknown"),
190
+ goal=str(run.get("input") or run.get("goal") or ""),
191
+ roles=roles,
192
+ current_role=run.get("current_role"),
193
+ retries=len(retry_history),
194
+ timeline=timeline,
195
+ artifacts=[
196
+ {
197
+ "type": "run_record",
198
+ "workspace_id": run.get("workspace_id"),
199
+ "graph_node_id": run.get("graph_node_id"),
200
+ "execution_mode": run.get("execution_mode"),
201
+ }
202
+ ],
203
+ blocking_reasons=[str(run.get("error"))] if run.get("error") else [],
204
+ ).as_dict()
205
+
206
+
83
207
  def single_agent_contract(
84
208
  *,
85
209
  ctx: Any,
@@ -105,3 +229,114 @@ def single_agent_contract(
105
229
  artifacts=[],
106
230
  blocking_reasons=list(blocking_reasons or []),
107
231
  ).as_dict()
232
+
233
+
234
+ # ── Sibling-surface contract builders ───────────────────────────────────────
235
+ # These wrap the other three observability surfaces into the same family so a
236
+ # consumer can iterate agent runs, workflow runs, audit events and realtime
237
+ # events through one envelope.
238
+
239
+ def workflow_run_contract(run: Any) -> Dict[str, Any]:
240
+ """Wrap a workflow run dict (``WorkflowRun.as_dict()``) in the family envelope.
241
+
242
+ Accepts either a ``WorkflowRun`` instance or its serialized dict so the
243
+ engine can stamp without importing the dataclass here (avoids a cycle).
244
+ """
245
+ body = run.as_dict() if hasattr(run, "as_dict") else dict(run or {})
246
+ run_id = body.get("id") or body.get("run_id") or body.get("workflow_id")
247
+ workflow_id = body.get("workflow_id")
248
+ contract_body = {
249
+ "run_id": run_id,
250
+ "agent_id": f"workflow:{workflow_id or body.get('name') or 'workflow'}",
251
+ "runtime": "workflow",
252
+ "mode": body.get("mode") or "live",
253
+ "goal": body.get("name") or "workflow",
254
+ "roles": ["workflow"],
255
+ "current_role": body.get("current_node") or body.get("paused_node"),
256
+ "retries": 0,
257
+ "timeline": list(body.get("timeline") or []),
258
+ "artifacts": [
259
+ {
260
+ "type": "workflow_outputs",
261
+ "workflow_id": workflow_id,
262
+ "outputs": body.get("outputs") or {},
263
+ "pause": body.get("pause") or body.get("pending_approval"),
264
+ }
265
+ ],
266
+ "blocking_reasons": [str((body.get("outputs") or {}).get("error"))] if (body.get("outputs") or {}).get("error") else [],
267
+ "is_terminal": str(body.get("status") or "") in {"ok", "failed", "cancelled", "interrupted", "partial", "rejected"},
268
+ }
269
+ return stamp_contract(
270
+ contract_body,
271
+ kind="workflow_run",
272
+ identity=run_id,
273
+ status=str(body.get("status") or "unknown"),
274
+ timestamp=body.get("started_at") or body.get("created_at"),
275
+ )
276
+
277
+
278
+ def audit_event_contract(event: Dict[str, Any]) -> Dict[str, Any]:
279
+ """Wrap a single audit-log event in the family envelope.
280
+
281
+ Audit events have no lifecycle ``status``; we expose the event type as the
282
+ status so the envelope stays uniform and filterable. Identity is a stable
283
+ ``event_type@timestamp`` key (audit events are append-only, not addressable).
284
+ """
285
+ body = dict(event or {})
286
+ event_type = str(body.get("event_type") or "event")
287
+ ts = body.get("timestamp")
288
+ identity = body.get("event_id") or (f"{event_type}@{ts}" if ts else event_type)
289
+ contract_body = {
290
+ "run_id": body.get("run_id"),
291
+ "agent_id": str(body.get("agent_id") or body.get("workflow_id") or f"audit:{event_type}"),
292
+ "runtime": "audit",
293
+ "mode": "event",
294
+ "goal": event_type,
295
+ "roles": [],
296
+ "current_role": None,
297
+ "retries": int(body.get("retries") or 0),
298
+ "timeline": [{"event": event_type, "timestamp": ts, "status": body.get("status") or event_type}],
299
+ "artifacts": [{"type": "audit_payload", "payload": body}],
300
+ "blocking_reasons": [],
301
+ "is_terminal": True,
302
+ }
303
+ return stamp_contract(
304
+ contract_body,
305
+ kind="audit_event",
306
+ identity=identity,
307
+ status=event_type,
308
+ timestamp=ts,
309
+ )
310
+
311
+
312
+ def realtime_event_contract(event: Dict[str, Any]) -> Dict[str, Any]:
313
+ """Wrap a realtime bus event in the family envelope.
314
+
315
+ The bus already assigns a monotonic ``seq`` and ``received_at``; we reuse
316
+ those as identity and timestamp so the envelope is consistent with the
317
+ feed's own ordering guarantees.
318
+ """
319
+ body = dict(event or {})
320
+ seq = body.get("seq")
321
+ payload = body.get("payload") if isinstance(body.get("payload"), dict) else {}
322
+ contract_body = {
323
+ "run_id": payload.get("run_id") or body.get("run_id"),
324
+ "agent_id": str(payload.get("agent_id") or payload.get("workflow_id") or f"realtime:{body.get('area') or 'workspace'}"),
325
+ "runtime": "realtime",
326
+ "mode": "event",
327
+ "goal": str(body.get("event_type") or "event"),
328
+ "roles": [],
329
+ "current_role": payload.get("current_role"),
330
+ "retries": int(payload.get("retries") or 0),
331
+ "timeline": [{"event": body.get("event_type") or "event", "timestamp": body.get("received_at"), "payload": payload}],
332
+ "artifacts": [{"type": "realtime_payload", "payload": payload}],
333
+ "blocking_reasons": [str(payload.get("error"))] if payload.get("error") else [],
334
+ "is_terminal": str(payload.get("status") or "") in {"ok", "failed", "cancelled", "interrupted", "partial", "rejected"},
335
+ }
336
+ return stamp_contract(
337
+ contract_body,
338
+ kind="realtime_event",
339
+ identity=f"rt:{seq}" if seq is not None else None,
340
+ status=str(body.get("event_type") or "event"),
341
+ timestamp=body.get("received_at"),
342
+ )
@@ -21,7 +21,7 @@ from typing import Any, Callable, Dict, List, Optional
21
21
  from .contracts import multi_agent_contract
22
22
 
23
23
 
24
- MULTI_AGENT_VERSION = "7.3.0"
24
+ MULTI_AGENT_VERSION = "7.4.0"
25
25
 
26
26
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
27
27
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -286,6 +286,7 @@ class AgentRunResult:
286
286
  retry_history: List[Dict[str, Any]] = field(default_factory=list)
287
287
  plan_review: Dict[str, Any] = field(default_factory=dict)
288
288
  memory_snapshots: List[Dict[str, Any]] = field(default_factory=list)
289
+ goal: str = ""
289
290
  # "simulation" = deterministic LLM-free runner; "llm" = model-driven (v4 runtime).
290
291
  mode: str = "simulation"
291
292
 
@@ -306,8 +307,9 @@ class AgentRunResult:
306
307
  "retry_history": self.retry_history,
307
308
  "plan_review": self.plan_review,
308
309
  "memory_snapshots": self.memory_snapshots,
310
+ "goal": self.goal,
309
311
  }
310
- payload["contract"] = multi_agent_contract(result=self, goal=self.output)
312
+ payload["contract"] = multi_agent_contract(result=self, goal=self.goal or self.output)
311
313
  return payload
312
314
 
313
315
 
@@ -800,5 +802,6 @@ class MultiAgentOrchestrator:
800
802
  retry_history=ctx.retry_history,
801
803
  plan_review=ctx.plan_review,
802
804
  memory_snapshots=ctx.memory_snapshots,
805
+ goal=ctx.goal,
803
806
  mode=self.mode,
804
807
  )
@@ -27,6 +27,8 @@ from dataclasses import dataclass, field
27
27
  from datetime import datetime
28
28
  from typing import Any, Callable, Dict, List, Optional
29
29
 
30
+ from lattice_brain.runtime.contracts import workflow_run_contract
31
+
30
32
 
31
33
  WORKFLOW_ENGINE_VERSION = "2.2.0"
32
34
 
@@ -198,7 +200,13 @@ class WorkflowRun:
198
200
  paused_context: Optional[Dict[str, Any]] = None
199
201
 
200
202
  def as_dict(self) -> Dict[str, Any]:
201
- return {
203
+ # Native workflow-run shape is preserved unchanged for existing readers.
204
+ # A normalized ``agent-run-contract/v1`` projection is attached under
205
+ # ``contract`` so workflow runs sit in the same observability family as
206
+ # agent runs, audit events and realtime events (mirrors the audit-log
207
+ # and realtime-bus convention). The projection is built from the native
208
+ # dict — never from ``self`` — so it cannot recurse back into as_dict().
209
+ body = {
202
210
  "workflow_id": self.workflow_id,
203
211
  "name": self.name,
204
212
  "status": self.status,
@@ -211,6 +219,8 @@ class WorkflowRun:
211
219
  "pending_approval": self.pending_approval,
212
220
  "paused_context": self.paused_context,
213
221
  }
222
+ body["contract"] = workflow_run_contract(body)
223
+ return body
214
224
 
215
225
 
216
226
  class ApprovalRequired(Exception):
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "7.3.0"
3
+ __version__ = "7.4.0"
@@ -5,9 +5,12 @@ import logging
5
5
  import os
6
6
  import re
7
7
  import threading
8
+ import hashlib
8
9
  from pathlib import Path
9
10
  from typing import Any, Callable, Dict, List, Optional
10
11
 
12
+ from lattice_brain.runtime.contracts import audit_event_contract
13
+
11
14
  from . import timezones
12
15
  from .security import redact_secrets
13
16
 
@@ -42,12 +45,21 @@ def get_audit_log(audit_file: Path) -> List[Dict]:
42
45
  def append_audit_event(audit_file: Path, event_type: str, **payload) -> None:
43
46
  try:
44
47
  safe_payload = redact_secrets(payload)
48
+ timestamp = timezones.now_iso()
49
+ event_hash = hashlib.sha256(
50
+ json.dumps([event_type, timestamp, safe_payload], ensure_ascii=False, sort_keys=True, default=str).encode("utf-8")
51
+ ).hexdigest()[:24]
45
52
  event = {
53
+ "event_id": f"audit-{event_hash}",
46
54
  "event_type": event_type,
47
55
  # item 7: 대시보드 "오늘" 계산과 동일한 시간대 기준으로 기록한다.
48
- "timestamp": timezones.now_iso(),
56
+ "timestamp": timestamp,
49
57
  **safe_payload,
50
58
  }
59
+ # Stamp the agent-run-contract/v1 family envelope so audit rows are
60
+ # uniform with agent/workflow/realtime records. Additive — existing
61
+ # readers ignore the extra keys; new consumers can filter by family.
62
+ event["contract"] = audit_event_contract(event)
51
63
  with _history_lock:
52
64
  events = get_audit_log(audit_file)
53
65
  events.append(event)
@@ -236,6 +248,8 @@ def build_admin_audit_report(
236
248
 
237
249
  def _public_audit_event(event: Dict) -> Dict:
238
250
  allowed = {
251
+ # Stable event id + nested agent-run-contract/v1 family projection.
252
+ "event_id", "contract",
239
253
  "event_type", "timestamp", "role", "user_email", "user_nickname", "source",
240
254
  "conversation_id", "workspace_id", "command", "scope", "target_email", "filename", "mime_type",
241
255
  "ext", "bytes", "extracted_chars", "graph_node", "keep_last", "removed", "kept",
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "7.3.0"
14
+ MARKETPLACE_VERSION = "7.4.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -32,6 +32,8 @@ import threading
32
32
  from datetime import datetime
33
33
  from typing import Any, AsyncIterator, Dict, List, Optional, Set
34
34
 
35
+ from lattice_brain.runtime.contracts import realtime_event_contract
36
+
35
37
 
36
38
  REALTIME_VERSION = "2.2.0"
37
39
  _FEED_LIMIT = 200
@@ -99,6 +101,7 @@ class RealtimeBus:
99
101
  "payload": event.get("payload", {}),
100
102
  **{k: v for k, v in event.items() if k not in {"area", "event_type", "workspace_id", "payload"}},
101
103
  }
104
+ enriched["contract"] = realtime_event_contract(enriched)
102
105
  self._feed.append(enriched)
103
106
  if len(self._feed) > _FEED_LIMIT:
104
107
  self._feed = self._feed[-_FEED_LIMIT:]
@@ -18,8 +18,10 @@ from datetime import datetime
18
18
  from pathlib import Path
19
19
  from typing import Any, Callable, Dict, Iterable, List, Optional
20
20
 
21
+ from lattice_brain.runtime.contracts import realtime_event_contract, run_record_contract, workflow_run_contract
21
22
 
22
- WORKSPACE_OS_VERSION = "7.3.0"
23
+
24
+ WORKSPACE_OS_VERSION = "7.4.0"
23
25
 
24
26
  # Workspace types separate single-user Personal workspaces from shared
25
27
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -526,6 +528,7 @@ class WorkspaceOSStore:
526
528
  "workspace_id": self._resolve_scope(workspace_id, state),
527
529
  "payload": payload,
528
530
  }
531
+ event["contract"] = realtime_event_contract({"seq": event["id"], "received_at": event["timestamp"], **event})
529
532
  state.setdefault("timeline", []).append(event)
530
533
  self.save_state(state)
531
534
  if self.event_sink is not None:
@@ -1565,6 +1568,13 @@ class WorkspaceOSStore:
1565
1568
  if status == "failed":
1566
1569
  self._emit_execution_event(area="agent", event_type="execution_failed", payload={"run_id": run["id"], "agent_id": agent_id, "status": status}, workspace_id=resolved_workspace)
1567
1570
  self.record_timeline_event("agent", "agent_run", {"run_id": run["id"], "agent_id": agent_id, "status": status}, workspace_id=resolved_workspace)
1571
+ run["contract"] = run_record_contract(run)
1572
+ state = self.load_state()
1573
+ for item in _listify(state.get("agent_runs")):
1574
+ if item.get("id") == run["id"]:
1575
+ item["contract"] = run["contract"]
1576
+ break
1577
+ self.save_state(state)
1568
1578
  return run
1569
1579
 
1570
1580
  def update_agent_run(
@@ -1636,6 +1646,13 @@ class WorkspaceOSStore:
1636
1646
  run["graph_error"] = str(exc)
1637
1647
 
1638
1648
  self.save_state(state)
1649
+ run["contract"] = run_record_contract(run)
1650
+ state = self.load_state()
1651
+ for item in _listify(state.get("agent_runs")):
1652
+ if item.get("id") == run_id:
1653
+ item["contract"] = run["contract"]
1654
+ break
1655
+ self.save_state(state)
1639
1656
 
1640
1657
  timeline = run.get("timeline") or []
1641
1658
  if len(timeline) > old_timeline_len:
@@ -1774,6 +1791,13 @@ class WorkspaceOSStore:
1774
1791
  elif status in {"ok", "partial"}:
1775
1792
  self._emit_execution_event(area="workflow", event_type="workflow_completed", payload={"run_id": run["id"], "workflow_id": workflow_id, "status": status}, workspace_id=resolved_workspace)
1776
1793
  self.record_timeline_event("workflow", "workflow_run", {"run_id": run["id"], "workflow_id": workflow_id, "status": status}, workspace_id=resolved_workspace)
1794
+ run["contract"] = workflow_run_contract(run)
1795
+ state = self.load_state()
1796
+ for item in _listify(state.get("workflow_runs")):
1797
+ if item.get("id") == run["id"]:
1798
+ item["contract"] = run["contract"]
1799
+ break
1800
+ self.save_state(state)
1777
1801
  return run
1778
1802
 
1779
1803
  def update_workflow_run(
@@ -1835,6 +1859,13 @@ class WorkspaceOSStore:
1835
1859
  run["graph_error"] = str(exc)
1836
1860
 
1837
1861
  self.save_state(state)
1862
+ run["contract"] = workflow_run_contract(run)
1863
+ state = self.load_state()
1864
+ for item in _listify(state.get("workflow_runs")):
1865
+ if item.get("id") == run_id:
1866
+ item["contract"] = run["contract"]
1867
+ break
1868
+ self.save_state(state)
1838
1869
 
1839
1870
  timeline = run.get("timeline") or []
1840
1871
  if len(timeline) > old_timeline_len:
@@ -2053,6 +2084,7 @@ class WorkspaceOSStore:
2053
2084
  "run_id": run_id,
2054
2085
  "status": run.get("status"),
2055
2086
  "workspace_id": self._record_workspace(run),
2087
+ "contract": run.get("contract") or run_record_contract(run),
2056
2088
  "replayable": True,
2057
2089
  "frames": self._replay_frames(run, kind="agent"),
2058
2090
  "handoffs": run.get("handoffs") or [],
@@ -2068,6 +2100,7 @@ class WorkspaceOSStore:
2068
2100
  "run_id": run_id,
2069
2101
  "status": run.get("status"),
2070
2102
  "workspace_id": self._record_workspace(run),
2103
+ "contract": run.get("contract") or workflow_run_contract(run),
2071
2104
  "replayable": True,
2072
2105
  "frames": self._replay_frames(run, kind="workflow"),
2073
2106
  "outputs": run.get("outputs") or {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -12,8 +12,15 @@ import sys
12
12
  import tempfile
13
13
  from pathlib import Path
14
14
 
15
+ REPO_ROOT = Path(__file__).resolve().parents[1]
16
+ if str(REPO_ROOT) not in sys.path:
17
+ sys.path.insert(0, str(REPO_ROOT))
18
+
19
+ from knowledge_graph import KnowledgeGraphStore
15
20
  from lattice_brain.quality import RetrievalBenchmarkRunner
21
+ from latticeai.services.search_service import SearchService
16
22
  from latticeai.services.memory_service import MemoryService
23
+ from lattice_brain.retrieval_benchmark_fixtures import DOCUMENTS, FIXTURE_NAME, QUERIES, TOP_K
17
24
 
18
25
 
19
26
  class _EvalStore:
@@ -81,6 +88,37 @@ def _fail(message: str) -> int:
81
88
  return 1
82
89
 
83
90
 
91
+ def _corpus_scale_retrieval_metrics(tmp_path: Path) -> dict:
92
+ graph = KnowledgeGraphStore(tmp_path / "kg.sqlite", tmp_path / "blobs")
93
+ id_map = {}
94
+ for doc in DOCUMENTS:
95
+ result = graph.ingest_event(
96
+ doc["type"],
97
+ f"{doc['title']} {doc['content']}",
98
+ source="retrieval-benchmark-v740",
99
+ conversation_id="retrieval-benchmark-v740",
100
+ metadata={
101
+ "fixture_id": doc["id"],
102
+ "title": doc["title"],
103
+ "content": doc["content"],
104
+ "workspace_id": "personal",
105
+ },
106
+ )
107
+ id_map[doc["id"]] = result["node_id"]
108
+ service = SearchService(graph)
109
+ judged = []
110
+ for query in QUERIES:
111
+ payload = service.hybrid_search(query["query"], limit=TOP_K, keyword_limit=TOP_K, vector_limit=TOP_K, graph_limit=TOP_K)
112
+ retrieved = [item.get("node_id") or item.get("id") for item in payload.get("matches", [])]
113
+ judged.append({
114
+ "query": query["query"],
115
+ "relevant": [id_map[item] for item in query["relevant"]],
116
+ "must_include": [id_map[item] for item in query.get("must_include", [])],
117
+ "retrieved": retrieved,
118
+ })
119
+ return RetrievalBenchmarkRunner().run_fixture(FIXTURE_NAME, judged, top_k=TOP_K)
120
+
121
+
84
122
  def main() -> int:
85
123
  with tempfile.TemporaryDirectory() as tmp:
86
124
  service = MemoryService(
@@ -133,7 +171,18 @@ def main() -> int:
133
171
  if retrieval_metrics.get("precision@5", 0.0) < 0.4:
134
172
  return _fail(f"hybrid precision regression below threshold: {retrieval_metrics}")
135
173
 
136
- print(f"brain-quality-eval: OK {retrieval_metrics}")
174
+ with tempfile.TemporaryDirectory() as tmp:
175
+ corpus_metrics = _corpus_scale_retrieval_metrics(Path(tmp))
176
+ if corpus_metrics.get("recall@5", 0.0) < 0.80:
177
+ return _fail(f"corpus hybrid recall below threshold: {corpus_metrics}")
178
+ if corpus_metrics.get("precision@5", 0.0) < 0.25:
179
+ return _fail(f"corpus hybrid precision below threshold: {corpus_metrics}")
180
+ if corpus_metrics.get("ndcg@5", 0.0) < 0.70:
181
+ return _fail(f"corpus hybrid ndcg below threshold: {corpus_metrics}")
182
+ if corpus_metrics.get("must_include_hit_rate", 0.0) < 0.90:
183
+ return _fail(f"corpus must-include hit rate below threshold: {corpus_metrics}")
184
+
185
+ print(f"brain-quality-eval: OK small={retrieval_metrics} corpus={corpus_metrics}")
137
186
  return 0
138
187
 
139
188
 
@@ -1654,7 +1654,7 @@ dependencies = [
1654
1654
 
1655
1655
  [[package]]
1656
1656
  name = "lattice-ai-desktop"
1657
- version = "7.3.0"
1657
+ version = "7.4.0"
1658
1658
  dependencies = [
1659
1659
  "plist",
1660
1660
  "serde",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "7.3.0"
3
+ version = "7.4.0"
4
4
  description = "Lattice AI Digital Brain desktop shell"
5
5
  authors = ["TaeSoo Park"]
6
6
  edition = "2021"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "7.3.0",
4
+ "version": "7.4.0",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "7.3.0",
2
+ "version": "7.4.0",
3
3
  "generated_at": "vite",
4
4
  "entrypoints": {
5
5
  "app": "/static/app/index.html"