ltcai 5.2.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.
Files changed (44) hide show
  1. package/README.md +144 -164
  2. package/docs/CHANGELOG.md +63 -0
  3. package/docs/DEVELOPMENT.md +99 -0
  4. package/docs/LEGACY_COMPATIBILITY.md +55 -0
  5. package/docs/WHY_LATTICE.md +4 -3
  6. package/frontend/src/App.tsx +8 -2
  7. package/frontend/src/api/client.ts +2 -0
  8. package/frontend/src/components/FirstRunGuide.tsx +5 -5
  9. package/frontend/src/components/ProductFlow.tsx +1 -1
  10. package/frontend/src/i18n.ts +40 -40
  11. package/frontend/src/pages/Act.tsx +82 -1
  12. package/frontend/src/pages/Library.tsx +18 -6
  13. package/lattice_brain/__init__.py +1 -1
  14. package/lattice_brain/archive.py +12 -0
  15. package/lattice_brain/portability.py +14 -0
  16. package/lattice_brain/runtime/__init__.py +53 -0
  17. package/lattice_brain/runtime/agent_runtime.py +7 -0
  18. package/lattice_brain/runtime/hooks.py +6 -0
  19. package/lattice_brain/runtime/multi_agent.py +7 -2
  20. package/latticeai/__init__.py +1 -1
  21. package/latticeai/api/workflow_designer.py +60 -0
  22. package/latticeai/app_factory.py +5 -78
  23. package/latticeai/core/marketplace.py +1 -1
  24. package/latticeai/core/workspace_os.py +1 -1
  25. package/latticeai/runtime/__init__.py +2 -0
  26. package/latticeai/runtime/brain_runtime.py +41 -0
  27. package/latticeai/runtime/config_runtime.py +36 -0
  28. package/latticeai/runtime/security_runtime.py +27 -0
  29. package/latticeai/services/brain_automation.py +214 -0
  30. package/latticeai/services/model_capability_registry.py +2 -3
  31. package/latticeai/services/triggers.py +61 -4
  32. package/package.json +2 -2
  33. package/scripts/verify_hf_model_registry.py +1 -3
  34. package/src-tauri/Cargo.lock +1 -1
  35. package/src-tauri/Cargo.toml +1 -1
  36. package/src-tauri/tauri.conf.json +1 -1
  37. package/static/app/asset-manifest.json +5 -5
  38. package/static/app/assets/index-C7vzwUjU.js +16 -0
  39. package/static/app/assets/index-C7vzwUjU.js.map +1 -0
  40. package/static/app/assets/index-HN4f2wbe.css +2 -0
  41. package/static/app/index.html +2 -2
  42. package/static/app/assets/index-CQmHhk8Q.css +0 -2
  43. package/static/app/assets/index-DsnfomFs.js +0 -16
  44. package/static/app/assets/index-DsnfomFs.js.map +0 -1
@@ -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.2.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.2.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)
@@ -17,90 +17,16 @@ from __future__ import annotations
17
17
  import threading
18
18
  from typing import TYPE_CHECKING, Any, Dict, List, Optional
19
19
 
20
+ from latticeai.runtime.brain_runtime import build_brain_runtime
21
+ from latticeai.runtime.config_runtime import build_config_runtime
22
+ from latticeai.runtime.security_runtime import build_security_runtime
23
+
20
24
  if TYPE_CHECKING: # imports for annotations only — keep module import light
21
25
  from fastapi import FastAPI
22
26
 
23
27
  from latticeai.core.config import Config
24
28
 
25
29
 
