ltcai 4.3.1 → 4.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.
Files changed (131) hide show
  1. package/README.md +191 -278
  2. package/docs/CHANGELOG.md +128 -0
  3. package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +174 -0
  4. package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +81 -0
  5. package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +75 -0
  6. package/docs/V4_3_2_GRAPH_UX_REPORT.md +48 -0
  7. package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +209 -0
  8. package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +57 -0
  9. package/docs/V4_3_2_SELF_AUDIT_REPORT.md +63 -0
  10. package/docs/V4_3_2_VALIDATION_REPORT.md +97 -0
  11. package/docs/V4_3_3_VALIDATION_REPORT.md +46 -0
  12. package/docs/V4_4_0_EXTRACTION_REPORT.md +239 -0
  13. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -19
  14. package/frontend/openapi.json +1 -1
  15. package/frontend/src/components/primitives.tsx +92 -10
  16. package/frontend/src/pages/Act.tsx +11 -9
  17. package/frontend/src/pages/Ask.tsx +2 -2
  18. package/frontend/src/pages/Brain.tsx +607 -65
  19. package/frontend/src/pages/Capture.tsx +11 -7
  20. package/frontend/src/pages/Library.tsx +3 -3
  21. package/frontend/src/pages/System.tsx +186 -23
  22. package/lattice_brain/__init__.py +38 -23
  23. package/lattice_brain/_kg_common.py +11 -1
  24. package/lattice_brain/context.py +212 -2
  25. package/lattice_brain/conversations.py +234 -1
  26. package/lattice_brain/discovery.py +11 -1
  27. package/lattice_brain/documents.py +11 -1
  28. package/lattice_brain/graph/__init__.py +28 -0
  29. package/lattice_brain/graph/_kg_common.py +1123 -0
  30. package/lattice_brain/graph/curator.py +473 -0
  31. package/lattice_brain/graph/discovery.py +1455 -0
  32. package/lattice_brain/graph/documents.py +218 -0
  33. package/lattice_brain/graph/identity.py +175 -0
  34. package/lattice_brain/graph/ingest.py +644 -0
  35. package/lattice_brain/graph/network.py +205 -0
  36. package/lattice_brain/graph/projection.py +571 -0
  37. package/lattice_brain/graph/provenance.py +401 -0
  38. package/lattice_brain/graph/retrieval.py +1341 -0
  39. package/lattice_brain/graph/schema.py +640 -0
  40. package/lattice_brain/graph/store.py +237 -0
  41. package/lattice_brain/graph/write_master.py +225 -0
  42. package/lattice_brain/identity.py +11 -13
  43. package/lattice_brain/ingest.py +11 -1
  44. package/lattice_brain/ingestion.py +318 -0
  45. package/lattice_brain/memory.py +100 -1
  46. package/lattice_brain/network.py +11 -1
  47. package/lattice_brain/portability.py +431 -0
  48. package/lattice_brain/projection.py +11 -1
  49. package/lattice_brain/provenance.py +11 -1
  50. package/lattice_brain/retrieval.py +11 -1
  51. package/lattice_brain/runtime/__init__.py +32 -0
  52. package/lattice_brain/runtime/agent_runtime.py +569 -0
  53. package/lattice_brain/runtime/hooks.py +754 -0
  54. package/lattice_brain/runtime/multi_agent.py +795 -0
  55. package/lattice_brain/schema.py +11 -1
  56. package/lattice_brain/store.py +10 -2
  57. package/lattice_brain/workflow.py +461 -0
  58. package/lattice_brain/write_master.py +11 -1
  59. package/latticeai/__init__.py +1 -1
  60. package/latticeai/api/agents.py +2 -2
  61. package/latticeai/api/browser.py +1 -1
  62. package/latticeai/api/chat.py +1 -1
  63. package/latticeai/api/computer_use.py +1 -1
  64. package/latticeai/api/hooks.py +2 -2
  65. package/latticeai/api/mcp.py +1 -1
  66. package/latticeai/api/tools.py +1 -1
  67. package/latticeai/api/workflow_designer.py +2 -2
  68. package/latticeai/app_factory.py +4 -4
  69. package/latticeai/brain/__init__.py +24 -6
  70. package/latticeai/brain/_kg_common.py +11 -1117
  71. package/latticeai/brain/context.py +12 -208
  72. package/latticeai/brain/conversations.py +12 -231
  73. package/latticeai/brain/discovery.py +13 -1451
  74. package/latticeai/brain/documents.py +13 -214
  75. package/latticeai/brain/identity.py +11 -169
  76. package/latticeai/brain/ingest.py +13 -640
  77. package/latticeai/brain/memory.py +12 -97
  78. package/latticeai/brain/network.py +12 -200
  79. package/latticeai/brain/projection.py +13 -567
  80. package/latticeai/brain/provenance.py +13 -397
  81. package/latticeai/brain/retrieval.py +13 -1337
  82. package/latticeai/brain/schema.py +12 -635
  83. package/latticeai/brain/store.py +13 -233
  84. package/latticeai/brain/write_master.py +13 -221
  85. package/latticeai/core/agent.py +1 -1
  86. package/latticeai/core/agent_registry.py +2 -2
  87. package/latticeai/core/builtin_hooks.py +2 -2
  88. package/latticeai/core/graph_curator.py +6 -468
  89. package/latticeai/core/hooks.py +6 -749
  90. package/latticeai/core/marketplace.py +1 -1
  91. package/latticeai/core/multi_agent.py +6 -790
  92. package/latticeai/core/workflow_engine.py +6 -456
  93. package/latticeai/core/workspace_os.py +1 -1
  94. package/latticeai/services/agent_runtime.py +6 -564
  95. package/latticeai/services/ingestion.py +6 -313
  96. package/latticeai/services/kg_portability.py +6 -426
  97. package/latticeai/services/platform_runtime.py +3 -3
  98. package/latticeai/services/run_executor.py +1 -1
  99. package/latticeai/services/upload_service.py +1 -1
  100. package/p_reinforce.py +1 -1
  101. package/package.json +3 -6
  102. package/scripts/build_vercel_static.mjs +77 -0
  103. package/scripts/bump_version.py +1 -1
  104. package/scripts/check_markdown_links.mjs +75 -0
  105. package/scripts/wheel_smoke.py +7 -0
  106. package/src-tauri/Cargo.lock +1 -1
  107. package/src-tauri/Cargo.toml +1 -1
  108. package/src-tauri/src/main.rs +12 -2
  109. package/src-tauri/tauri.conf.json +1 -1
  110. package/static/app/asset-manifest.json +5 -5
  111. package/static/app/assets/index-CHHal8Zl.css +2 -0
  112. package/static/app/assets/index-pdzil9ac.js +333 -0
  113. package/static/app/assets/index-pdzil9ac.js.map +1 -0
  114. package/static/app/index.html +2 -2
  115. package/latticeai/api/deps.py +0 -15
  116. package/scripts/capture/README.md +0 -28
  117. package/scripts/capture/capture_enterprise.js +0 -8
  118. package/scripts/capture/capture_graph.js +0 -8
  119. package/scripts/capture/capture_onboarding.js +0 -8
  120. package/scripts/capture/capture_page.js +0 -43
  121. package/scripts/capture/capture_release_media.js +0 -125
  122. package/scripts/capture/capture_skills.js +0 -8
  123. package/scripts/capture/capture_v340.js +0 -88
  124. package/scripts/capture/capture_workspace.js +0 -8
  125. package/scripts/generate_diagrams.py +0 -512
  126. package/scripts/release-0.3.1.sh +0 -105
  127. package/scripts/take_screenshots.js +0 -69
  128. package/static/app/assets/index-BhPuj8rT.js +0 -333
  129. package/static/app/assets/index-BhPuj8rT.js.map +0 -1
  130. package/static/app/assets/index-yZswHE3d.css +0 -2
  131. package/static/css/tokens.3ba22e37.css +0 -260
