ltcai 7.3.0 → 7.5.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.
@@ -4,19 +4,158 @@ 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
10
30
 
11
31
  from dataclasses import dataclass, field
12
32
  from datetime import datetime
13
- from typing import Any, Dict, List, Optional
33
+ from typing import Any, Dict, Iterable, List, Optional
14
34
 
15
35
 
16
36
  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
+ CONTRACT_VIEW_KEYS = (
56
+ "family",
57
+ "schema_version",
58
+ "kind",
59
+ "id",
60
+ "run_id",
61
+ "agent_id",
62
+ "runtime",
63
+ "mode",
64
+ "status",
65
+ "goal",
66
+ "current_role",
67
+ "timestamp",
68
+ "is_terminal",
69
+ "blocking_reasons",
70
+ )
71
+
72
+ _SCHEMA_FOR_KIND = {
73
+ "agent_run": AGENT_RUN_SCHEMA,
74
+ "workflow_run": WORKFLOW_RUN_SCHEMA,
75
+ "audit_event": AUDIT_EVENT_SCHEMA,
76
+ "realtime_event": REALTIME_EVENT_SCHEMA,
77
+ }
78
+
79
+
80
+ def stamp_contract(
81
+ body: Dict[str, Any],
82
+ *,
83
+ kind: str,
84
+ identity: Optional[str],
85
+ status: str,
86
+ timestamp: Optional[str] = None,
87
+ ) -> Dict[str, Any]:
88
+ """Return ``body`` with the ``agent-run-contract/v1`` envelope merged in.
89
+
90
+ Additive and idempotent: existing keys in ``body`` win for everything except
91
+ the six envelope keys, which are authoritative. ``kind`` must be one of
92
+ :data:`CONTRACT_KINDS`; the schema version is derived from it so callers
93
+ cannot mismatch ``kind`` and ``schema_version``.
94
+ """
95
+ if kind not in _SCHEMA_FOR_KIND:
96
+ raise ValueError(f"unknown contract kind: {kind!r}")
97
+ enveloped = dict(body)
98
+ enveloped["family"] = CONTRACT_FAMILY
99
+ enveloped["schema_version"] = _SCHEMA_FOR_KIND[kind]
100
+ enveloped["kind"] = kind
101
+ enveloped["id"] = identity
102
+ enveloped["status"] = status
103
+ enveloped["timestamp"] = timestamp or body.get("timestamp") or runtime_timestamp()
104
+ return enveloped
105
+
106
+
107
+ def is_contract_member(record: Any) -> bool:
108
+ """True if ``record`` carries a valid ``agent-run-contract/v1`` envelope."""
109
+ if not isinstance(record, dict):
110
+ return False
111
+ if record.get("family") != CONTRACT_FAMILY:
112
+ return False
113
+ kind = record.get("kind")
114
+ if kind not in _SCHEMA_FOR_KIND:
115
+ return False
116
+ if record.get("schema_version") != _SCHEMA_FOR_KIND[kind]:
117
+ return False
118
+ return all(key in record for key in CONTRACT_ENVELOPE_KEYS)
119
+
120
+
121
+ def extract_contract(record: Any) -> Optional[Dict[str, Any]]:
122
+ """Return the normalized family contract carried by ``record``.
123
+
124
+ Consumers should call this instead of branching on whether they received an
125
+ agent run, workflow run, audit row, or realtime event. The function accepts
126
+ either a raw contract dict or a surface record with a nested ``contract``.
127
+ """
128
+ if is_contract_member(record):
129
+ return dict(record)
130
+ if isinstance(record, dict) and is_contract_member(record.get("contract")):
131
+ return dict(record["contract"])
132
+ return None
133
+
134
+
135
+ def require_contract(record: Any) -> Dict[str, Any]:
136
+ """Return a valid family contract or raise a precise error."""
137
+ contract = extract_contract(record)
138
+ if contract is None:
139
+ raise ValueError("record is missing an agent-run-contract/v1 family contract")
140
+ return contract
141
+
142
+
143
+ def contract_view(record: Any) -> Dict[str, Any]:
144
+ """Return a compact, surface-agnostic view for API and UI consumers."""
145
+ contract = require_contract(record)
146
+ return {key: contract.get(key) for key in CONTRACT_VIEW_KEYS if key in contract}
147
+
148
+
149
+ def contract_views(records: Iterable[Any]) -> List[Dict[str, Any]]:
150
+ """Return compact views for every valid contract in ``records``."""
151
+ views: List[Dict[str, Any]] = []
152
+ for record in records:
153
+ contract = extract_contract(record)
154
+ if contract is not None:
155
+ views.append({key: contract.get(key) for key in CONTRACT_VIEW_KEYS if key in contract})
156
+ return views
157
+
158
+
20
159
  @dataclass(frozen=True)
