ltcai 9.9.1 → 9.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +40 -39
  2. package/docs/CHANGELOG.md +80 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/ONBOARDING.md +1 -1
  6. package/docs/OPERATIONS.md +1 -1
  7. package/docs/TRUST_MODEL.md +1 -1
  8. package/docs/WHY_LATTICE.md +1 -1
  9. package/docs/kg-schema.md +1 -1
  10. package/lattice_brain/__init__.py +1 -1
  11. package/lattice_brain/graph/curator.py +141 -0
  12. package/lattice_brain/graph/documents.py +111 -0
  13. package/lattice_brain/graph/fusion.py +179 -0
  14. package/lattice_brain/graph/projection.py +161 -0
  15. package/lattice_brain/graph/retrieval.py +19 -1
  16. package/lattice_brain/ingestion.py +66 -0
  17. package/lattice_brain/runtime/multi_agent.py +1 -1
  18. package/latticeai/__init__.py +1 -1
  19. package/latticeai/api/automation_intelligence.py +159 -0
  20. package/latticeai/api/browser.py +15 -4
  21. package/latticeai/api/chat.py +30 -2
  22. package/latticeai/api/chat_agent_http.py +193 -4
  23. package/latticeai/api/chat_contracts.py +7 -1
  24. package/latticeai/api/chat_helpers.py +147 -0
  25. package/latticeai/api/chat_intents.py +319 -8
  26. package/latticeai/api/chat_stream.py +17 -1
  27. package/latticeai/api/funnel_metrics.py +31 -0
  28. package/latticeai/api/knowledge_graph.py +30 -0
  29. package/latticeai/api/local_files.py +90 -0
  30. package/latticeai/api/setup.py +134 -1
  31. package/latticeai/api/tools.py +33 -3
  32. package/latticeai/app_factory.py +15 -0
  33. package/latticeai/core/agent.py +205 -11
  34. package/latticeai/core/agent_eval.py +107 -0
  35. package/latticeai/core/file_generation.py +337 -2
  36. package/latticeai/core/legacy_compatibility.py +1 -1
  37. package/latticeai/core/marketplace.py +1 -1
  38. package/latticeai/core/workspace_os.py +1 -1
  39. package/latticeai/runtime/persistence_runtime.py +19 -1
  40. package/latticeai/runtime/router_registration.py +8 -1
  41. package/latticeai/services/app_context.py +6 -0
  42. package/latticeai/services/architecture_readiness.py +1 -1
  43. package/latticeai/services/automation_execution.py +266 -0
  44. package/latticeai/services/automation_intelligence.py +7 -0
  45. package/latticeai/services/command_center.py +20 -1
  46. package/latticeai/services/folder_watch.py +391 -0
  47. package/latticeai/services/funnel_metrics.py +202 -0
  48. package/latticeai/services/product_readiness.py +6 -1
  49. package/latticeai/services/search_service.py +17 -1
  50. package/latticeai/services/tool_dispatch.py +36 -0
  51. package/latticeai/setup/demo_corpus.py +98 -0
  52. package/latticeai/tools/filesystem.py +38 -1
  53. package/package.json +1 -1
  54. package/scripts/bench_models.py +234 -5
  55. package/scripts/check_current_release_docs.mjs +1 -1
  56. package/src-tauri/Cargo.lock +1 -1
  57. package/src-tauri/Cargo.toml +1 -1
  58. package/src-tauri/tauri.conf.json +1 -1
  59. package/static/app/asset-manifest.json +28 -27
  60. package/static/app/assets/Act-DmdruVKV.js +1 -0
  61. package/static/app/assets/{Brain-CSCDZEHd.js → Brain-DEY9jLVt.js} +2 -2
  62. package/static/app/assets/BrainHome-CeNaxjP1.js +2 -0
  63. package/static/app/assets/BrainSignals-CStjIqYi.js +1 -0
  64. package/static/app/assets/{Capture-BiuerOio.js → Capture-CVZ09QXi.js} +1 -1
  65. package/static/app/assets/CommandPalette-DepwOQFv.js +1 -0
  66. package/static/app/assets/Library-Bp0n-HlW.js +1 -0
  67. package/static/app/assets/{LivingBrain-6FvA0H5T.js → LivingBrain-DxP4efJF.js} +1 -1
  68. package/static/app/assets/{ProductFlow-Cr8bkQPw.js → ProductFlow-DRbm7NEq.js} +1 -1
  69. package/static/app/assets/ReviewCard-C4HAO7A3.js +3 -0
  70. package/static/app/assets/{System-D6uJacqu.js → System-ByQcmJW-.js} +1 -1
  71. package/static/app/assets/{bot-DgC7xgUl.js → bot-BNDyZLR7.js} +1 -1
  72. package/static/app/assets/circle-play-BkhdcHgd.js +1 -0
  73. package/static/app/assets/{cpu-DDGb4XHl.js → cpu-C6jjYm6i.js} +1 -1
  74. package/static/app/assets/{folder-open-BbVuSO9M.js → folder-open-DjGIvDBQ.js} +1 -1
  75. package/static/app/assets/{hard-drive-yDZR6XYz.js → hard-drive-BlSbwSaT.js} +1 -1
  76. package/static/app/assets/index-Bge3DXW7.css +2 -0
  77. package/static/app/assets/{index-DrZSkwrE.js → index-CHu7cgj3.js} +3 -3
  78. package/static/app/assets/{input-DPIkTs9n.js → input-DVDI0YR3.js} +1 -1
  79. package/static/app/assets/{navigation-B1mkmyUe.js → navigation-BddhEWA0.js} +1 -1
  80. package/static/app/assets/{network-0XDI1Uf_.js → network-pYQt5oBu.js} +1 -1
  81. package/static/app/assets/{primitives-sIQYKyf5.js → primitives-D7gCdEvS.js} +1 -1
  82. package/static/app/assets/shield-alert-K9RKGQeg.js +1 -0
  83. package/static/app/assets/{textarea-DvnwpnqS.js → textarea-sqQmoBKL.js} +1 -1
  84. package/static/app/assets/useFocusTrap-7EV9dFP2.js +1 -0
  85. package/static/app/assets/utils-uQYKXNeq.js +7 -0
  86. package/static/app/index.html +4 -4
  87. package/static/sw.js +1 -1
  88. package/static/app/assets/Act-CcSWTQsx.js +0 -1
  89. package/static/app/assets/BrainHome-B5QpJ1Fx.js +0 -2
  90. package/static/app/assets/BrainSignals-CmFD8PWk.js +0 -1
  91. package/static/app/assets/CommandPalette-BrqpT7K2.js +0 -1
  92. package/static/app/assets/Library-BcaiPbum.js +0 -1
  93. package/static/app/assets/ReviewCard-D56TMypk.js +0 -1
  94. package/static/app/assets/download-C-Ei3GFa.js +0 -1
  95. package/static/app/assets/index-CFRhNBuH.css +0 -2
  96. package/static/app/assets/sunrise-DXMjueec.js +0 -1
  97. package/static/app/assets/utils-BrfFHAJl.js +0 -7