@@ -1 +1,11 @@
1
- from latticeai.brain.schema import * # noqa: F401,F403
1
+ """Compatibility shim: implementation moved to lattice_brain.graph.schema.
2
+
3
+ This module aliases itself to the physical module so identity, singletons,
4
+ and monkeypatching behave as if the old flat path were the real module.
5
+ """
6
+
7
+ import sys
8
+
9
+ from .graph import schema as _impl
10
+
11
+ sys.modules[__name__] = _impl
@@ -1,3 +1,11 @@
1
- from latticeai.brain.store import KnowledgeGraphStore
1
+ """Compatibility shim: implementation moved to lattice_brain.graph.store.
2
2
 
3
- __all__ = ["KnowledgeGraphStore"]
3
+ This module aliases itself to the physical module so identity, singletons,
4
+ and monkeypatching behave as if the old flat path were the real module.
5
+ """
6
+
7
+ import sys
8
+
9
+ from .graph import store as _impl
10
+
11
+ sys.modules[__name__] = _impl
@@ -0,0 +1,461 @@
1
+ """Workflow engine — typed-node workflow definitions, validation, and a
2
+ deterministic execution interpreter with full run observability.
3
+
4
+ A workflow is a small directed graph of *nodes* starting from a ``trigger``.
5
+ Each node has a ``type`` (:data:`NODE_TYPES`), a ``config`` blob, and a ``next``
6
+ pointer (or a list of branches for ``condition`` nodes). The engine walks the
7
+ graph from the trigger, dispatching each node to an injected *runner* and
8
+ recording a step-by-step timeline so a run can be inspected, replayed, and
9
+ linked into the Workspace timeline / Knowledge Graph.
10
+
11
+ The engine is pure logic with injected runners, mirroring
12
+ :class:`latticeai.core.agent.AgentRuntime`:
13
+
14
+ * production wires runners that call the real tool registry, skill registry,
15
+ plugin registry, and multi-agent orchestrator;
16
+ * tests pass fakes and drive a full trigger→...→output run with no server,
17
+ no LLM, and no network.
18
+
19
+ Backward compatibility: legacy workflows persisted as a flat ``steps`` list
20
+ (pre-2.0) still validate and run — :func:`normalize_definition` lifts them into
21
+ a linear node chain so existing workflow history keeps working.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from dataclasses import dataclass, field
27
+ from datetime import datetime
28
+ from typing import Any, Callable, Dict, List, Optional
29
+
30
+
31
+ WORKFLOW_ENGINE_VERSION = "2.2.0"
32
+
33
+ # The node vocabulary a workflow can be built from. ``trigger`` and ``output``
34
+ # are structural; the rest dispatch to an injected runner of the same family.
35
+ NODE_TYPES = (
36
+ "trigger",
37
+ "tool",
38
+ "skill",
39
+ "plugin",
40
+ "agent",
41
+ "condition",
42
+ "output",
43
+ )
44
+
45
+ # Which runner family handles each executable node type.
46
+ _RUNNER_FOR = {
47
+ "tool": "tool",
48
+ "skill": "skill",
49
+ "plugin": "plugin",
50
+ "agent": "agent",
51
+ }
52
+
53
+ _MAX_STEPS = 100 # hard cap so a mis-wired ``next`` cycle can never hang a run.
54
+
55
+
56
+ class WorkflowError(Exception):
57
+ """Raised for invalid workflow definitions."""
58
+
59
+
60
+ def _now() -> str:
61
+ return datetime.now().isoformat(timespec="seconds")
62
+
63
+
64
+ def normalize_definition(workflow: Dict[str, Any]) -> Dict[str, Any]:
65
+ """Return a node-based definition, lifting legacy ``steps`` lists if needed.
66
+
67
+ Never mutates the input. A legacy ``{"steps": [...]}`` workflow becomes a
68
+ linear ``trigger -> tool... -> output`` node chain so it validates and runs
69
+ under the v2.0 engine without rewriting stored history.
70
+ """
71
+ nodes = workflow.get("nodes")
72
+ if isinstance(nodes, list) and nodes:
73
+ return {
74
+ "id": workflow.get("id"),
75
+ "name": workflow.get("name") or "Untitled workflow",
76
+ "nodes": nodes,
77
+ "metadata": workflow.get("metadata") or {},
78
+ }
79
+
80
+ steps = workflow.get("steps") or []
81
+ lifted: List[Dict[str, Any]] = [{
82
+ "id": "trigger",
83
+ "type": "trigger",
84
+ "name": "Start",
85
+ "config": {"trigger": "manual"},
86
+ "next": "step-0" if steps else "output",
87
+ }]
88
+ for index, step in enumerate(steps):
89
+ action = str(step.get("action") or "tool") if isinstance(step, dict) else "tool"
90
+ nxt = f"step-{index + 1}" if index + 1 < len(steps) else "output"
91
+ lifted.append({
92
+ "id": f"step-{index}",
93
+ "type": "tool",
94
+ "name": action,
95
+ "config": {"tool": action, "args": step if isinstance(step, dict) else {"value": step}},
96
+ "next": nxt,
97
+ })
98
+ lifted.append({"id": "output", "type": "output", "name": "Output", "config": {}, "next": None})
99
+ return {
100
+ "id": workflow.get("id"),
101
+ "name": workflow.get("name") or "Untitled workflow",
102
+ "nodes": lifted,
103
+ "metadata": {**(workflow.get("metadata") or {}), "lifted_from_steps": True},
104
+ }
105
+
106
+
107
+ def validate_definition(workflow: Dict[str, Any]) -> List[str]:
108
+ """Return a list of validation errors ([] means valid)."""
109
+ errors: List[str] = []
110
+ definition = normalize_definition(workflow)
111
+ nodes = definition["nodes"]
112
+ if not isinstance(nodes, list) or not nodes:
113
+ return ["workflow has no nodes"]
114
+
115
+ ids = [node.get("id") for node in nodes]
116
+ if len(set(ids)) != len(ids):
117
+ errors.append("duplicate node ids")
118
+ id_set = {nid for nid in ids if nid}
119
+
120
+ triggers = [node for node in nodes if node.get("type") == "trigger"]
121
+ if not triggers:
122
+ errors.append("workflow must have a trigger node")
123
+ elif len(triggers) > 1:
124
+ errors.append("workflow must have exactly one trigger node")
125
+
126
+ for node in nodes:
127
+ nid = node.get("id")
128
+ ntype = node.get("type")
129
+ if not nid:
130
+ errors.append("node missing id")
131
+ if ntype not in NODE_TYPES:
132
+ errors.append(f"node '{nid}': unknown type '{ntype}'")
133
+ # Validate edges point at real nodes (None terminates a branch).
134
+ targets: List[Any] = []
135
+ if ntype == "condition":
136
+ branches = node.get("branches") or {}
137
+ if not isinstance(branches, dict) or not branches:
138
+ errors.append(f"condition node '{nid}' must define branches (e.g. true/false)")
139
+ else:
140
+ targets.extend(branches.values())
141
+ else:
142
+ targets.append(node.get("next"))
143
+ for target in targets:
144
+ if target is not None and target not in id_set:
145
+ errors.append(f"node '{nid}' points at unknown node '{target}'")
146
+ return errors
147
+
148
+
149
+ def _entry_node(nodes: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
150
+ for node in nodes:
151
+ if node.get("type") == "trigger":
152
+ return node
153
+ return nodes[0] if nodes else None
154
+
155
+
156
+ def _evaluate_condition(config: Dict[str, Any], context: Dict[str, Any]) -> bool:
157
+ """Safe condition evaluation — NO eval. Compares a context value to a literal.
158
+
159
+ config: ``{"left": "<context key>", "op": "==|!=|>|<|>=|<=|contains|truthy",
160
+ "right": <literal>}``. Unknown keys / ops resolve to ``False`` so a
161
+ mis-configured condition fails closed onto the ``false`` branch.
162
+ """
163
+ left_key = config.get("left")
164
+ op = str(config.get("op") or "truthy")
165
+ right = config.get("right")
166
+ left = context.get(left_key) if left_key in context else config.get("left_value")
167
+ try:
168
+ if op == "truthy":
169
+ return bool(left)
170
+ if op == "==":
171
+ return left == right
172
+ if op == "!=":
173
+ return left != right
174
+ if op == "contains":
175
+ return right in left # type: ignore[operator]
176
+ if op in (">", "<", ">=", "<="):
177
+ lf, rf = float(left), float(right) # type: ignore[arg-type]
178
+ return {">": lf > rf, "<": lf < rf, ">=": lf >= rf, "<=": lf <= rf}[op]
179
+ except Exception:
180
+ return False
181
+ return False
182
+
183
+
184
+ @dataclass
185
+ class WorkflowRun:
186
+ workflow_id: Optional[str]
187
+ name: str
188
+ status: str = "ok" # ok | failed | partial | awaiting_approval
189
+ timeline: List[Dict[str, Any]] = field(default_factory=list)
190
+ outputs: Dict[str, Any] = field(default_factory=dict)
191
+ started_at: str = field(default_factory=_now)
192
+ finished_at: Optional[str] = None
193
+ # Suspension cursor (status == awaiting_approval): the paused node, what
194
+ # it is waiting for, and a JSON-serializable context snapshot resume()
195
+ # re-enters with — completed nodes are never re-executed.
196
+ paused_node: Optional[str] = None
197
+ pending_approval: Optional[Dict[str, Any]] = None
198
+ paused_context: Optional[Dict[str, Any]] = None
199
+
200
+ def as_dict(self) -> Dict[str, Any]:
201
+ return {
202
+ "workflow_id": self.workflow_id,
203
+ "name": self.name,
204
+ "status": self.status,
205
+ "timeline": self.timeline,
206
+ "outputs": self.outputs,
207
+ "started_at": self.started_at,
208
+ "finished_at": self.finished_at,
209
+ "step_count": len(self.timeline),
210
+ "paused_node": self.paused_node,
211
+ "pending_approval": self.pending_approval,
212
+ "paused_context": self.paused_context,
213
+ }
214
+
215
+
216
+ class ApprovalRequired(Exception):
217
+ """A node needs an explicit human decision before it may execute.
218
+
219
+ Raised by governed runners (e.g. a non-auto-approve tool). The engine
220
+ pauses the run into ``awaiting_approval`` with a serializable cursor —
221
+ it never records a fake success and never silently skips the node.
222
+ """
223
+
224
+ def __init__(self, message: str, *, tool: Optional[str] = None,
225
+ args: Optional[Dict[str, Any]] = None,
226
+ permission: Optional[Dict[str, Any]] = None):
227
+ super().__init__(message)
228
+ self.tool = tool
229
+ self.args = args or {}
230
+ self.permission = permission or {}
231
+
232
+
233
+ def _json_safe(value: Any) -> Any:
234
+ """Round-trip through JSON so paused context is durably serializable."""
235
+ import json as _json
236
+
237
+ return _json.loads(_json.dumps(value, ensure_ascii=False, default=str))
238
+
239
+
240
+ class WorkflowEngine:
241
+ """Interprets a validated workflow definition over injected runners.
242
+
243
+ ``runners`` maps a family ("tool" / "skill" / "plugin" / "agent") to a
244
+ callable ``runner(node, context) -> Any``. A missing runner records the
245
+ node as ``skipped`` with a reason rather than failing the whole run, so a
246
+ workflow that references a capability the host has not wired degrades
247
+ gracefully (and the gap is visible in the timeline).
248
+
249
+ Suspension model (v4): a runner raising :class:`ApprovalRequired` pauses
250
+ the run (status ``awaiting_approval``) with the node cursor and a
251
+ JSON-serializable context snapshot. :meth:`resume` re-enters at the
252
+ paused node — completed nodes are NEVER re-executed.
253
+ """
254
+
255
+ def __init__(self, runners: Optional[Dict[str, Callable[..., Any]]] = None, *, hooks: Any = None):
256
+ self.runners = runners or {}
257
+ # Optional lifecycle hooks registry. When present, ``run`` fires the
258
+ # ``workflow`` hooks at workflow start and end so automation registered
259
+ # against the workflow lifecycle actually executes.
260
+ self.hooks = hooks
261
+
262
+ def run(self, workflow: Dict[str, Any], *, inputs: Optional[Dict[str, Any]] = None) -> WorkflowRun:
263
+ definition = normalize_definition(workflow)
264
+ errors = validate_definition(definition)
265
+ run = WorkflowRun(workflow_id=definition.get("id"), name=definition.get("name") or "workflow")
266
+ if self.hooks is not None:
267
+ self.hooks.fire_hook(
268
+ "pre_workflow", "workflow.start",
269
+ payload={"workflow_id": definition.get("id"), "name": definition.get("name"), "valid": not errors},
270
+ )
271
+ if errors:
272
+ run.status = "failed"
273
+ run.timeline.append({"node": None, "type": "validation", "status": "failed", "errors": errors, "timestamp": _now()})
274
+ run.finished_at = _now()
275
+ if self.hooks is not None:
276
+ self.hooks.fire_hook(
277
+ "post_workflow", "workflow.end",
278
+ payload={"workflow_id": definition.get("id"), "status": run.status},
279
+ )
280
+ return run
281
+
282
+ nodes = {node["id"]: node for node in definition["nodes"]}
283
+ context: Dict[str, Any] = {"inputs": inputs or {}, **(inputs or {})}
284
+ current = _entry_node(definition["nodes"])
285
+ return self._execute(definition, run, nodes, context, current)
286
+
287
+ def resume(
288
+ self,
289
+ workflow: Dict[str, Any],
290
+ *,
291
+ paused_node: str,
292
+ paused_context: Dict[str, Any],
293
+ approved: bool,
294
+ prior_timeline: Optional[List[Dict[str, Any]]] = None,
295
+ ) -> WorkflowRun:
296
+ """Re-enter a paused run at its cursor; completed nodes never re-run.
297
+
298
+ ``approved=True`` marks the paused node as human-approved (its runner
299
+ sees the node id in ``context['__approved_nodes__']``); ``False``
300
+ records an explicit denial and fails the run honestly.
301
+ """
302
+ definition = normalize_definition(workflow)
303
+ nodes = {node["id"]: node for node in definition["nodes"]}
304
+ node = nodes.get(paused_node)
305
+ run = WorkflowRun(workflow_id=definition.get("id"), name=definition.get("name") or "workflow")
306
+ if prior_timeline:
307
+ run.timeline.extend(prior_timeline)
308
+ if node is None:
309
+ run.status = "failed"
310
+ run.timeline.append({"node": paused_node, "type": "resume", "status": "failed",
311
+ "reason": "paused node no longer exists in the definition",
312
+ "timestamp": _now()})
313
+ run.finished_at = _now()
314
+ return run
315
+ context: Dict[str, Any] = dict(paused_context or {})
316
+ if not approved:
317
+ run.status = "failed"
318
+ run.timeline.append({"node": paused_node, "type": node.get("type"),
319
+ "name": node.get("name") or paused_node,
320
+ "status": "denied",
321
+ "reason": "approval denied by the user",
322
+ "timestamp": _now()})
323
+ run.finished_at = _now()
324
+ return run
325
+ approvals = set(context.get("__approved_nodes__") or [])
326
+ approvals.add(paused_node)
327
+ context["__approved_nodes__"] = sorted(approvals)
328
+ return self._execute(definition, run, nodes, context, node)
329
+
330
+ def _execute(
331
+ self,
332
+ definition: Dict[str, Any],
333
+ run: WorkflowRun,
334
+ nodes: Dict[str, Dict[str, Any]],
335
+ context: Dict[str, Any],
336
+ current: Optional[Dict[str, Any]],
337
+ ) -> WorkflowRun:
338
+ steps = 0
339
+ had_error = False
340
+ had_skip = False
341
+ while current is not None and steps < _MAX_STEPS:
342
+ steps += 1
343
+ ntype = current.get("type")
344
+ nid = current.get("id")
345
+ entry: Dict[str, Any] = {
346
+ "node": nid,
347
+ "type": ntype,
348
+ "name": current.get("name") or nid,
349
+ "timestamp": _now(),
350
+ }
351
+
352
+ if ntype == "trigger":
353
+ entry["status"] = "ok"
354
+ entry["trigger"] = (current.get("config") or {}).get("trigger", "manual")
355
+ run.timeline.append(entry)
356
+ current = nodes.get(current.get("next")) if current.get("next") else None
357
+ continue
358
+
359
+ if ntype == "output":
360
+ entry["status"] = "ok"
361
+ payload = (current.get("config") or {}).get("value")
362
+ entry["output"] = payload if payload is not None else context.get("last_output")
363
+ run.outputs[nid] = entry["output"]
364
+ run.timeline.append(entry)
365
+ current = nodes.get(current.get("next")) if current.get("next") else None
366
+ continue
367
+
368
+ if ntype == "condition":
369
+ result = _evaluate_condition(current.get("config") or {}, context)
370
+ entry["status"] = "ok"
371
+ entry["result"] = result
372
+ run.timeline.append(entry)
373
+ branches = current.get("branches") or {}
374
+ target = branches.get("true" if result else "false")
375
+ current = nodes.get(target) if target else None
376
+ continue
377
+
378
+ # Executable node → dispatch to its runner family.
379
+ family = _RUNNER_FOR.get(ntype)
380
+ runner = self.runners.get(family) if family else None
381
+ if runner is None:
382
+ entry["status"] = "skipped"
383
+ entry["reason"] = f"no '{family}' runner configured"
384
+ had_skip = True
385
+ run.timeline.append(entry)
386
+ current = nodes.get(current.get("next")) if current.get("next") else None
387
+ continue
388
+ try:
389
+ result = runner(node=current, context=context)
390
+ entry["status"] = "ok"
391
+ entry["result"] = result
392
+ context["last_output"] = result
393
+ context[nid] = result
394
+ except ApprovalRequired as pause:
395
+ # Suspend — never a fake success, never a silent skip.
396
+ entry["status"] = "awaiting_approval"
397
+ entry["pending"] = {
398
+ "tool": pause.tool, "args": pause.args,
399
+ "permission": pause.permission, "reason": str(pause),
400
+ }
401
+ run.timeline.append(entry)
402
+ run.status = "awaiting_approval"
403
+ run.paused_node = nid
404
+ run.pending_approval = entry["pending"]
405
+ try:
406
+ run.paused_context = _json_safe(context)
407
+ except Exception:
408
+ run.paused_context = {"inputs": context.get("inputs") or {}}
409
+ if self.hooks is not None:
410
+ self.hooks.fire_hook(
411
+ "post_workflow", "workflow.paused",
412
+ payload={"workflow_id": definition.get("id"),
413
+ "status": run.status, "node": nid},
414
+ )
415
+ return run
416
+ except Exception as exc:
417
+ entry["status"] = "error"
418
+ entry["reason"] = str(exc)
419
+ had_error = True
420
+ run.timeline.append(entry)
421
+ current = nodes.get(current.get("next")) if current.get("next") else None
422
+
423
+ if steps >= _MAX_STEPS:
424
+ run.timeline.append({"node": None, "type": "guard", "status": "error", "reason": f"exceeded {_MAX_STEPS} steps (cycle?)", "timestamp": _now()})
425
+ had_error = True
426
+
427
+ run.status = "failed" if had_error else ("partial" if had_skip else "ok")
428
+ run.finished_at = _now()
429
+ if self.hooks is not None:
430
+ self.hooks.fire_hook(
431
+ "post_workflow", "workflow.end",
432
+ payload={"workflow_id": definition.get("id"), "name": definition.get("name"),
433
+ "status": run.status, "steps": steps},
434
+ )
435
+ return run
436
+
437
+
438
+ def export_workflow(workflow: Dict[str, Any]) -> Dict[str, Any]:
439
+ """Portable JSON representation (definition only — no run history / scope)."""
440
+ definition = normalize_definition(workflow)
441
+ return {
442
+ "lattice_workflow_export": WORKFLOW_ENGINE_VERSION,
443
+ "name": definition.get("name"),
444
+ "nodes": definition.get("nodes"),
445
+ "metadata": {k: v for k, v in (definition.get("metadata") or {}).items() if k != "lifted_from_steps"},
446
+ }
447
+
448
+
449
+ def import_workflow(data: Dict[str, Any]) -> Dict[str, Any]:
450
+ """Validate an exported workflow and return a definition ready to persist."""
451
+ if not isinstance(data, dict):
452
+ raise WorkflowError("import payload must be a JSON object")
453
+ definition = {
454
+ "name": data.get("name") or "Imported workflow",
455
+ "nodes": data.get("nodes") or [],
456
+ "metadata": {**(data.get("metadata") or {}), "imported": True},
457
+ }
458
+ errors = validate_definition(definition)
459
+ if errors:
460
+ raise WorkflowError("; ".join(errors))
461
+ return definition
@@ -1 +1,11 @@
1
- from latticeai.brain.write_master import * # noqa: F401,F403
1
+ """Compatibility shim: implementation moved to lattice_brain.graph.write_master.
2
+
3
+ This module aliases itself to the physical module so identity, singletons,
4
+ and monkeypatching behave as if the old flat path were the real module.
5
+ """
6
+
7
+ import sys
8
+
9
+ from .graph import write_master as _impl
10
+
11
+ sys.modules[__name__] = _impl
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "4.3.1"
3
+ __version__ = "4.4.0"
@@ -45,8 +45,8 @@ def create_agents_router(
45
45
  agent_runtime: Any = None,
46
46
  run_executor: Any = None,
47
47
  ) -> APIRouter:
48
- from latticeai.core.multi_agent import AGENT_ROLES, ROLE_AGENT_IDS
49
- from latticeai.services.agent_runtime import AgentRuntime, AgentRuntimeUnavailable
48
+ from lattice_brain.runtime.multi_agent import AGENT_ROLES, ROLE_AGENT_IDS
49
+ from lattice_brain.runtime.agent_runtime import AgentRuntime, AgentRuntimeUnavailable
50
50
 
51
51
  # Single AgentRuntime boundary: the router (and via it, the frontend) talks
52
52
  # to this façade instead of reaching into the orchestrator/store directly.
@@ -24,7 +24,7 @@ from urllib.parse import urlparse
24
24
  from fastapi import APIRouter, HTTPException, Request
25
25
  from pydantic import BaseModel
26
26
 
27
- from latticeai.services.ingestion import IngestionItem
27
+ from lattice_brain.ingestion import IngestionItem
28
28
 
29
29
  MAX_TAB_BYTES = 4 * 1024 * 1024 # 4 MB per captured tab payload
30
30
  MAX_URL_FETCH_BYTES = 4 * 1024 * 1024 # 4 MB cap on a fetched page
@@ -24,7 +24,7 @@ from PIL import Image
24
24
  from latticeai.core.agent import AgentRunContext, AgentState
25
25
  from latticeai.core.context_builder import format_sources_footnote, retrieve_context_for_generation
26
26
  from latticeai.core.document_generator import DocumentGenerationSession, detect_document_intent
27
- from latticeai.core.hooks import dispatch_tool
27
+ from lattice_brain.runtime.hooks import dispatch_tool
28
28
  from latticeai.services.app_context import AppContext
29
29
  from latticeai.services.tool_dispatch import build_agent_runtime, collect_created_files
30
30
  from tools import AGENT_ROOT, ToolError, ensure_agent_root, execute_tool, knowledge_save, local_read, network_status
@@ -11,7 +11,7 @@ from fastapi.responses import StreamingResponse
11
11
  from pydantic import BaseModel
12
12
 
13
13
  from latticeai.core.agent import extract_action as _extract_agent_action
14
- from latticeai.core.hooks import dispatch_tool
14
+ from lattice_brain.runtime.hooks import dispatch_tool
15
15
  from tools import (
16
16
  ToolError,
17
17
  computer_click,
@@ -1,6 +1,6 @@
1
1
  """Hooks platform API router (v3.2.0).