21
160
  class AgentRunContract:
22
161
  run_id: Optional[str]
@@ -38,13 +177,11 @@ class AgentRunContract:
38
177
  return self.status in {"ok", "retried_ok", "failed", "rejected", "cancelled", "interrupted", "partial", "done"}
39
178
 
40
179
  def as_dict(self) -> Dict[str, Any]:
41
- return {
42
- "schema_version": self.schema_version,
180
+ body = {
43
181
  "run_id": self.run_id,
44
182
  "agent_id": self.agent_id,
45
183
  "runtime": self.runtime,
46
184
  "mode": self.mode,
47
- "status": self.status,
48
185
  "goal": self.goal,
49
186
  "roles": list(self.roles),
50
187
  "current_role": self.current_role,
@@ -54,6 +191,12 @@ class AgentRunContract:
54
191
  "blocking_reasons": list(self.blocking_reasons),
55
192
  "is_terminal": self.is_terminal,
56
193
  }
194
+ return stamp_contract(
195
+ body,
196
+ kind="agent_run",
197
+ identity=self.run_id,
198
+ status=self.status,
199
+ )
57
200
 
58
201
 
59
202
  def multi_agent_contract(
@@ -80,6 +223,41 @@ def multi_agent_contract(
80
223
  ).as_dict()
81
224
 
82
225
 
226
+ def run_record_contract(run: Dict[str, Any], *, runtime: str = "multi_agent") -> Dict[str, Any]:
227
+ """Build the family contract from a persisted agent run row."""
228
+ run = dict(run or {})
229
+ timeline = list(run.get("timeline") or [])
230
+ retry_history = list(run.get("retry_history") or [])
231
+ roles = list(run.get("roles_run") or run.get("requested_roles") or [])
232
+ if not roles:
233
+ roles = [
234
+ str(item.get("role"))
235
+ for item in timeline
236
+ if isinstance(item, dict) and item.get("role")
237
+ ]
238
+ return AgentRunContract(
239
+ run_id=run.get("id") or run.get("run_id"),
240
+ agent_id=str(run.get("agent_id") or "agent:executor"),
241
+ runtime=runtime,
242
+ mode=str(run.get("mode") or "simulation"),
243
+ status=str(run.get("status") or "unknown"),
244
+ goal=str(run.get("input") or run.get("goal") or ""),
245
+ roles=roles,
246
+ current_role=run.get("current_role"),
247
+ retries=len(retry_history),
248
+ timeline=timeline,
249
+ artifacts=[
250
+ {
251
+ "type": "run_record",
252
+ "workspace_id": run.get("workspace_id"),
253
+ "graph_node_id": run.get("graph_node_id"),
254
+ "execution_mode": run.get("execution_mode"),
255
+ }
256
+ ],
257
+ blocking_reasons=[str(run.get("error"))] if run.get("error") else [],
258
+ ).as_dict()
259
+
260
+
83
261
  def single_agent_contract(
84
262
  *,
85
263
  ctx: Any,
@@ -105,3 +283,114 @@ def single_agent_contract(
105
283
  artifacts=[],
106
284
  blocking_reasons=list(blocking_reasons or []),
107
285
  ).as_dict()
286
+
287
+
288
+ # ── Sibling-surface contract builders ───────────────────────────────────────
289
+ # These wrap the other three observability surfaces into the same family so a
290
+ # consumer can iterate agent runs, workflow runs, audit events and realtime
291
+ # events through one envelope.
292
+
293
+ def workflow_run_contract(run: Any) -> Dict[str, Any]:
294
+ """Wrap a workflow run dict (``WorkflowRun.as_dict()``) in the family envelope.
295
+
296
+ Accepts either a ``WorkflowRun`` instance or its serialized dict so the
297
+ engine can stamp without importing the dataclass here (avoids a cycle).
298
+ """
299
+ body = run.as_dict() if hasattr(run, "as_dict") else dict(run or {})
300
+ run_id = body.get("id") or body.get("run_id") or body.get("workflow_id")
301
+ workflow_id = body.get("workflow_id")
302
+ contract_body = {
303
+ "run_id": run_id,
304
+ "agent_id": f"workflow:{workflow_id or body.get('name') or 'workflow'}",
305
+ "runtime": "workflow",
306
+ "mode": body.get("mode") or "live",
307
+ "goal": body.get("name") or "workflow",
308
+ "roles": ["workflow"],
309
+ "current_role": body.get("current_node") or body.get("paused_node"),
310
+ "retries": 0,
311
+ "timeline": list(body.get("timeline") or []),
312
+ "artifacts": [
313
+ {
314
+ "type": "workflow_outputs",
315
+ "workflow_id": workflow_id,
316
+ "outputs": body.get("outputs") or {},
317
+ "pause": body.get("pause") or body.get("pending_approval"),
318
+ }
319
+ ],
320
+ "blocking_reasons": [str((body.get("outputs") or {}).get("error"))] if (body.get("outputs") or {}).get("error") else [],
321
+ "is_terminal": str(body.get("status") or "") in {"ok", "failed", "cancelled", "interrupted", "partial", "rejected"},
322
+ }
323
+ return stamp_contract(
324
+ contract_body,
325
+ kind="workflow_run",
326
+ identity=run_id,
327
+ status=str(body.get("status") or "unknown"),
328
+ timestamp=body.get("started_at") or body.get("created_at"),
329
+ )
330
+
331
+
332
+ def audit_event_contract(event: Dict[str, Any]) -> Dict[str, Any]:
333
+ """Wrap a single audit-log event in the family envelope.
334
+
335
+ Audit events have no lifecycle ``status``; we expose the event type as the
336
+ status so the envelope stays uniform and filterable. Identity is a stable
337
+ ``event_type@timestamp`` key (audit events are append-only, not addressable).
338
+ """
339
+ body = dict(event or {})
340
+ event_type = str(body.get("event_type") or "event")
341
+ ts = body.get("timestamp")
342
+ identity = body.get("event_id") or (f"{event_type}@{ts}" if ts else event_type)
343
+ contract_body = {
344
+ "run_id": body.get("run_id"),
345
+ "agent_id": str(body.get("agent_id") or body.get("workflow_id") or f"audit:{event_type}"),
346
+ "runtime": "audit",
347
+ "mode": "event",
348
+ "goal": event_type,
349
+ "roles": [],
350
+ "current_role": None,
351
+ "retries": int(body.get("retries") or 0),
352
+ "timeline": [{"event": event_type, "timestamp": ts, "status": body.get("status") or event_type}],
353
+ "artifacts": [{"type": "audit_payload", "payload": body}],
354
+ "blocking_reasons": [],
355
+ "is_terminal": True,
356
+ }
357
+ return stamp_contract(
358
+ contract_body,
359
+ kind="audit_event",
360
+ identity=identity,
361
+ status=event_type,
362
+ timestamp=ts,
363
+ )
364
+
365
+
366
+ def realtime_event_contract(event: Dict[str, Any]) -> Dict[str, Any]:
367
+ """Wrap a realtime bus event in the family envelope.
368
+
369
+ The bus already assigns a monotonic ``seq`` and ``received_at``; we reuse
370
+ those as identity and timestamp so the envelope is consistent with the
371
+ feed's own ordering guarantees.
372
+ """
373
+ body = dict(event or {})
374
+ seq = body.get("seq")
375
+ payload = body.get("payload") if isinstance(body.get("payload"), dict) else {}
376
+ contract_body = {
377
+ "run_id": payload.get("run_id") or body.get("run_id"),
378
+ "agent_id": str(payload.get("agent_id") or payload.get("workflow_id") or f"realtime:{body.get('area') or 'workspace'}"),
379
+ "runtime": "realtime",
380
+ "mode": "event",
381
+ "goal": str(body.get("event_type") or "event"),
382
+ "roles": [],
383
+ "current_role": payload.get("current_role"),
384
+ "retries": int(payload.get("retries") or 0),
385
+ "timeline": [{"event": body.get("event_type") or "event", "timestamp": body.get("received_at"), "payload": payload}],
386
+ "artifacts": [{"type": "realtime_payload", "payload": payload}],
387
+ "blocking_reasons": [str(payload.get("error"))] if payload.get("error") else [],
388
+ "is_terminal": str(payload.get("status") or "") in {"ok", "failed", "cancelled", "interrupted", "partial", "rejected"},
389
+ }
390
+ return stamp_contract(
391
+ contract_body,
392
+ kind="realtime_event",
393
+ identity=f"rt:{seq}" if seq is not None else None,
394
+ status=str(body.get("event_type") or "event"),
395
+ timestamp=body.get("received_at"),
396
+ )
@@ -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.5.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.5.0"
@@ -111,7 +111,7 @@ def create_agents_router(
111
111
  async def agent_runs(request: Request):
112
112
  require_user(request)
113
113
  scope = gate_read(request)
114
- return store.list_agents(workspace_id=scope)
114
+ return runtime.list_runs(scope=scope)
115
115
 
116
116
  @router.get("/agents/api/handoffs")
117
117
  async def agent_handoffs(request: Request, run_id: str = ""):
@@ -124,7 +124,7 @@ def create_agents_router(
124
124
  require_user(request)
125
125
  scope = gate_read(request)
126
126
  try:
127
- return {"run": store.get_agent_run(run_id, workspace_id=scope)}
127
+ return runtime.get_run(run_id, scope=scope)
128
128
  except FileNotFoundError as exc:
129
129
  raise HTTPException(status_code=404, detail=f"Agent run not found: {run_id}") from exc
130
130
 
@@ -133,7 +133,7 @@ def create_agents_router(
133
133
  require_user(request)
134
134
  scope = gate_read(request)
135
135
  try:
136
- return {"replay": store.replay_agent_run(run_id, workspace_id=scope)}
136
+ return runtime.replay(run_id, scope=scope)
137
137
  except FileNotFoundError as exc:
138
138
  raise HTTPException(status_code=404, detail=f"Agent run not found: {run_id}") from exc
139
139
 
@@ -17,6 +17,7 @@ from fastapi.responses import StreamingResponse
17
17
  from pydantic import BaseModel
18
18
 
19
19
  from latticeai.api.ui_redirects import app_redirect
20
+ from lattice_brain.runtime.contracts import contract_views
20
21
 
21
22
 
22
23
  class PresenceRequest(BaseModel):
@@ -63,7 +64,8 @@ def create_realtime_router(
63
64
  async def realtime_feed(request: Request, limit: int = 50):
64
65
  user = require_user(request)
65
66
  scope = allowed_scopes(user or None)
66
- return {"events": bus.recent(limit=limit, workspace_scope=scope), "stats": bus.stats()}
67
+ events = bus.recent(limit=limit, workspace_scope=scope)
68
+ return {"events": events, "contracts": contract_views(events), "stats": bus.stats()}
67
69
 
68
70
  @router.get("/realtime/presence")
69
71
  async def realtime_presence(request: Request):
@@ -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.5.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.5.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.5.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": {
@@ -115,8 +115,8 @@
115
115
  "@playwright/test": "^1.60.0",
116
116
  "@tailwindcss/postcss": "^4.3.0",
117
117
  "@tanstack/react-query": "^5.101.0",
118
- "@tauri-apps/api": "^2.0.0",
119
- "@tauri-apps/cli": "^2.0.0",
118
+ "@tauri-apps/api": "^2.11.1",
119
+ "@tauri-apps/cli": "^2.11.3",
120
120
  "@types/cytoscape": "^3.21.9",
121
121
  "@types/react": "^19.2.17",
122
122
  "@types/react-dom": "^19.2.3",
@@ -138,5 +138,8 @@
138
138
  "typescript": "^5.9.3",
139
139
  "vite": "^8.0.16",
140
140
  "zustand": "^5.0.14"
141
+ },
142
+ "overrides": {
143
+ "js-yaml": "^4.2.0"
141
144
  }
142
145
  }