26
- def build_config_runtime(config: "Optional[Config]" = None) -> Dict[str, Any]:
27
- """Build the app configuration runtime without importing heavy model code."""
28
-
29
- from latticeai.core.config import Config
30
-
31
- cfg = config if config is not None else Config.from_env()
32
- return {
33
- "CONFIG": cfg,
34
- "APP_MODE": cfg.app_mode,
35
- "IS_PUBLIC_MODE": cfg.is_public,
36
- "DEFAULT_HOST": cfg.host,
37
- "DEFAULT_PORT": cfg.port,
38
- "NETWORK_EXPOSED": cfg.network_exposed,
39
- "ENABLE_TELEGRAM": cfg.enable_telegram,
40
- "ENABLE_GRAPH": cfg.enable_graph,
41
- "AUTOLOAD_MODELS": cfg.autoload_models,
42
- "MODEL_IDLE_UNLOAD_SECONDS": cfg.model_idle_unload_seconds,
43
- "ALLOW_LOCAL_MODELS": cfg.allow_local_models,
44
- "REQUIRE_AUTH": cfg.require_auth,
45
- "ALLOW_PLAINTEXT_API_KEYS": cfg.allow_plaintext_api_keys,
46
- "CORS_ALLOW_NETWORK": cfg.cors_allow_network,
47
- "CORS_EXTRA_ORIGINS": cfg.cors_extra_origins,
48
- "PUBLIC_MODEL": cfg.public_model,
49
- "LOCAL_MODEL": cfg.local_model,
50
- "LOCAL_DRAFT_MODEL": cfg.local_draft_model,
51
- }
52
-
53
-
54
- def build_security_runtime(config: "Config") -> Dict[str, Any]:
55
- """Build auth/security-derived runtime settings from the central config."""
56
-
57
- from latticeai.core.security import configure_trusted_proxies
58
-
59
- configure_trusted_proxies(config.trusted_proxies)
60
- return {
61
- "SSO_DISCOVERY_URL": config.sso_discovery_url,
62
- "SSO_CLIENT_ID": config.sso_client_id,
63
- "SSO_CLIENT_SECRET": config.sso_client_secret,
64
- "SSO_REDIRECT_URI": config.sso_redirect_uri,
65
- "SSO_PROVIDER_NAME": config.sso_provider_name,
66
- "RATE_LIMIT_ENABLED": config.rate_limit_enabled,
67
- "OPEN_REGISTRATION": config.open_registration,
68
- "INVITE_CODE": config.invite_code,
69
- "INVITE_GATE_ENABLED": config.invite_gate_enabled,
70
- }
71
-
72
-
73
- def build_brain_runtime(
74
- *,
75
- data_dir: Any,
76
- history_file: Any,
77
- enable_graph: bool,
78
- embedder: Any,
79
- storage_engine: Any,
80
- ) -> Dict[str, Any]:
81
- """Construct Brain Core storage/conversation primitives behind one seam."""
82
-
83
- from lattice_brain import BrainCore, ConversationStore
84
-
85
- brain_core = BrainCore.from_paths(
86
- data_dir,
87
- embedder=embedder.provider,
88
- storage_engine=storage_engine,
89
- ) if enable_graph else None
90
- knowledge_graph = brain_core.knowledge if brain_core is not None else None
91
- conversations = (
92
- brain_core.conversations
93
- if brain_core is not None
94
- else ConversationStore(data_dir / "knowledge_graph.sqlite")
95
- )
96
- conversations.import_legacy_json(history_file)
97
- return {
98
- "BRAIN_CORE": brain_core,
99
- "KNOWLEDGE_GRAPH": knowledge_graph,
100
- "CONVERSATIONS": conversations,
101
- }
102
-
103
-
104
30
  def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