2
2
 
3
- Exposes the lifecycle :class:`~latticeai.core.hooks.HooksRegistry` over HTTP so
3
+ Exposes the lifecycle :class:`~lattice_brain.runtime.hooks.HooksRegistry` over HTTP so
4
4
  the /app Hooks view can list, inspect, enable/disable, reorder, and register
5
5
  hooks. Full paths live in the decorators (no ``prefix=``), matching the rest of
6
6
  the API.
@@ -13,7 +13,7 @@ from typing import Callable, List, Optional
13
13
  from fastapi import APIRouter, HTTPException, Request
14
14
  from pydantic import BaseModel
15
15
 
16
- from latticeai.core.hooks import HooksRegistry
16
+ from lattice_brain.runtime.hooks import HooksRegistry
17
17
 
18
18
 
19
19
  class HookToggleRequest(BaseModel):
@@ -17,7 +17,7 @@ from datetime import datetime
17
17
  from pathlib import Path
18
18
  from typing import Any, Callable, Dict, List, Optional
19
19
 
20
- from latticeai.services.ingestion import IngestionItem
20
+ from lattice_brain.ingestion import IngestionItem
21
21
  from fastapi import APIRouter, HTTPException, Request
22
22
  from pydantic import BaseModel
23
23
 
@@ -15,7 +15,7 @@ from pydantic import BaseModel
15
15
 