@@ -34,11 +34,17 @@ class AppContext:
34
34
  chat_service: Any = None
35
35
  context_assembler: Any = None
36
36
  brain_memory: Any = None
37
+ # Unified ingestion gateway (lattice_brain.ingestion.IngestionPipeline);
38
+ # None when the knowledge graph is disabled.
39
+ ingestion_pipeline: Any = None
37
40
  chat_agent_runtime: Any = None
38
41
  gardener: Any = None
39
42
  hooks: Any = None
40
43
  realtime_bus: Any = None
41
44
  capability_registry: Any = None
45
+ # UX funnel metrics (latticeai.services.funnel_metrics); None in tests
46
+ # that don't observe the funnel — every increment site is nil-safe.
47
+ funnel_metrics: Any = None
42
48
 
43
49
  # ── auth / session callables ──────────────────────────────────────────
44
50
  require_user: Optional[Callable[..., str]] = None
@@ -17,7 +17,7 @@ from typing import Any, Dict, List
17
17
  from latticeai.core.legacy_compatibility import legacy_shim_report
18
18
 
19
19
 
20
- ARCHITECTURE_VERSION_TARGET = "9.9.1"
20
+ ARCHITECTURE_VERSION_TARGET = "9.9.3"
21
21
 
22
22
  PREFERRED_REFACTORING_ORDER = [
23
23
  "agent-runtime",
@@ -0,0 +1,266 @@
1
+ """Automation "run now" support: dry-run reports + execution log surfacing.
2
+
3
+ Backlog #6 (review §7.2 F, Wave 2): after installing an automation the user
4
+ should be able to run it once immediately — first as a **dry run** that
5
+ reports exactly what WOULD happen without side effects, then for real — and
6
+ every automation card should show its last execution result.
7
+
8
+ This module owns the deterministic policy pieces:
9
+
10
+ * :func:`dry_run_report` — walks the workflow's node graph (same normalizer
11
+ as the engine) and describes each step without invoking any runner. No
12
+ model call, no store write, no side effects.
13
+ * :func:`build_last_execution` / :func:`summarize_workflow_run` — the compact
14
+ ``last_execution`` record persisted on the workflow's metadata and surfaced
15
+ by the automation overview + daily briefing.
16
+ * :func:`last_execution_view` — read-side merge: the stamped metadata record
17
+ vs. the newest persisted workflow run (so trigger-driven runs surface too).
18
+ * :func:`enqueue_failed_execution` — failed live executions become a review
19
+ queue item (source ``workflow_run``, kind ``automation_failure``) so
20
+ failures land in the same inbox as every other automation outcome.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ from typing import Any, Dict, List, Optional
27
+
28
+ from latticeai.core.timeutil import now_iso as _now
29
+
30
+ LOGGER = logging.getLogger(__name__)
31
+
32
+ AUTOMATION_CREATED_FROM = frozenset({
33
+ "automation_suggestion",
34
+ "brain_automation_recipe",
35
+ })
36
+
37
+ _TRIGGER_LABEL = {
38
+ "interval": "wait for the user-enabled schedule",
39
+ "brain_event": "wait for new Brain knowledge",
40
+ "manual": "start when the user asks",
41
+ }
42
+
43
+
44
+ def is_automation_workflow(workflow: Dict[str, Any]) -> bool:
45
+ metadata = (workflow or {}).get("metadata") or {}
46
+ return metadata.get("created_from") in AUTOMATION_CREATED_FROM
47
+
48
+
49
+ def dry_run_report(workflow: Dict[str, Any]) -> Dict[str, Any]:
50
+ """Describe what a run WOULD do — deterministically, with no side effects.
51
+
52
+ Walks the normalized node chain (``trigger`` first, then ``next``
53
+ pointers) and emits one human-readable step per node. Nothing is
54
+ executed: no runner, no model, no store write.
55
+ """
56
+ from lattice_brain.workflow import normalize_definition, validate_definition
57
+
58
+ definition = normalize_definition(workflow or {})
59
+ errors = validate_definition(definition)
60
+ metadata = definition.get("metadata") or {}
61
+ steps: List[Dict[str, Any]] = []
62
+
63
+ nodes = {node.get("id"): node for node in definition.get("nodes") or []}
64
+ current = next(
65
+ (node for node in definition.get("nodes") or [] if node.get("type") == "trigger"),
66
+ None,
67
+ )
68
+ visited: set = set()
69
+ while current is not None and current.get("id") not in visited:
70
+ visited.add(current.get("id"))
71
+ ntype = str(current.get("type") or "")
72
+ config = current.get("config") or {}
73
+ if ntype == "trigger":
74
+ would = _TRIGGER_LABEL.get(
75
+ str(config.get("trigger") or "manual"),
76
+ "start when triggered",
77
+ )
78
+ would = f"skipped in a manual run ({would})"
79
+ elif ntype == "agent":
80
+ prompt = str(config.get("prompt") or config.get("goal") or "")[:160]
81
+ would = f"ask the local agent to draft: {prompt}" if prompt else (
82
+ "ask the local agent to draft a review item"
83
+ )
84
+ elif ntype == "tool":
85
+ would = f"run tool '{config.get('tool') or current.get('name') or 'tool'}'"
86
+ elif ntype == "skill":
87
+ would = f"run skill '{config.get('skill') or current.get('name') or 'skill'}'"
88
+ elif ntype == "plugin":
89
+ would = f"run plugin '{config.get('plugin') or current.get('name') or 'plugin'}'"
90
+ elif ntype == "condition":
91
+ would = "evaluate a branch condition"
92
+ elif ntype == "output":
93
+ would = "deliver the draft to the review inbox"
94
+ else:
95
+ would = f"run node '{current.get('id')}'"
96
+ steps.append({
97
+ "node": current.get("id"),
98
+ "type": ntype,
99
+ "name": current.get("name") or current.get("id"),
100
+ "would": would,
101
+ })
102
+ next_id = current.get("next")
103
+ current = nodes.get(next_id) if next_id else None
104
+
105
+ executable = [s for s in steps if s["type"] not in {"trigger", "output", "condition"}]
106
+ creates = list(metadata.get("creates") or [])
107
+ if errors:
108
+ summary = "invalid workflow: " + "; ".join(errors[:3])
109
+ status = "invalid"
110
+ else:
111
+ summary = (
112
+ f"{len(executable)} step(s) would run locally and produce a reviewable draft"
113
+ )
114
+ if creates:
115
+ summary += " (" + ", ".join(str(c) for c in creates[:3]) + ")"
116
+ summary += "; no external actions, nothing is written until you approve."
117
+ status = "ok"
118
+ return {
119
+ "mode": "dry_run",
120
+ "status": status,
121
+ "steps": steps,
122
+ "summary": summary,
123
+ "side_effects": False,
124
+ "validation_errors": errors,
125
+ }
126
+
127
+
128
+ def build_last_execution(
129
+ *,
130
+ mode: str,
131
+ status: str,
132
+ summary: str,
133
+ run_id: Optional[str] = None,
134
+ ) -> Dict[str, Any]:
135
+ """The compact per-automation execution record (persisted on metadata)."""
136
+ return {
137
+ "mode": mode, # dry_run | live
138
+ "status": status, # ok | failed | running | partial | ...
139
+ "summary": str(summary or "")[:300],
140
+ "run_id": run_id,
141
+ "finished_at": _now(),
142
+ }
143
+
144
+
145
+ def summarize_workflow_run(run: Dict[str, Any]) -> str:
146
+ """One honest line for a persisted workflow run."""
147
+ status = str(run.get("status") or "unknown")
148
+ timeline = run.get("timeline") or []
149
+ if status == "failed":
150
+ detail = ""
151
+ for entry in reversed(timeline):
152
+ if entry.get("status") in {"failed", "error"}:
153
+ detail = str(
154
+ entry.get("reason") or entry.get("detail") or entry.get("errors") or ""
155
+ )[:160]
156
+ break
157
+ if not detail:
158
+ detail = str((run.get("outputs") or {}).get("error") or "")[:160]
159
+ return f"failed after {len(timeline)} step(s)" + (f": {detail}" if detail else "")
160
+ if status == "awaiting_approval":
161
+ return "paused — a step needs your approval before it can run"
162
+ if status in {"queued", "running", "cancelling"}:
163
+ return "still running"
164
+ return f"{status} — {len(timeline)} step(s) recorded"
165
+
166
+
167
+ def last_execution_view(
168
+ workflow: Dict[str, Any],
169
+ *,
170
+ store: Any = None,
171
+ workspace_id: Optional[str] = None,
172
+ ) -> Optional[Dict[str, Any]]:
173
+ """Read-side ``last_execution``: stamped metadata vs. newest persisted run.
174
+
175
+ Run-now stamps ``metadata.last_execution`` directly; trigger/background
176
+ runs only leave workflow-run rows. Whichever is newer wins so the card is
177
+ honest about the latest execution regardless of how it was started.
178
+ """
179
+ metadata = (workflow or {}).get("metadata") or {}
180
+ stamped = metadata.get("last_execution")
181
+ stamped = dict(stamped) if isinstance(stamped, dict) else None
182
+
183
+ latest_run: Optional[Dict[str, Any]] = None
184
+ if store is not None and hasattr(store, "list_workflow_runs"):
185
+ try:
186
+ runs = (
187
+ store.list_workflow_runs(
188
+ workflow_id=workflow.get("id"), limit=1, workspace_id=workspace_id
189
+ )
190
+ or {}
191
+ ).get("runs") or []
192
+ latest_run = runs[0] if runs else None
193
+ except Exception: # noqa: BLE001 — surfacing must never break the overview
194
+ LOGGER.exception("automation last-execution run read failed")
195
+ latest_run = None
196
+
197
+ derived: Optional[Dict[str, Any]] = None
198
+ if latest_run is not None:
199
+ derived = {
200
+ "mode": "live",
201
+ "status": str(latest_run.get("status") or "unknown"),
202
+ "summary": summarize_workflow_run(latest_run),
203
+ "run_id": latest_run.get("id"),
204
+ "finished_at": str(
205
+ latest_run.get("completed_at")
206
+ or latest_run.get("updated_at")
207
+ or latest_run.get("created_at")
208
+ or ""
209
+ ),
210
+ }
211
+
212
+ if stamped is None:
213
+ return derived
214
+ if derived is None:
215
+ return stamped
216
+ return derived if str(derived.get("finished_at") or "") > str(
217
+ stamped.get("finished_at") or ""
218
+ ) else stamped
219
+
220
+
221
+ def enqueue_failed_execution(
222
+ review_queue: Any,
223
+ *,
224
+ workflow: Dict[str, Any],
225
+ run_id: Optional[str],
226
+ error: str,
227
+ user_email: Optional[str] = None,
228
+ workspace_id: Optional[str] = None,
229
+ ) -> Optional[Dict[str, Any]]:
230
+ """Failed live execution → review queue item (never raises)."""
231
+ if review_queue is None:
232
+ return None
233
+ name = str(workflow.get("name") or "Automation")
234
+ try:
235
+ return review_queue.create(
236
+ title=f"Automation failed: {name}"[:160],
237
+ summary=str(error or "")[:500],
238
+ source="workflow_run",
239
+ kind="automation_failure",
240
+ payload={
241
+ "workflow_id": workflow.get("id"),
242
+ "run_id": run_id,
243
+ "error": str(error or "")[:500],
244
+ },
245
+ provenance={
246
+ "workflow_id": workflow.get("id"),
247
+ "run_id": run_id,
248
+ "origin": "automation_run_now",
249
+ },
250
+ user_email=user_email,
251
+ workspace_id=workspace_id,
252
+ )
253
+ except Exception: # noqa: BLE001 — review enqueue is best-effort surfacing
254
+ LOGGER.exception("automation failure review enqueue failed")
255
+ return None
256
+
257
+
258
+ __all__ = [
259
+ "AUTOMATION_CREATED_FROM",
260
+ "is_automation_workflow",
261
+ "dry_run_report",
262
+ "build_last_execution",
263
+ "summarize_workflow_run",
264
+ "last_execution_view",
265
+ "enqueue_failed_execution",
266
+ ]
@@ -560,6 +560,8 @@ class AutomationIntelligenceService:
560
560
  except Exception:
561
561
  LOGGER.exception("automation intelligence workflow read failed")
562
562
  workflows = []
563
+ from latticeai.services.automation_execution import last_execution_view
564
+
563
565
  for workflow in workflows:
564
566
  metadata = (workflow or {}).get("metadata") or {}
565
567
  if metadata.get("created_from") not in {"automation_suggestion", "brain_automation_recipe"}:
@@ -573,6 +575,11 @@ class AutomationIntelligenceService:
573
575
  "enabled": metadata.get("automation_state") == "enabled",
574
576
  "requires_user_enable": bool(metadata.get("requires_user_enable", True)),
575
577
  "creates": metadata.get("creates") or [],
578
+ # Execution log surfacing (backlog #6): the newest of the
579
+ # run-now stamp vs. the latest persisted workflow run.
580
+ "last_execution": last_execution_view(
581
+ workflow, store=self._store, workspace_id=workspace_id
582
+ ),
576
583
  })
577
584
  return {
578
585
  "suggestions": suggestion_report["suggestions"],
@@ -147,18 +147,37 @@ class CommandCenterService:
147
147
  workflows = self._workflows(workspace_id=workspace_id)
148
148
  enabled = 0
149
149
  drafts = 0
150
+ last_execution: Optional[Dict[str, Any]] = None
150
151
  for workflow in workflows:
151
152
  metadata = (workflow or {}).get("metadata") or {}
152
153
  if metadata.get("automation_state") == "enabled":
153
154
  enabled += 1
154
155
  elif metadata.get("automation_state") == "draft_disabled":
155
156
  drafts += 1
156
- return {
157
+ # Execution log surfacing (backlog #6): the briefing carries the
158
+ # most recent stamped automation execution so "did my automation
159
+ # run, and how did it go?" is answered on the home surface.
160
+ stamped = metadata.get("last_execution")
161
+ if isinstance(stamped, dict) and str(stamped.get("finished_at") or "") > str(
162
+ (last_execution or {}).get("finished_at") or ""
163
+ ):
164
+ last_execution = {
165
+ "workflow_id": workflow.get("id"),
166
+ "name": _clip(workflow.get("name"), 120),
167
+ "mode": stamped.get("mode"),
168
+ "status": stamped.get("status"),
169
+ "summary": _clip(stamped.get("summary"), 200),
170
+ "finished_at": stamped.get("finished_at"),
171
+ }
172
+ section = {
157
173
  "available": True,
158
174
  "total": len(workflows),
159
175
  "enabled": enabled,
160
176
  "drafts": drafts,
161
177
  }
178
+ if last_execution is not None:
179
+ section["last_execution"] = last_execution
180
+ return section
162
181
 
163
182
  def _review_section(
164
183
  self, *, user_email: Optional[str], workspace_id: Optional[str]