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 +24 -22
- package/docs/CHANGELOG.md +24 -0
- package/docs/ROADMAP_RECOMMENDATIONS.md +17 -5
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/quality.py +31 -4
- package/lattice_brain/retrieval_benchmark_fixtures.py +56 -0
- package/lattice_brain/runtime/contracts.py +238 -3
- package/lattice_brain/runtime/multi_agent.py +5 -2
- package/lattice_brain/workflow.py +11 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/core/audit.py +15 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/realtime.py +3 -0
- package/latticeai/core/workspace_os.py +34 -1
- package/package.json +1 -1
- package/scripts/brain_quality_eval.py +50 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +1 -1
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.
|
|
204
|
-
|
|
205
|
-
-
|
|
206
|
-
`agent-run-contract/v1`
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
- `dist/ltcai-7.
|
|
221
|
-
- `ltcai-7.
|
|
222
|
-
- `
|
|
223
|
-
- `
|
|
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
|
|
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
|
-
-
|
|
23
|
-
-
|
|
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
|
-
-
|
|
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
|
|
package/lattice_brain/quality.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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):
|
package/latticeai/__init__.py
CHANGED
package/latticeai/core/audit.py
CHANGED
|
@@ -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":
|
|
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",
|
|
@@ -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
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED