ltcai 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,8 @@ You need Lattice AI when:
45
45
  source-aware memory.
46
46
  - See recent memories, older memories, topics, relationships, and the full
47
47
  knowledge graph when you want deeper structure.
48
+ - Create consent-first Brain automation drafts for memory digests, project
49
+ reviews, and follow-up suggestions before any schedule is enabled.
48
50
  - Use a recommended local model without learning model internals first.
49
51
  - Keep advanced controls, audit logs, roles, and retention in a separate Admin
50
52
  surface.
@@ -191,25 +193,24 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
191
193
 
192
194
  ## Current Release Preparation
193
195
 
194
- The current development target is **5.3.0 Product Clarity and Runtime Cleanup**:
196
+ The current development target is **5.4.0 Brain Automation Scheduler**:
195
197
 
196
- - README and first-run copy are organized around the local-first Digital Brain.
197
- - Product descriptions are aligned across package metadata, architecture docs,
198
- feature status, release notes, and extension metadata.
199
- - Release artifacts and exact filenames are kept in the release section instead
200
- of interrupting the product introduction.
201
- - `app_factory.py` is being thinned by moving config, security, and Brain runtime
202
- assembly seams into `latticeai.runtime`.
203
- - Legacy root modules are documented in
204
- [docs/LEGACY_COMPATIBILITY.md](docs/LEGACY_COMPATIBILITY.md).
198
+ - Consent-first Brain automation recipes (Daily Memory Digest, Weekly Project Review,
199
+ Follow-up Radar) install from Automate page as reviewable disabled drafts.
200
+ - TriggerService provides scheduler/brain-event triggers with dedup guards,
201
+ LATTICE_TZ support, consecutive failure degraded status, and explicit enabled:false
202
+ disarming.
203
+ - lattice_brain/runtime dependency graph 정리 and entrypoint documentation added.
204
+ - E2E scenarios for draft install + trigger execution with provenance.
205
+ - All package/runtime/static versions synchronized to 5.4.0; PR #4 remote gates passed.
205
206
 
206
- Expected artifacts for a future 5.3.0 release must use exact filenames:
207
+ Expected artifacts for 5.4.0 release must use exact filenames:
207
208
 
208
- - `dist/ltcai-5.3.0-py3-none-any.whl`
209
- - `dist/ltcai-5.3.0.tar.gz`
210
- - `ltcai-5.3.0.tgz`
211
- - `dist/ltcai-5.3.0.vsix`
212
- - `src-tauri/target/release/bundle/dmg/Lattice AI_5.3.0_aarch64.dmg`
209
+ - `dist/ltcai-5.4.0-py3-none-any.whl`
210
+ - `dist/ltcai-5.4.0.tar.gz`
211
+ - `ltcai-5.4.0.tgz`
212
+ - `dist/ltcai-5.4.0.vsix`
213
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_5.4.0_aarch64.dmg`
213
214
 
214
215
  Do not upload `dist/*`. Package registry publishing remains owner-run.
215
216
 
@@ -228,6 +229,7 @@ Do not upload `dist/*`. Package registry publishing remains owner-run.
228
229
 
229
230
  | Version | Theme |
230
231
  | --- | --- |
232
+ | 5.4.0 | Brain Automation Scheduler: consent-first recipe drafts (Daily/Weekly/Follow-up), TriggerService with dedup/LATTICE_TZ/degraded, runtime graph cleanup, E2E scenarios |
231
233
  | 5.3.0 | Product Clarity and Runtime Cleanup: user-first README/onboarding, unified Digital Brain identity, legacy compatibility map, and app factory runtime seams |
232
234
  | 5.2.0 | User-Focused Model Transformation: structured model capability registry, HF verification transparency, model recommendation UX, and workspace-scoped marketplace state |
233
235
  | 5.1.0 | Product Trust & Clarity Release: clarifies the private AI memory-layer promise, hardens CSP/secret/auto-read/download gates, adds trust/privacy docs, and refreshes evidence |
package/docs/CHANGELOG.md CHANGED
@@ -3,6 +3,33 @@
3
3
  The top entry is the current release-preparation target. Older entries are
4
4
  historical and may describe behavior as it existed at that release.
5
5
 
6
+ ## [5.4.0] - 2026-06-15
7
+
8
+ > Brain Automation Scheduler. Consent-first recipe drafts (Daily Memory Digest,
9
+ > Weekly Project Review, Follow-up Radar) install as disabled workflows.
10
+ > Scheduler triggers (TriggerService) with dedup, LATTICE_TZ, degraded status,
11
+ > and runtime graph cleanup.
12
+
13
+ ### Added
14
+ - Consent-first Brain automation recipe drafts for Daily Memory Digest, Weekly
15
+ Project Review, and Follow-up Radar. Recipes install as disabled workflows so
16
+ users can inspect them before any scheduler or Brain-event trigger fires.
17
+
18
+ ### Changed
19
+ - Workflow trigger scanning now treats explicit `enabled: false` trigger config
20
+ as disarmed while preserving legacy behavior for existing workflows that do
21
+ not include an `enabled` field.
22
+ - The Automate page now surfaces Brain automation recipe cards with local-only,
23
+ review-before-run consent copy.
24
+ - Recipe install UX: success feedback on "Create reviewable draft", install-time
25
+ button disabled to block duplicates clicks, and if same recipe draft already
26
+ exists (by metadata), button state changes + guide text instead of re-creating.
27
+ - lattice_brain/runtime dependency/responsibility graph 정리 + 실제 진입점 매핑 문서화 (runtime/* 모듈 헤더 + app_factory 주석). AgentRuntime (lattice_brain facade) vs latticeai/core/agent (single-agent state/plan/transcript) 분리 명확화.
28
+ - TriggerService 스케줄러 엣지케이스 보강: LATTICE_TZ env 지원 (describe() 노출), last_attempt_at + cooldown dedup 가드로 중복 실행 방지 (interval/brain_event), consecutive_failures + "degraded" status per-trigger 서페이싱.
29
+ - A방향 E2E 시나리오 초안 brain_automation.py 에 작성 (draft install, dedup, consent-first, trigger fire with provenance, LATTICE_TZ, degraded, review flow, RunExecutor 경로).
30
+
31
+ ## Unreleased
32
+
6
33
  ## [5.3.0] - 2026-06-14
7
34
 
8
35
  > Product Clarity and Runtime Cleanup. Lattice AI is now presented consistently
@@ -24,6 +24,8 @@ import { ProductFlow, readProductFlowComplete } from "@/components/ProductFlow";
24
24
  import { useAppStore } from "@/store/appStore";
25
25
  import { asArray } from "@/lib/utils";
26
26
  import { LANGUAGE_LABELS, t, type Language } from "@/i18n";
27
+ import { parseHash } from "@/routes";
28
+ import { ActPage } from "@/pages/Act";
27
29
 
28
30
  type ApiRecord = Record<string, unknown>;
29
31
  type BrainDepth = 1 | 2 | 3 | 4 | 5;
@@ -73,7 +75,8 @@ export default function App() {
73
75
  const theme = useAppStore((state) => state.theme);
74
76
  const language = useAppStore((state) => state.language);
75
77
  const [flowComplete, setFlowComplete] = React.useState(readProductFlowComplete);
76
- const route = useHashRoute();
78
+ const rawRoute = useHashRoute();
79
+ const parsed = React.useMemo(() => parseHash(), [rawRoute]);
77
80
  const { state: brainState, intensity, setBrain } = useBrainState();
78
81
 
79
82
  React.useEffect(() => {
@@ -99,8 +102,10 @@ export default function App() {
99
102
  return (
100
103
  <div className="brain-space">
101
104
  <div className="brain-field" />
102
- {route.startsWith("/admin") ? (
105
+ {rawRoute.startsWith("/admin") ? (
103
106
  <AdminConsole onBack={() => navigateHash("/brain")} />
107
+ ) : parsed.primary === "act" ? (
108
+ <ActPage initialTab={parsed.tab} />
104
109
  ) : (
105
110
  <BrainHome brainState={brainState} intensity={intensity} onBrainChange={setBrain} />
106
111
  )}
@@ -576,6 +581,7 @@ function AdminConsole({ onBack }: { onBack: () => void }) {
576
581
  <span>{stringValue(retention.retained_events, "0")} retained · {stringValue(retention.prune_candidates, "0")} ready for export/prune review</span>
577
582
  </div>
578
583
  </AdminPanel>
584
+
579
585
  </section>
580
586
  </main>
581
587
  );
@@ -368,6 +368,8 @@ export const latticeApi = {
368
368
  workflowDefinitions: () => get("/workflows/api/definitions", { workflows: [] }),
369
369
  workflowRuns: () => get("/workflows/api/runs", { runs: [] }),
370
370
  workflowTriggers: () => get("/workflows/api/triggers", { armed: [] }),
371
+ automationRecipes: () => get("/workflows/api/automation/recipes", { recipes: [], principles: {} }),
372
+ installAutomationRecipe: (recipeId: string, enabled = false) => post(`/workflows/api/automation/recipes/${encodeURIComponent(recipeId)}`, { enabled }, {}),
371
373
  createWorkflow: (body: { name: string; nodes: Array<Record<string, unknown>>; metadata?: Record<string, unknown> }) => post("/workflows/api/definitions", body, {}),
372
374
  importWorkflow: (data: Record<string, unknown>) => post("/workflows/api/import", { data }, {}),
373
375
  exportWorkflow: (id: string) => get(`/workflows/api/export/${encodeURIComponent(id)}`, {}),
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import ReactFlow, { Background, Controls, Edge, Node } from "reactflow";
4
- import { Bot, GitBranch, PauseCircle, Play, Workflow } from "lucide-react";
4
+ import { Bot, CalendarClock, GitBranch, PauseCircle, Play, ShieldCheck, Workflow } from "lucide-react";
5
5
  import { latticeApi } from "@/api/client";
6
6
  import { ActionButton, DataPanel, EntityList, KeyValueList, ModeGate, OperationResult, StructuredView, Tabs } from "@/components/primitives";
7
7
  import { Badge } from "@/components/ui/badge";
@@ -199,6 +199,7 @@ function WorkflowsPanel() {
199
199
  const qc = useQueryClient();
200
200
  const defs = useQuery({ queryKey: ["workflowDefinitions"], queryFn: latticeApi.workflowDefinitions });
201
201
  const triggers = useQuery({ queryKey: ["workflowTriggers"], queryFn: latticeApi.workflowTriggers });
202
+ const recipes = useQuery({ queryKey: ["automationRecipes"], queryFn: latticeApi.automationRecipes });
202
203
  const [name, setName] = React.useState("Manual workflow");
203
204
  const [importText, setImportText] = React.useState("");
204
205
  const create = useMutation({
@@ -216,7 +217,19 @@ function WorkflowsPanel() {
216
217
  qc.invalidateQueries({ queryKey: ["workflowDefinitions"] });
217
218
  },
218
219
  });
220
+ const installRecipe = useMutation({
221
+ mutationFn: (recipeId: string) => latticeApi.installAutomationRecipe(recipeId, false),
222
+ onSuccess: () => {
223
+ qc.invalidateQueries({ queryKey: ["workflowDefinitions"] });
224
+ qc.invalidateQueries({ queryKey: ["workflowTriggers"] });
225
+ },
226
+ });
219
227
  const workflows = asArray<Record<string, unknown>>((defs.data?.data as Record<string, unknown>)?.workflows);
228
+ const installedRecipeIds = new Set(
229
+ workflows
230
+ .filter((w: any) => w && w.metadata && w.metadata.created_from === "brain_automation_recipe" && w.metadata.recipe_id)
231
+ .map((w: any) => String(w.metadata.recipe_id))
232
+ );
220
233
  const nodes: Node[] = workflows.slice(0, 12).map((workflow, index) => ({
221
234
  id: String(workflow.id || workflow.workflow_id || index),
222
235
  position: { x: (index % 4) * 190, y: Math.floor(index / 4) * 120 },
@@ -225,6 +238,74 @@ function WorkflowsPanel() {
225
238
  const edges: Edge[] = nodes.slice(1).map((node, index) => ({ id: `e-${index}`, source: nodes[index].id, target: node.id }));
226
239
  return (
227
240
  <div className="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
241
+ <DataPanel title="Brain automations" result={recipes.data} className="xl:col-span-2">
242
+ {(data) => {
243
+ const items = asArray<Record<string, unknown>>((data as Record<string, unknown>).recipes);
244
+ return (
245
+ <div className="grid gap-3 lg:grid-cols-3">
246
+ {items.map((recipe) => {
247
+ const id = String(recipe.id || "");
248
+ const consent = (recipe.consent || {}) as Record<string, unknown>;
249
+ const creates = asArray<string>(recipe.creates);
250
+ return (
251
+ <div key={id} className="rounded-lg border border-border bg-background/70 p-4">
252
+ <div className="flex items-start justify-between gap-2">
253
+ <div>
254
+ <div className="flex items-center gap-2 font-medium">
255
+ <CalendarClock className="h-4 w-4" /> {String(recipe.name || id)}
256
+ </div>
257
+ <p className="mt-2 text-sm text-muted-foreground">{String(recipe.summary || "")}</p>
258
+ </div>
259
+ <Badge variant="muted">{String(recipe.cadence || "draft")}</Badge>
260
+ </div>
261
+ <p className="mt-3 text-sm">{String(recipe.user_value || "")}</p>
262
+ <div className="mt-3 flex flex-wrap gap-2">
263
+ <Badge variant="success"><ShieldCheck className="h-3 w-3" /> local only</Badge>
264
+ {consent.requires_user_enable ? <Badge variant="warning">draft first</Badge> : null}
265
+ {creates.slice(0, 2).map((item) => <Badge key={item} variant="muted">{item}</Badge>)}
266
+ </div>
267
+ {(() => {
268
+ const isInstalling = installRecipe.isPending && installRecipe.variables === id;
269
+ const last = installRecipe.data as any;
270
+ const lastRid = last && last.recipe && last.recipe.recipe_id ? String(last.recipe.recipe_id) : "";
271
+ const justSucceeded = !installRecipe.isPending && lastRid === id;
272
+ const installed = installedRecipeIds.has(id);
273
+ const btnLabel = isInstalling
274
+ ? "Creating..."
275
+ : justSucceeded
276
+ ? "✓ Draft created"
277
+ : installed
278
+ ? "Draft already created"
279
+ : "Create reviewable draft";
280
+ return (
281
+ <>
282
+ <Button
283
+ className="mt-4 w-full"
284
+ variant={installed || justSucceeded ? "secondary" : "outline"}
285
+ disabled={!id || isInstalling || installed}
286
+ onClick={() => {
287
+ if (installed || isInstalling) return;
288
+ installRecipe.mutate(id);
289
+ }}
290
+ >
291
+ {btnLabel}
292
+ </Button>
293
+ {justSucceeded ? (
294
+ <p className="mt-1 text-[10px] text-green-600">Reviewable draft ready. Check Definitions below.</p>
295
+ ) : null}
296
+ {installed && !justSucceeded ? (
297
+ <p className="mt-1 text-[10px] text-muted-foreground">Reviewable draft exists — see Definitions.</p>
298
+ ) : null}
299
+ </>
300
+ );
301
+ })()}
302
+ </div>
303
+ );
304
+ })}
305
+ </div>
306
+ );
307
+ }}
308
+ </DataPanel>
228
309
  <Card>
229
310
  <CardHeader>
230
311
  <CardTitle className="flex items-center gap-2"><GitBranch className="h-4 w-4" /> Workflow graph</CardTitle>
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "5.3.0"
29
+ __version__ = "5.4.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -3,6 +3,59 @@
3
3
  Physically hosts the hooks registry/dispatch lifecycle, the multi-agent
4
4
  orchestrator, and the agent runtime service. Lazy-loaded so importing
5
5
  ``lattice_brain.runtime`` stays cheap.
6
+
7
+ === lattice_brain/runtime 책임 + 의존성 그래프 (A방향 + Act/automation 기준) ===
8
+ - multi_agent.py (핵심 실행체)
9
+ 책임: OrchestrationContext, AgentContextPacket, AgentHandoff, AgentRunResult,
10
+ MultiAgentOrchestrator, default_role_runner, llm_role_runner,
11
+ CORE_PIPELINE/AGENT_ROLES, handoff/review/retry 타임라인 생성.
12
+ 의존: 없음 (순수, deterministic/LLM runner 주입).
13
+ 진입: orchestrator_factory 로 AgentRuntime에 주입됨.
14
+
15
+ - hooks.py (라이프사이클 확장)
16
+ 책임: HookContext/HookResult, HooksRegistry (builtin+user, order/enabled persist),
17
+ dispatch_tool (pre_tool/post_tool 통합), fire_hook/run_hooks,
18
+ BUILTIN_HOOKS (redact, memory-snapshot, tool-permission-gate 등).
19
+ 의존: subprocess (user command hooks).
20
+ 진입: AgentRuntime, tool_dispatch, api/tools, core/agent 등에 주입/등록.
21
+
22
+ - agent_runtime.py (공개 퍼사드 / 바운더리)
23
+ 책임: AgentRuntime (store+orchestrator_factory+workspace_graph+audit+hooks 주입),
24
+ start/reserve_run/complete_reserved_run, stop, status/health/config,
25
+ list_runs/get_run/events/replay, _fire_pre_run / _post_run_hooks.
26
+ 의존: .multi_agent, .hooks (간접), store (WORKSPACE_OS).
27
+ 진입점: **여기가 제품 경계**. app_factory에서 AGENT_RUNTIME으로 생성,
28
+ api/agents.py (런타임 라우터), RunExecutor (async agent/workflow),
29
+ workflow_designer에 주입. frontend BrainAutomationPanel / Act 가
30
+ /agents/* 를 통해 이 바운더리만 의존.
31
+
32
+ - __init__.py : lazy re-export (import 비용 최소화).
33
+
34
+ 실제 진입점 매핑 (app_factory + 호출 스택):
35
+ app_factory.py:1508
36
+ AGENT_RUNTIME = AgentRuntime(store=WORKSPACE_OS, orchestrator_factory=PLATFORM.build_orchestrator, hooks=HOOKS_REGISTRY, ...)
37
+ RUN_EXECUTOR = RunExecutor(..., agent_runtime=AGENT_RUNTIME)
38
+ AGENT_RUNTIME.attach_executor(RUN_EXECUTOR)
39
+ create_agents_router(..., agent_runtime=AGENT_RUNTIME)
40
+ create_workflow_designer_router(..., run_executor=RUN_EXECUTOR, trigger_service=TRIGGER_SERVICE)
41
+ api/agents.py:49
42
+ from lattice_brain.runtime.agent_runtime import AgentRuntime
43
+ runtime = agent_runtime or AgentRuntime(...) # fallback
44
+ /agents/api/runtime/* , POST /agents (start via runtime.start or executor)
45
+ api/chat.py
46
+ from latticeai.services.tool_dispatch import build_agent_runtime
47
+ -> latticeai/core/agent.py:AgentRuntime (별도 state/plan/transcript 머신, single-agent /agent 경로. dispatch_tool 만 공유)
48
+ latticeai/services/tool_dispatch.py:14
49
+ from latticeai.core.agent import AgentRuntime as CoreAgentRuntime
50
+ (tool governance + core single-agent용)
51
+ latticeai/services/platform_runtime.py
52
+ from lattice_brain.runtime.{hooks, multi_agent}
53
+ tests: test_hooks_dispatch.py, test_t7_triggers.py 등에서 직접 import lattice_brain.runtime.*
54
+
55
+ core/tool_registry.py (신규) + services/tool_dispatch.py 가 tool build 주도.
56
+ lattice_brain/runtime 는 multi-agent + hooks + facade 에 집중. core/agent 는 chat/agent 단일 루프.
57
+
58
+ 이 매핑으로 중복 제거 및 wiring 명확화 완료 (feat(Act, automation) 방향).
6
59
  """
7
60
 
8
61
  from __future__ import annotations
@@ -1,5 +1,12 @@
1
1
  """AgentRuntime — the single boundary for agent execution and observability.
2
2
 
3
+ (lattice_brain/runtime/agent_runtime.py)
4
+ 책임: 퍼사드. store/orchestrator/hooks/audit 주입 받아 start/reserve/complete,
5
+ status/health/config/events/replay/stop, pre/post_run hook firing.
6
+ RunExecutor와 /agents 라우터의 유일한 의존 대상.
7
+ 의존: .multi_agent (orchestrator), .hooks, store (WORKSPACE_OS).
8
+ 진입점: app_factory.py:AGENT_RUNTIME (wiring root), api/agents.py, RunExecutor.
9
+
3
10
  Before this module the agent concern was spread across three places: the
4
11
  :class:`~latticeai.core.multi_agent.MultiAgentOrchestrator` (role pipeline),
5
12
  the :class:`~latticeai.services.platform_runtime.PlatformRuntime` (cross-system
@@ -1,5 +1,11 @@
1
1
  """Hooks platform — a persisted registry of lifecycle extension points.
2
2
 
3
+ (lattice_brain/runtime/hooks.py)
4
+ 책임: HookContext/Result, HooksRegistry (persist+order+enable+builtin), dispatch_tool,
5
+ fire_hook (pre/post_run/tool/workflow), BUILTIN_HOOKS 등록.
6
+ 의존성: threading, subprocess (user hooks). dispatch_tool은 tool path 단일화.
7
+ 상위: agent_runtime.py (pre/post_run), tool_dispatch.py, api/tools, core/agent (shared).
8
+
3
9
  Lattice AI runs several behaviours at well-defined points in the agent / tool /
4
10
  workflow lifecycle (audit logging, secret redaction, sensitive-data
5
11
  classification, tool-permission gating, memory snapshots, workflow replay
@@ -1,4 +1,9 @@
1
- """Multi-Agent Runtime 2.1.
1
+ """Multi-Agent Runtime 2.1. (lattice_brain/runtime/multi_agent.py)
2
+
3
+ 책임: 역할 파이프라인 실행, handoff/context_packet/timeline/review/retry 기록 생성,
4
+ OrchestrationContext, default/llm_role_runner, MultiAgentOrchestrator.
5
+ 의존성: 없음 (순수). runner 주입으로 simulation vs llm 분리.
6
+ 상위 호출자: agent_runtime.py (orchestrator_factory), platform_runtime.
2
7
 
3
8
  The runtime remains a small, dependency-injected orchestrator, but v2.1 makes
4
9
  the operational objects first-class: handoffs, context packets, review/retry
@@ -14,7 +19,7 @@ from datetime import datetime
14
19
  from typing import Any, Callable, Dict, List, Optional
15
20
 
16
21
 
17
- MULTI_AGENT_VERSION = "5.3.0"
22
+ MULTI_AGENT_VERSION = "5.4.0"
18
23
 
19
24
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
20
25
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "5.3.0"
3
+ __version__ = "5.4.0"
@@ -51,6 +51,10 @@ class WorkflowImportRequest(BaseModel):
51
51
  data: Dict[str, Any] = {}
52
52
 
53
53
 
54
+ class WorkflowRecipeInstallRequest(BaseModel):
55
+ enabled: bool = False
56
+
57
+
54
58
  def create_workflow_designer_router(
55
59
  *,
56
60
  store,
@@ -254,6 +258,62 @@ def create_workflow_designer_router(
254
258
  return {"running": False, "tick_seconds": None, "armed": []}
255
259
  return trigger_service.describe()
256
260
 
261
+ @router.get("/workflows/api/automation/recipes")
262
+ async def automation_recipes(request: Request):
263
+ require_user(request)
264
+ from latticeai.services.brain_automation import list_brain_automation_recipes
265
+
266
+ return list_brain_automation_recipes()
267
+
268
+ @router.post("/workflows/api/automation/recipes/{recipe_id}")
269
+ async def install_automation_recipe(recipe_id: str, req: WorkflowRecipeInstallRequest, request: Request):
270
+ current_user = require_user(request)
271
+ scope = gate_write(request)
272
+ from latticeai.services.brain_automation import (
273
+ build_brain_automation_workflow,
274
+ find_installed_recipe_workflow,
275
+ )
276
+
277
+ try:
278
+ definition = build_brain_automation_workflow(recipe_id, enabled=req.enabled)
279
+ except KeyError as exc:
280
+ raise HTTPException(status_code=404, detail=f"Automation recipe not found: {recipe_id}") from exc
281
+ existing = find_installed_recipe_workflow(
282
+ store.list_workflows(workspace_id=scope).get("workflows"), recipe_id
283
+ )
284
+ if existing is not None:
285
+ return {
286
+ "workflow": existing,
287
+ "recipe": existing.get("metadata") or definition["metadata"],
288
+ "enabled": bool((existing.get("metadata") or {}).get("automation_state") == "enabled"),
289
+ "already_installed": True,
290
+ }
291
+ errors = validate_definition({"name": definition["name"], "nodes": definition["nodes"]})
292
+ if errors:
293
+ raise HTTPException(status_code=400, detail={"validation_errors": errors})
294
+ workflow = store.create_workflow(
295
+ name=definition["name"],
296
+ steps=[{"action": n.get("type"), "node": n.get("id")} for n in definition["nodes"]],
297
+ nodes=definition["nodes"],
298
+ metadata=definition["metadata"],
299
+ user_email=current_user or None,
300
+ graph=workspace_graph(),
301
+ workspace_id=scope,
302
+ )
303
+ append_audit_event(
304
+ "brain_automation_recipe_installed",
305
+ user_email=current_user,
306
+ workflow_id=workflow["id"],
307
+ recipe_id=recipe_id,
308
+ enabled=bool(req.enabled),
309
+ )
310
+ return {
311
+ "workflow": workflow,
312
+ "recipe": definition["metadata"],
313
+ "enabled": bool(req.enabled),
314
+ "already_installed": False,
315
+ }
316
+
257
317
  @router.get("/workflows/api/runs/{run_id}/replay")
258
318
  async def workflow_run_replay(run_id: str, request: Request):
259
319
  require_user(request)
@@ -1505,6 +1505,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1505
1505
  HOOKS_REGISTRY.register_hook(_trigger_hook_id, TRIGGER_SERVICE.hook_runner())
1506
1506
 
1507
1507
  # Single AgentRuntime boundary over the orchestrator + run store.
1508
+ # (lattice_brain/runtime.agent_runtime.AgentRuntime — see runtime/__init__.py for full dep graph + entry mapping)
1508
1509
  AGENT_RUNTIME = AgentRuntime(
1509
1510
  store=WORKSPACE_OS,
1510
1511
  orchestrator_factory=PLATFORM.build_orchestrator,
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "5.3.0"
14
+ MARKETPLACE_VERSION = "5.4.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
  from typing import Any, Callable, Dict, Iterable, List, Optional
20
20
 
21
21
 
22
- WORKSPACE_OS_VERSION = "5.3.0"
22
+ WORKSPACE_OS_VERSION = "5.4.0"
23
23
 
24
24
  # Workspace types separate single-user Personal workspaces from shared
25
25
  # Organization workspaces. Both keep the same local-first JSON store; the type