105
31
  """The legacy ``server_app`` assembly, moved verbatim into function scope.
106
32
 
@@ -1579,6 +1505,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1579
1505
  HOOKS_REGISTRY.register_hook(_trigger_hook_id, TRIGGER_SERVICE.hook_runner())
1580
1506
 
1581
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)
1582
1509
  AGENT_RUNTIME = AgentRuntime(
1583
1510
  store=WORKSPACE_OS,
1584
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.2.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.2.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
@@ -0,0 +1,2 @@
1
+ """Runtime assembly seams for the Lattice AI application factory."""
2
+
@@ -0,0 +1,41 @@
1
+ """Brain Core runtime assembly for app startup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict
6
+
7
+
8
+ def build_brain_runtime(
9
+ *,
10
+ data_dir: Any,
11
+ history_file: Any,
12
+ enable_graph: bool,
13
+ embedder: Any,
14
+ storage_engine: Any,
15
+ ) -> Dict[str, Any]:
16
+ """Construct Brain Core storage/conversation primitives behind one seam."""
17
+
18
+ from lattice_brain import BrainCore, ConversationStore
19
+
20
+ brain_core = (
21
+ BrainCore.from_paths(
22
+ data_dir,
23
+ embedder=embedder.provider,
24
+ storage_engine=storage_engine,
25
+ )
26
+ if enable_graph
27
+ else None
28
+ )
29
+ knowledge_graph = brain_core.knowledge if brain_core is not None else None
30
+ conversations = (
31
+ brain_core.conversations
32
+ if brain_core is not None
33
+ else ConversationStore(data_dir / "knowledge_graph.sqlite")
34
+ )
35
+ conversations.import_legacy_json(history_file)
36
+ return {
37
+ "BRAIN_CORE": brain_core,
38
+ "KNOWLEDGE_GRAPH": knowledge_graph,
39
+ "CONVERSATIONS": conversations,
40
+ }
41
+
@@ -0,0 +1,36 @@
1
+ """Configuration runtime assembly for the FastAPI composition root."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Dict, Optional
6
+
7
+ if TYPE_CHECKING:
8
+ from latticeai.core.config import Config
9
+
10
+
11
+ def build_config_runtime(config: "Optional[Config]" = None) -> Dict[str, Any]:
12
+ """Build app configuration values without importing model/runtime code."""
13
+
14
+ from latticeai.core.config import Config
15
+
16
+ cfg = config if config is not None else Config.from_env()
17
+ return {
18
+ "CONFIG": cfg,
19
+ "APP_MODE": cfg.app_mode,
20
+ "IS_PUBLIC_MODE": cfg.is_public,
21
+ "DEFAULT_HOST": cfg.host,
22
+ "DEFAULT_PORT": cfg.port,
23
+ "NETWORK_EXPOSED": cfg.network_exposed,
24
+ "ENABLE_TELEGRAM": cfg.enable_telegram,
25
+ "ENABLE_GRAPH": cfg.enable_graph,
26
+ "AUTOLOAD_MODELS": cfg.autoload_models,
27
+ "MODEL_IDLE_UNLOAD_SECONDS": cfg.model_idle_unload_seconds,
28
+ "ALLOW_LOCAL_MODELS": cfg.allow_local_models,
29
+ "REQUIRE_AUTH": cfg.require_auth,
30
+ "ALLOW_PLAINTEXT_API_KEYS": cfg.allow_plaintext_api_keys,
31
+ "CORS_ALLOW_NETWORK": cfg.cors_allow_network,
32
+ "CORS_EXTRA_ORIGINS": cfg.cors_extra_origins,
33
+ "PUBLIC_MODEL": cfg.public_model,
34
+ "LOCAL_MODEL": cfg.local_model,
35
+ "LOCAL_DRAFT_MODEL": cfg.local_draft_model,
36
+ }
@@ -0,0 +1,27 @@
1
+ """Security runtime assembly for app startup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Dict
6
+
7
+ if TYPE_CHECKING:
8
+ from latticeai.core.config import Config
9
+
10
+
11
+ def build_security_runtime(config: "Config") -> Dict[str, Any]:
12
+ """Build auth/security-derived runtime settings from the central config."""
13
+
14
+ from latticeai.core.security import configure_trusted_proxies
15
+
16
+ configure_trusted_proxies(config.trusted_proxies)
17
+ return {
18
+ "SSO_DISCOVERY_URL": config.sso_discovery_url,
19
+ "SSO_CLIENT_ID": config.sso_client_id,
20
+ "SSO_CLIENT_SECRET": config.sso_client_secret,
21
+ "SSO_REDIRECT_URI": config.sso_redirect_uri,
22
+ "SSO_PROVIDER_NAME": config.sso_provider_name,
23
+ "RATE_LIMIT_ENABLED": config.rate_limit_enabled,
24
+ "OPEN_REGISTRATION": config.open_registration,
25
+ "INVITE_CODE": config.invite_code,
26
+ "INVITE_GATE_ENABLED": config.invite_gate_enabled,
27
+ }
@@ -0,0 +1,214 @@
1
+ """Consent-first Brain automation recipes.
2
+
3
+ The recipes here are product-level starter workflows, not hidden background
4
+ jobs. Installing one creates a disabled draft workflow so the user can inspect
5
+ and enable it deliberately.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from copy import deepcopy
11
+ from dataclasses import dataclass
12
+ from typing import Any, Dict, List
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class BrainAutomationRecipe:
17
+ id: str
18
+ name: str
19
+ summary: str
20
+ user_value: str
21
+ cadence: str
22
+ trigger: Dict[str, Any]
23
+ prompt: str
24
+ creates: List[str]
25
+
26
+ def as_dict(self) -> Dict[str, Any]:
27
+ return {
28
+ "id": self.id,
29
+ "name": self.name,
30
+ "summary": self.summary,
31
+ "user_value": self.user_value,
32
+ "cadence": self.cadence,
33
+ "trigger": deepcopy(self.trigger),
34
+ "creates": list(self.creates),
35
+ "consent": {
36
+ "default_state": "draft_disabled",
37
+ "local_only": True,
38
+ "external_actions": False,
39
+ "requires_user_enable": True,
40
+ "review_before_run": True,
41
+ },
42
+ }
43
+
44
+
45
+ _RECIPES: List[BrainAutomationRecipe] = [
46
+ BrainAutomationRecipe(
47
+ id="daily-memory-digest",
48
+ name="Daily Memory Digest",
49
+ summary="Collects the day's new memories into a short review draft.",
50
+ user_value="Users see what the Brain kept today without searching through chats.",
51
+ cadence="daily",
52
+ trigger={"trigger": "interval", "interval_seconds": 86_400},
53
+ prompt=(
54
+ "Review today's new Brain memories and draft a concise digest with "
55
+ "important decisions, unresolved questions, and suggested next actions. "
56
+ "Do not contact external services."
57
+ ),
58
+ creates=["memory digest", "decision summary", "next-action suggestions"],
59
+ ),
60
+ BrainAutomationRecipe(
61
+ id="weekly-project-review",
62
+ name="Weekly Project Review",
63
+ summary="Turns project context into a weekly checkpoint draft.",
64
+ user_value="Users can restart a project without explaining the week again.",
65
+ cadence="weekly",
66
+ trigger={"trigger": "interval", "interval_seconds": 604_800},
67
+ prompt=(
68
+ "Review this workspace's recent memories, workflow runs, and decisions. "
69
+ "Draft a project checkpoint with progress, risks, blockers, and next steps. "
70
+ "Keep it local and ask before any external action."
71
+ ),
72
+ creates=["project checkpoint", "risk list", "next-week plan"],
73
+ ),
74
+ BrainAutomationRecipe(
75
+ id="follow-up-radar",
76
+ name="Follow-up Radar",
77
+ summary="Looks for follow-up candidates when new knowledge enters the Brain.",
78
+ user_value="Users get gentle reminders for loose ends without a noisy task system.",
79
+ cadence="when new memory is saved",
80
+ trigger={"trigger": "brain_event"},
81
+ prompt=(
82
+ "Inspect the new Brain memory for follow-up signals such as decisions, "
83
+ "promises, deadlines, unresolved questions, or 'later' language. "
84
+ "Return suggestions only; do not create tasks without approval."
85
+ ),
86
+ creates=["follow-up suggestions", "open-question list", "approval-ready task drafts"],
87
+ ),
88
+ ]
89
+
90
+ _RECIPE_BY_ID = {recipe.id: recipe for recipe in _RECIPES}
91
+
92
+
93
+ def list_brain_automation_recipes() -> Dict[str, Any]:
94
+ """Return user-facing, consent-first automation recipe metadata."""
95
+ return {
96
+ "recipes": [recipe.as_dict() for recipe in _RECIPES],
97
+ "principles": {
98
+ "local_first": True,
99
+ "drafts_before_automation": True,
100
+ "no_external_actions_without_consent": True,
101
+ },
102
+ }
103
+
104
+
105
+ def find_installed_recipe_workflow(
106
+ workflows: Any, recipe_id: str
107
+ ) -> Dict[str, Any] | None:
108
+ """Return an existing draft installed from ``recipe_id``, if any.
109
+
110
+ Installing a recipe is idempotent: clicking "Create reviewable draft" twice
111
+ should surface the existing draft instead of accumulating duplicates. We
112
+ match on the ``brain_automation_recipe`` provenance metadata stamped by
113
+ :func:`build_brain_automation_workflow`.
114
+ """
115
+ for workflow in workflows or []:
116
+ metadata = (workflow or {}).get("metadata") or {}
117
+ if (
118
+ metadata.get("created_from") == "brain_automation_recipe"
119
+ and metadata.get("recipe_id") == recipe_id
120
+ ):
121
+ return workflow
122
+ return None
123
+
124
+
125
+ def build_brain_automation_workflow(recipe_id: str, *, enabled: bool = False) -> Dict[str, Any]:
126
+ """Build a workflow definition for a recipe.
127
+
128
+ ``enabled`` defaults to ``False`` so installing a recipe creates an
129
+ inspectable draft. The trigger service treats explicit ``enabled: false`` as
130
+ disarmed, while legacy workflows without the field keep their behavior.
131
+ """
132
+ recipe = _RECIPE_BY_ID.get(recipe_id)
133
+ if recipe is None:
134
+ raise KeyError(recipe_id)
135
+
136
+ trigger_config = {
137
+ **deepcopy(recipe.trigger),
138
+ "enabled": bool(enabled),
139
+ "consent_required": True,
140
+ "local_only": True,
141
+ "external_actions": False,
142
+ }
143
+ return {
144
+ "name": recipe.name,
145
+ "nodes": [
146
+ {
147
+ "id": "trigger",
148
+ "type": "trigger",
149
+ "name": "User-enabled schedule" if recipe.trigger["trigger"] == "interval" else "New Brain memory",
150
+ "config": trigger_config,
151
+ "next": "draft",
152
+ },
153
+ {
154
+ "id": "draft",
155
+ "type": "agent",
156
+ "name": "Draft Brain review",
157
+ "config": {
158
+ "agent": "agent:planner",
159
+ "prompt": recipe.prompt,
160
+ "mode": "draft",
161
+ "local_only": True,
162
+ "external_actions": False,
163
+ "requires_review": True,
164
+ },
165
+ "next": "output",
166
+ },
167
+ {
168
+ "id": "output",
169
+ "type": "output",
170
+ "name": "Review before saving",
171
+ "config": {
172
+ "value": "Draft ready for review. Save, edit, or discard it before it becomes durable memory.",
173
+ },
174
+ "next": None,
175
+ },
176
+ ],
177
+ "metadata": {
178
+ "created_from": "brain_automation_recipe",
179
+ "recipe_id": recipe.id,
180
+ "recipe_summary": recipe.summary,
181
+ "recipe_user_value": recipe.user_value,
182
+ "automation_state": "enabled" if enabled else "draft_disabled",
183
+ "local_only": True,
184
+ "external_actions": False,
185
+ "requires_user_enable": not enabled,
186
+ "creates": list(recipe.creates),
187
+ },
188
+ }
189
+
190
+
191
+ # === A방향 (Act/automation + BrainAutomationPanel) E2E 시나리오 초안 ===
192
+ # (backend 인터페이스 list_brain_automation_recipes / find_installed... / build_... 완료 후 즉시 작성)
193
+ # 1. Recipe 목록 노출: frontend BrainAutomationPanel이 list_brain_automation_recipes() 호출 → recipes + consent metadata 표시.
194
+ # 2. "Create reviewable draft" 클릭:
195
+ # - build_brain_automation_workflow(recipe_id, enabled=False) 로 draft 생성 (metadata.recipe_id + created_from=brain_automation_recipe)
196
+ # - find_installed_recipe_workflow 로 사전 dedup 체크 → 이미 있으면 기존 반환, UI disabled + "✓ Reviewable draft ready" 피드백.
197
+ # - 생성된 workflow는 automation_state="draft_disabled", trigger.enabled=False → TriggerService 무시.
198
+ # 3. Dedup guard (UI + backend): metadata.recipe_id + created_from 기준. 중복 클릭 가드 (no-dup) 로 double submit 방지.
199
+ # 4. User가 draft를 review/수정 후 enabled=True 로 전환 → TriggerService._triggered_workflows 에서 enabled True인 것만 arm.
200
+ # 5. Interval trigger E2E:
201
+ # - reconcile_missed() : 다운타임 중 missed → "skipped" 이벤트 기록 (catch-up storm 없음).
202
+ # - tick_intervals() : last_fired_at + interval + last_attempt_at dedup 가드 (10s cooldown) 로 중복 실행 방지.
203
+ # - LATTICE_TZ 환경변수: describe()에 "tz" 노출, 이벤트 at 값은 epoch이지만 클라이언트가 LATTICE_TZ로 현지화.
204
+ # 6. Brain event trigger E2E: kg_ingest.* post_tool hook → on_brain_event → matching source_type 필터 → _fire (dedup 5s).
205
+ # 7. Failure degraded:
206
+ # - _fire 에서 run_workflow 예외 → _record_fire_outcome → consecutive_failures++ , describe() "status":"degraded" ( >=3 ).
207
+ # - 성공 시 reset. (실행 내부 실패는 workflow run record에 남음, scheduler는 launch health만).
208
+ # 8. Run provenance: fired run의 inputs["__trigger__"] = {"type": "interval"|"brain_event", ...} 로 감사/디버그 가능.
209
+ # 9. Consent-first: draft_disabled 기본, user enable 전까지 절대 실행 안 됨. "review_before_run": True.
210
+ # 10. End-to-end Act: draft → enable → trigger fire → agent:planner "Draft Brain review" 노드 → output (requires_review) → user review → save or discard.
211
+ #
212
+ # 다음: 실제 API (e.g. POST /brain/automation/install-draft) 가 UI에서 호출되면 위 시나리오에 대한 통합 테스트 + RunExecutor 경로 검증 즉시 추가.
213
+ # 현재 상태: backend recipe 인터페이스 + TriggerService edge 하드닝 + AgentRuntime wiring 완료. UI (App.tsx + styles) + test_brain_automation.py 는 별도 완료 보고됨.
214
+
@@ -20,9 +20,8 @@ are never auto-fetched by the verifier. Large models (>12GB) explicitly note
20
20
 
21
21
  from __future__ import annotations
22
22
 
23
- import datetime as _dt
24
- from dataclasses import dataclass, asdict, field
25
- from typing import Any, Dict, List, Optional, Tuple
23
+ from dataclasses import dataclass, field
24
+ from typing import Any, Dict, List, Optional
26
25
 
27
26
 
28
27
  @dataclass(frozen=True)