16
16
  from latticeai.api.computer_use import create_computer_use_router
17
17
  from latticeai.api.local_files import create_local_files_router
18
- from latticeai.core.hooks import dispatch_tool
18
+ from lattice_brain.runtime.hooks import dispatch_tool
19
19
  from latticeai.api.mcp import create_mcp_router
20
20
  from latticeai.api.permissions import create_permissions_router
21
21
  from latticeai.services.upload_service import process_uploaded_document
@@ -1,7 +1,7 @@
1
1
  """Workflow Designer API router (v2).
2
2
 
3
3
  Create / edit / validate / execute / inspect / export / import workflows plus
4
- run history, layered on :mod:`latticeai.core.workflow_engine` and the existing
4
+ run history, layered on :mod:`lattice_brain.workflow` and the existing
5
5
  ``WorkspaceOSStore`` workflow persistence (so pre-2.0 workflow history is
6
6
  preserved). Paths are namespaced under ``/workflows`` to avoid colliding with
7
7
  ``/workspace/workflows``.
@@ -67,7 +67,7 @@ def create_workflow_designer_router(
67
67
  run_executor: Any = None,
68
68
  trigger_service: Any = None,
69
69
  ) -> APIRouter:
70
- from latticeai.core.workflow_engine import (
70
+ from lattice_brain.workflow import (
71
71
  WorkflowEngine,
72
72
  validate_definition,
73
73
  export_workflow,
@@ -110,7 +110,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
110
110
  from latticeai.services.chat_service import ChatService
111
111
  from latticeai.services.search_service import SearchService
112
112
  from latticeai.core.embedding_providers import resolve_embedder, resolve_embedding_profile
113
- from latticeai.services.agent_runtime import AgentRuntime
113
+ from lattice_brain.runtime.agent_runtime import AgentRuntime
114
114
  from latticeai.services.model_runtime import (
115
115
  CLOUD_VERIFY_TTL_SECONDS,
116
116
  ENGINE_MODEL_CATALOG,
@@ -152,7 +152,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
152
152
  from latticeai.api.garden import create_garden_router
153
153
  from latticeai.api.setup import create_setup_router
154
154
  from latticeai.api.hooks import create_hooks_router
155
- from latticeai.core.hooks import HooksRegistry
155
+ from lattice_brain.runtime.hooks import HooksRegistry
156
156
  from latticeai.core.builtin_hooks import register_builtin_hook_runners
157
157
  from latticeai.core.product_hardening import build_product_hardening_status
158
158
  from latticeai.api.agent_registry import create_agent_registry_router
@@ -161,7 +161,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
161
161
  from latticeai.api.browser import create_browser_router
162
162
  from latticeai.api.portability import create_portability_router
163
163
  from latticeai.services.memory_service import MemoryService
164
- from latticeai.services.ingestion import IngestionItem, IngestionPipeline
164
+ from lattice_brain.ingestion import IngestionItem, IngestionPipeline
165
165
  from lattice_brain import BrainCore, ConversationStore
166
166
  from lattice_brain.storage import storage_from_env
167
167
  from lattice_brain.context import ContextAssembler
@@ -169,7 +169,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
169
169
  from lattice_brain.identity import DeviceIdentity
170
170
  from lattice_brain.network import BrainNetwork
171
171
  from latticeai.api.network import create_network_router
172
- from latticeai.services.kg_portability import KGPortabilityService
172
+ from lattice_brain.portability import KGPortabilityService
173
173
  # The aliased names below look unused but are part of the legacy
174
174
  # ``server_app`` attribute surface: every local is exported via
175
175
  # ``dict(locals())`` and reached through ``server_app.__getattr__``