ltcai 8.8.0 → 9.0.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 (111) hide show
  1. package/README.md +33 -37
  2. package/auto_setup.py +84 -70
  3. package/docs/CHANGELOG.md +113 -237
  4. package/docs/CODE_REVIEW_2026-07-06.md +764 -0
  5. package/docs/COMMUNITY_AND_PLUGINS.md +3 -3
  6. package/docs/DEVELOPMENT.md +10 -10
  7. package/docs/LEGACY_COMPATIBILITY.md +1 -1
  8. package/docs/ONBOARDING.md +2 -2
  9. package/docs/PRODUCT_DIRECTION_REVIEW.md +3 -2
  10. package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
  11. package/docs/TRUST_MODEL.md +5 -1
  12. package/docs/WHY_LATTICE.md +4 -3
  13. package/docs/architecture.md +4 -0
  14. package/docs/kg-schema.md +1 -1
  15. package/lattice_brain/__init__.py +1 -1
  16. package/lattice_brain/archive.py +4 -9
  17. package/lattice_brain/conversations.py +156 -21
  18. package/lattice_brain/embeddings.py +38 -2
  19. package/lattice_brain/graph/_kg_common.py +39 -496
  20. package/lattice_brain/graph/_kg_constants.py +243 -0
  21. package/lattice_brain/graph/_kg_fsutil.py +297 -0
  22. package/lattice_brain/graph/discovery.py +0 -948
  23. package/lattice_brain/graph/discovery_index.py +972 -0
  24. package/lattice_brain/graph/json_utils.py +25 -0
  25. package/lattice_brain/graph/retrieval.py +66 -597
  26. package/lattice_brain/graph/retrieval_docgen.py +210 -0
  27. package/lattice_brain/graph/retrieval_vector.py +460 -0
  28. package/lattice_brain/graph/runtime.py +16 -0
  29. package/lattice_brain/graph/store.py +6 -0
  30. package/lattice_brain/ingestion.py +68 -0
  31. package/lattice_brain/portability.py +1 -9
  32. package/lattice_brain/quality.py +98 -4
  33. package/lattice_brain/runtime/agent_runtime.py +166 -0
  34. package/lattice_brain/runtime/multi_agent.py +1 -1
  35. package/lattice_brain/utils.py +28 -0
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/chat.py +368 -418
  38. package/latticeai/api/chat_helpers.py +227 -0
  39. package/latticeai/api/computer_use.py +149 -31
  40. package/latticeai/api/marketplace.py +11 -0
  41. package/latticeai/api/mcp.py +3 -2
  42. package/latticeai/api/models.py +4 -1
  43. package/latticeai/api/permissions.py +72 -33
  44. package/latticeai/api/setup.py +17 -2
  45. package/latticeai/api/tools.py +105 -62
  46. package/latticeai/app_factory.py +101 -296
  47. package/latticeai/core/agent.py +25 -7
  48. package/latticeai/core/io_utils.py +37 -0
  49. package/latticeai/core/legacy_compatibility.py +1 -1
  50. package/latticeai/core/local_embeddings.py +2 -4
  51. package/latticeai/core/marketplace.py +33 -2
  52. package/latticeai/core/mcp_catalog.py +450 -0
  53. package/latticeai/core/mcp_registry.py +2 -441
  54. package/latticeai/core/sessions.py +11 -3
  55. package/latticeai/core/tool_registry.py +15 -4
  56. package/latticeai/core/users.py +4 -9
  57. package/latticeai/core/workspace_os.py +1 -1
  58. package/latticeai/core/workspace_os_utils.py +3 -17
  59. package/latticeai/integrations/telegram_bot.py +7 -2
  60. package/latticeai/models/model_providers.py +111 -0
  61. package/latticeai/models/router.py +58 -136
  62. package/latticeai/runtime/audit_runtime.py +27 -16
  63. package/latticeai/runtime/automation_runtime.py +9 -0
  64. package/latticeai/runtime/bootstrap.py +1 -1
  65. package/latticeai/runtime/history_runtime.py +163 -0
  66. package/latticeai/runtime/namespace_runtime.py +173 -0
  67. package/latticeai/runtime/network_config_runtime.py +56 -0
  68. package/latticeai/runtime/sso_config_runtime.py +128 -0
  69. package/latticeai/runtime/user_key_runtime.py +106 -0
  70. package/latticeai/services/app_context.py +1 -0
  71. package/latticeai/services/architecture_readiness.py +2 -2
  72. package/latticeai/services/memory_service.py +213 -0
  73. package/latticeai/services/model_engines.py +79 -12
  74. package/latticeai/services/model_runtime.py +24 -4
  75. package/latticeai/services/platform_runtime.py +9 -1
  76. package/latticeai/services/process_audit.py +208 -0
  77. package/latticeai/services/product_readiness.py +11 -11
  78. package/latticeai/services/review_queue.py +64 -11
  79. package/latticeai/services/run_executor.py +21 -0
  80. package/latticeai/services/search_service.py +106 -30
  81. package/latticeai/services/setup_detection.py +80 -0
  82. package/latticeai/services/tool_dispatch.py +66 -0
  83. package/latticeai/services/workspace_service.py +15 -0
  84. package/package.json +1 -1
  85. package/scripts/check_i18n_literals.mjs +20 -8
  86. package/scripts/i18n_literal_allowlist.json +34 -0
  87. package/scripts/lint_frontend.mjs +6 -2
  88. package/setup_wizard.py +196 -74
  89. package/src-tauri/Cargo.lock +1 -1
  90. package/src-tauri/Cargo.toml +1 -1
  91. package/src-tauri/tauri.conf.json +1 -1
  92. package/static/app/asset-manifest.json +11 -11
  93. package/static/app/assets/{Act-C7K9wsO9.js → Act-21lIXx2E.js} +1 -1
  94. package/static/app/assets/Brain-BqUd5UJJ.js +321 -0
  95. package/static/app/assets/{Capture-B3V4_5Xp.js → Capture-BA7Z2Q1u.js} +1 -1
  96. package/static/app/assets/{Library-Cgj-EF50.js → Library-bFMtyni3.js} +1 -1
  97. package/static/app/assets/System-K6krGCqn.js +1 -0
  98. package/static/app/assets/index-C4R3ws30.js +17 -0
  99. package/static/app/assets/index-ChSeOB02.css +2 -0
  100. package/static/app/assets/primitives-sQU3it5I.js +1 -0
  101. package/static/app/assets/{textarea-CVQkN2Tk.js → textarea-DK3Fd_lR.js} +1 -1
  102. package/static/app/index.html +2 -2
  103. package/static/css/tokens.css +4 -2
  104. package/static/sw.js +1 -1
  105. package/tools/local_files.py +6 -0
  106. package/latticeai/runtime/sso_runtime.py +0 -52
  107. package/static/app/assets/Brain-I1OSzxJu.js +0 -321
  108. package/static/app/assets/System-D1Lkei3I.js +0 -1
  109. package/static/app/assets/index--P0ksosz.js +0 -17
  110. package/static/app/assets/index-DCh5AoXt.css +0 -2
  111. package/static/app/assets/primitives-BLqaKk5g.js +0 -1
@@ -271,6 +271,7 @@ class MemoryService:
271
271
  memory_count = len(memory_ids) + len(snaps) + len(convs)
272
272
  return {
273
273
  "sources": sources,
274
+ "recent_memories": self._manager_recent_memories([*ws_mem, *project_mem], limit=8),
274
275
  "tiers": list(TIERS),
275
276
  "usage": {"total_items": total_items, "total_bytes": total_bytes, "sources": len(sources)},
276
277
  "brain_readiness": self._brain_readiness(
@@ -284,6 +285,23 @@ class MemoryService:
284
285
  "generated_at": _now(),
285
286
  }
286
287
 
288
+ @staticmethod
289
+ def _manager_recent_memories(memories: List[Dict[str, Any]], *, limit: int = 8) -> List[Dict[str, Any]]:
290
+ rows: List[Dict[str, Any]] = []
291
+ for item in memories[: max(1, limit)]:
292
+ metadata = item.get("metadata") if isinstance(item.get("metadata"), dict) else {}
293
+ rows.append({
294
+ "id": item.get("id") or "",
295
+ "kind": item.get("kind") or "memory",
296
+ "content": str(item.get("content") or "")[:320],
297
+ "tags": item.get("tags") if isinstance(item.get("tags"), list) else [],
298
+ "metadata": metadata,
299
+ "workspace_id": item.get("workspace_id") or "personal",
300
+ "created_at": item.get("created_at"),
301
+ "updated_at": item.get("updated_at"),
302
+ })
303
+ return rows
304
+
287
305
  def brain_quality_summary(self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None) -> Dict[str, Any]:
288
306
  """Return the backend-owned Brain readiness signal for API consumers."""
289
307
  return self.manager(user_email=user_email, workspace_id=workspace_id)["brain_readiness"]
@@ -338,6 +356,22 @@ class MemoryService:
338
356
  has_recall=bool(recall_items),
339
357
  graph_concepts=graph_concepts,
340
358
  )
359
+ suggested_questions = self._brain_brief_suggested_questions(
360
+ focus=focus,
361
+ has_durable_evidence=bool(proofs.get("has_durable_evidence")),
362
+ has_recall=bool(recall_items),
363
+ graph_concepts=graph_concepts,
364
+ conversations=conversations,
365
+ )
366
+ proactive_actions = self._brain_brief_proactive_actions(
367
+ focus=focus,
368
+ state=state,
369
+ has_durable_evidence=bool(proofs.get("has_durable_evidence")),
370
+ has_recall=bool(recall_items),
371
+ graph_concepts=graph_concepts,
372
+ vector_items=vector_items,
373
+ healthy_sources=healthy_sources,
374
+ )
341
375
  return {
342
376
  "status": state,
343
377
  "score": int(readiness.get("score") or 0),
@@ -345,6 +379,8 @@ class MemoryService:
345
379
  "body_key": f"brain.brief.body.{state if state in {'quiet', 'forming', 'alive'} else 'quiet'}",
346
380
  "focus": focus,
347
381
  "next_actions": actions,
382
+ "suggested_questions": suggested_questions,
383
+ "proactive_actions": proactive_actions,
348
384
  "evidence": [
349
385
  {
350
386
  "id": "durable",
@@ -501,6 +537,183 @@ class MemoryService:
501
537
  })
502
538
  return sorted(actions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
503
539
 
540
+ @staticmethod
541
+ def _brain_brief_proactive_actions(
542
+ *,
543
+ focus: Dict[str, Any],
544
+ state: str,
545
+ has_durable_evidence: bool,
546
+ has_recall: bool,
547
+ graph_concepts: int,
548
+ vector_items: int,
549
+ healthy_sources: int,
550
+ ) -> List[Dict[str, Any]]:
551
+ """Return concrete, one-click actions Brain can proactively suggest."""
552
+ focus_title = str(focus.get("title") or "").strip() or "Brain"
553
+ focus_detail = str(focus.get("detail") or "").strip()
554
+ actions: List[Dict[str, Any]] = []
555
+ if not has_durable_evidence:
556
+ actions.append({
557
+ "id": "proactive_add_source",
558
+ "intent": "route",
559
+ "label_key": "brain.proactive.addSource.label",
560
+ "detail_key": "brain.proactive.addSource.detail",
561
+ "route": "/capture",
562
+ "prompt": "Add a useful source to my Brain and explain what it learned.",
563
+ "priority": 100,
564
+ })
565
+ actions.append({
566
+ "id": "proactive_seed_memory",
567
+ "intent": "ask",
568
+ "label_key": "brain.proactive.seed.label",
569
+ "detail_key": "brain.proactive.seed.detail",
570
+ "prompt": "Help me seed my Brain with the most useful personal context to remember.",
571
+ "priority": 90,
572
+ })
573
+ return actions
574
+
575
+ if has_recall:
576
+ actions.append({
577
+ "id": "proactive_evidence_review",
578
+ "intent": "ask",
579
+ "label_key": "brain.proactive.evidence.label",
580
+ "detail_key": "brain.proactive.evidence.detail",
581
+ "prompt": (
582
+ f"Review the evidence Brain has for {focus_title}. "
583
+ "Separate confirmed facts, weak signals, contradictions, and next checks."
584
+ ),
585
+ "priority": 100,
586
+ "context": {"focus": focus_title, "detail": focus_detail},
587
+ })
588
+ actions.append({
589
+ "id": "proactive_delegate",
590
+ "intent": "delegate",
591
+ "label_key": "brain.proactive.delegate.label",
592
+ "detail_key": "brain.proactive.delegate.detail",
593
+ "prompt": (
594
+ f"Turn {focus_title} into an execution plan, verify the known context, "
595
+ "and return concrete next steps with risks."
596
+ ),
597
+ "priority": 95,
598
+ "context": {"focus": focus_title, "detail": focus_detail},
599
+ })
600
+ actions.append({
601
+ "id": "proactive_review_draft",
602
+ "intent": "review",
603
+ "label_key": "brain.proactive.review.label",
604
+ "detail_key": "brain.proactive.review.detail",
605
+ "prompt": (
606
+ f"Create a reviewable task from Brain's current focus: {focus_title}. "
607
+ f"{focus_detail[:240]}"
608
+ ).strip(),
609
+ "priority": 90,
610
+ "context": {"focus": focus_title, "detail": focus_detail},
611
+ })
612
+
613
+ if graph_concepts > 0:
614
+ actions.append({
615
+ "id": "proactive_map_connections",
616
+ "intent": "route",
617
+ "label_key": "brain.proactive.map.label",
618
+ "detail_key": "brain.proactive.map.detail",
619
+ "route": "/knowledge-graph",
620
+ "prompt": f"Map the strongest Knowledge Graph connections around {focus_title}.",
621
+ "priority": 82,
622
+ "context": {"focus": focus_title, "graph_concepts": graph_concepts},
623
+ })
624
+
625
+ if state == "alive" and vector_items > 0 and healthy_sources > 0:
626
+ actions.append({
627
+ "id": "proactive_weekly_brief",
628
+ "intent": "review",
629
+ "label_key": "brain.proactive.weekly.label",
630
+ "detail_key": "brain.proactive.weekly.detail",
631
+ "prompt": (
632
+ "Prepare a weekly Brain review: what changed, what decisions are pending, "
633
+ "what should be delegated, and what evidence is stale."
634
+ ),
635
+ "priority": 78,
636
+ "context": {"vector_items": vector_items, "healthy_sources": healthy_sources},
637
+ })
638
+
639
+ return sorted(actions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
640
+
641
+ @staticmethod
642
+ def _brain_brief_suggested_questions(
643
+ *,
644
+ focus: Dict[str, Any],
645
+ has_durable_evidence: bool,
646
+ has_recall: bool,
647
+ graph_concepts: int,
648
+ conversations: int,
649
+ ) -> List[Dict[str, Any]]:
650
+ """Return reusable, localized UI prompt descriptors for the Brain home."""
651
+ focus_title = str(focus.get("title") or "").strip()
652
+ focus_kind = str(focus.get("kind") or "empty")
653
+ questions: List[Dict[str, Any]] = []
654
+
655
+ if not has_durable_evidence or focus_kind == "empty":
656
+ questions.extend([
657
+ {
658
+ "id": "start_brain",
659
+ "label_key": "brain.suggestion.start.label",
660
+ "detail_key": "brain.suggestion.start.detail",
661
+ "prompt_key": "brain.suggestion.start.prompt",
662
+ "params": {},
663
+ "priority": 10,
664
+ },
665
+ {
666
+ "id": "add_context",
667
+ "label_key": "brain.suggestion.context.label",
668
+ "detail_key": "brain.suggestion.context.detail",
669
+ "prompt_key": "brain.suggestion.context.prompt",
670
+ "params": {},
671
+ "priority": 9,
672
+ },
673
+ ])
674
+ return questions
675
+
676
+ questions.append({
677
+ "id": "focus_next",
678
+ "label_key": "brain.suggestion.focus.label",
679
+ "detail_key": "brain.suggestion.focus.detail",
680
+ "prompt_key": "brain.suggestion.focus.prompt",
681
+ "params": {"focus": focus_title or "Brain"},
682
+ "priority": 10,
683
+ })
684
+
685
+ if has_recall:
686
+ questions.append({
687
+ "id": "evidence_check",
688
+ "label_key": "brain.suggestion.evidence.label",
689
+ "detail_key": "brain.suggestion.evidence.detail",
690
+ "prompt_key": "brain.suggestion.evidence.prompt",
691
+ "params": {"focus": focus_title or "this topic"},
692
+ "priority": 9,
693
+ })
694
+
695
+ if graph_concepts > 0:
696
+ questions.append({
697
+ "id": "graph_connections",
698
+ "label_key": "brain.suggestion.graph.label",
699
+ "detail_key": "brain.suggestion.graph.detail",
700
+ "prompt_key": "brain.suggestion.graph.prompt",
701
+ "params": {"focus": focus_title or "Knowledge Graph"},
702
+ "priority": 8,
703
+ })
704
+
705
+ if conversations > 0:
706
+ questions.append({
707
+ "id": "conversation_followup",
708
+ "label_key": "brain.suggestion.history.label",
709
+ "detail_key": "brain.suggestion.history.detail",
710
+ "prompt_key": "brain.suggestion.history.prompt",
711
+ "params": {"focus": focus_title or "recent conversations"},
712
+ "priority": 7,
713
+ })
714
+
715
+ return sorted(questions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
716
+
504
717
  def brain_proof(
505
718
  self,
506
719
  *,
@@ -23,6 +23,13 @@ from typing import Any, Dict, List, Optional
23
23
 
24
24
  from fastapi import HTTPException
25
25
 
26
+ from latticeai.services.process_audit import (
27
+ CommandConfirmationError,
28
+ append_process_audit_event,
29
+ command_plan,
30
+ require_command_confirmation,
31
+ )
32
+
26
33
 
27
34
  def _progress_payload(*args, **kwargs) -> Dict[str, object]:
28
35
  try:
@@ -440,8 +447,8 @@ def engine_support_status(engine: str) -> Dict[str, object]:
440
447
  return {"supported": True, "reason": None}
441
448
 
442
449
 
443
- def install_engine(engine: str) -> Dict[str, Any]:
444
- from latticeai.services.model_runtime import BASE_DIR, ENGINE_INSTALLERS, engine_installed
450
+ def _engine_install_command(engine: str) -> tuple[list[str], str, bool]:
451
+ from latticeai.services.model_runtime import BASE_DIR, ENGINE_INSTALLERS
445
452
 
446
453
  if engine not in ENGINE_INSTALLERS:
447
454
  raise HTTPException(status_code=400, detail="지원하지 않는 엔진입니다.")
@@ -449,14 +456,7 @@ def install_engine(engine: str) -> Dict[str, Any]:
449
456
  required_binary = installer.get("requires_binary")
450
457
  if required_binary and shutil.which(required_binary) is None:
451
458
  raise HTTPException(status_code=400, detail=f"{required_binary}가 설치되어 있지 않아 자동 설치할 수 없습니다.")
452
- command = installer["command"]
453
- run_kwargs = {
454
- "cwd": str(BASE_DIR),
455
- "capture_output": True,
456
- "text": True,
457
- "timeout": 900,
458
- "check": False,
459
- }
459
+ command = list(installer["command"])
460
460
 
461
461
  if engine == "vllm" and sys.platform == "darwin" and platform.machine() == "arm64":
462
462
  command = [
@@ -468,13 +468,69 @@ def install_engine(engine: str) -> Dict[str, Any]:
468
468
  "~/.venv-vllm-metal/bin/pip install -U pip setuptools wheel; "
469
469
  "~/.venv-vllm-metal/bin/pip install vllm-metal",
470
470
  ]
471
+ requires_admin = bool(command and command[0] in {"apt", "apt-get", "dnf", "pacman"})
472
+ return command, str(BASE_DIR), requires_admin
473
+
474
+
475
+ def engine_install_plan(engine: str) -> Dict[str, Any]:
476
+ command, cwd, requires_admin = _engine_install_command(engine)
477
+ return command_plan(
478
+ command,
479
+ name=f"engine:{engine}",
480
+ purpose="engine_install",
481
+ cwd=cwd,
482
+ requires_admin=requires_admin,
483
+ metadata={"engine": engine},
484
+ )
485
+
486
+
487
+ def install_engine(engine: str, confirmation_token: Optional[str] = None) -> Dict[str, Any]:
488
+ from latticeai.services.model_runtime import engine_installed
489
+
490
+ command, cwd, _requires_admin = _engine_install_command(engine)
491
+ plan = engine_install_plan(engine)
492
+ try:
493
+ require_command_confirmation(command, confirmation_token, cwd=cwd, purpose="engine_install")
494
+ except CommandConfirmationError as exc:
495
+ append_process_audit_event("engine_install", plan=plan, status="denied", error=str(exc))
496
+ raise HTTPException(
497
+ status_code=403,
498
+ detail={
499
+ "status": "confirmation_required",
500
+ "reason": str(exc),
501
+ "install_plan": plan,
502
+ },
503
+ ) from exc
504
+
505
+ run_kwargs = {
506
+ "cwd": cwd,
507
+ "capture_output": True,
508
+ "text": True,
509
+ "timeout": 900,
510
+ "check": False,
511
+ }
471
512
  try:
513
+ append_process_audit_event("engine_install", plan=plan, status="started")
472
514
  completed = subprocess.run(command, **run_kwargs)
473
515
  except subprocess.TimeoutExpired:
516
+ append_process_audit_event("engine_install", plan=plan, status="timeout")
474
517
  raise HTTPException(status_code=408, detail="엔진 설치 시간이 초과되었습니다.")
518
+ except Exception as exc:
519
+ append_process_audit_event("engine_install", plan=plan, status="error", error=str(exc))
520
+ raise
521
+ append_process_audit_event(
522
+ "engine_install",
523
+ plan=plan,
524
+ status="finished",
525
+ returncode=completed.returncode,
526
+ stdout=completed.stdout,
527
+ stderr=completed.stderr,
528
+ )
475
529
  result = {
476
530
  "engine": engine,
477
- "command": " ".join(command),
531
+ "command_hash": plan["command_hash"],
532
+ "command_preview": plan["command_preview"],
533
+ "install_plan": plan,
478
534
  "returncode": completed.returncode,
479
535
  "stdout": completed.stdout[-12000:],
480
536
  "stderr": completed.stderr[-12000:],
@@ -491,15 +547,25 @@ def install_engine(engine: str) -> Dict[str, Any]:
491
547
  if already_up:
492
548
  result["daemon_started"] = "already_running"
493
549
  else:
550
+ daemon_command = [ollama, "serve"]
551
+ daemon_plan = command_plan(
552
+ daemon_command,
553
+ name="engine:ollama:serve",
554
+ purpose="engine_daemon_start",
555
+ metadata={"engine": "ollama"},
556
+ )
494
557
  try:
558
+ append_process_audit_event("engine_daemon_start", plan=daemon_plan, status="started")
495
559
  subprocess.Popen(
496
- [ollama, "serve"],
560
+ daemon_command,
497
561
  stdout=subprocess.DEVNULL,
498
562
  stderr=subprocess.DEVNULL,
499
563
  start_new_session=True,
500
564
  )
565
+ append_process_audit_event("engine_daemon_start", plan=daemon_plan, status="spawned")
501
566
  result["daemon_started"] = True
502
567
  except Exception as exc:
568
+ append_process_audit_event("engine_daemon_start", plan=daemon_plan, status="error", error=str(exc))
503
569
  logging.warning("ollama serve spawn failed: %s", exc)
504
570
  result["daemon_started"] = False
505
571
  return result
@@ -589,6 +655,7 @@ __all__ = [
589
655
  "pull_ollama_model_with_progress",
590
656
  "get_ollama_pulled_models",
591
657
  "engine_support_status",
658
+ "engine_install_plan",
592
659
  "install_engine",
593
660
  "_smoke_test_loaded_model",
594
661
  ]
@@ -47,6 +47,7 @@ from .model_engines import (
47
47
  pull_ollama_model_with_progress as _pull_ollama_model_with_progress,
48
48
  get_ollama_pulled_models as _get_ollama_pulled_models,
49
49
  engine_support_status as _engine_support_status,
50
+ engine_install_plan as _engine_install_plan,
50
51
  install_engine as _install_engine,
51
52
  local_binary as _local_binary,
52
53
  vllm_executable as _vllm_executable,
@@ -88,6 +89,8 @@ class ModelRuntimeState:
88
89
  self.ENABLE_GRAPH = True
89
90
  self.AUTOLOAD_MODELS = False
90
91
  self.MODEL_IDLE_UNLOAD_SECONDS = 0
92
+ self.ALLOW_MODEL_DOWNLOADS = False
93
+ self.MODEL_DOWNLOAD_TIMEOUT = 300
91
94
  self.ALLOW_LOCAL_MODELS = True
92
95
  self.REQUIRE_AUTH = False
93
96
  self.INVITE_GATE_ENABLED = False
@@ -118,7 +121,8 @@ class ModelRuntimeState:
118
121
  """Internal no-warning sync used at init."""
119
122
  global router, APP_MODE, DEFAULT_HOST, DEFAULT_PORT, DATA_DIR, BASE_DIR
120
123
  global ENABLE_TELEGRAM, ENABLE_GRAPH, AUTOLOAD_MODELS, MODEL_IDLE_UNLOAD_SECONDS
121
- global ALLOW_LOCAL_MODELS, REQUIRE_AUTH, INVITE_GATE_ENABLED, ALLOW_PLAINTEXT_API_KEYS
124
+ global ALLOW_MODEL_DOWNLOADS, MODEL_DOWNLOAD_TIMEOUT, ALLOW_LOCAL_MODELS, REQUIRE_AUTH
125
+ global INVITE_GATE_ENABLED, ALLOW_PLAINTEXT_API_KEYS
122
126
  global CORS_ALLOW_NETWORK, PUBLIC_MODEL, LOCAL_MODEL, IS_PUBLIC_MODE
123
127
  global keyring, get_current_user, get_user_api_key
124
128
  router = self.router
@@ -131,6 +135,8 @@ class ModelRuntimeState:
131
135
  ENABLE_GRAPH = self.ENABLE_GRAPH
132
136
  AUTOLOAD_MODELS = self.AUTOLOAD_MODELS
133
137
  MODEL_IDLE_UNLOAD_SECONDS = self.MODEL_IDLE_UNLOAD_SECONDS
138
+ ALLOW_MODEL_DOWNLOADS = self.ALLOW_MODEL_DOWNLOADS
139
+ MODEL_DOWNLOAD_TIMEOUT = self.MODEL_DOWNLOAD_TIMEOUT
134
140
  ALLOW_LOCAL_MODELS = self.ALLOW_LOCAL_MODELS
135
141
  REQUIRE_AUTH = self.REQUIRE_AUTH
136
142
  INVITE_GATE_ENABLED = self.INVITE_GATE_ENABLED
@@ -159,7 +165,8 @@ def _env_bool(key: str, default: bool = False) -> bool:
159
165
  def _download_allowed(allow_download: bool = False) -> bool:
160
166
  # Prefer STATE (the source of truth) over bare module global for internal logic.
161
167
  autoload = getattr(STATE, "AUTOLOAD_MODELS", AUTOLOAD_MODELS)
162
- return bool(allow_download) or _env_bool("LATTICEAI_ALLOW_MODEL_DOWNLOADS", default=False) or bool(autoload)
168
+ configured = getattr(STATE, "ALLOW_MODEL_DOWNLOADS", ALLOW_MODEL_DOWNLOADS)
169
+ return bool(allow_download) or bool(configured) or bool(autoload)
163
170
 
164
171
 
165
172
  def _download_block(provider: str, model_name: str) -> None:
@@ -726,6 +733,13 @@ def ensure_llamacpp_server(model_name: str) -> None:
726
733
  return _ensure_llamacpp_server(model_name)
727
734
 
728
735
 
736
+ def _safe_engine_install_plan(engine: str) -> Optional[Dict[str, object]]:
737
+ try:
738
+ return _engine_install_plan(engine)
739
+ except Exception:
740
+ return None
741
+
742
+
729
743
  def engine_installed(engine: str) -> bool:
730
744
  if engine == "local_mlx":
731
745
  return bool(
@@ -847,6 +861,7 @@ def engine_status() -> List[Dict]:
847
861
  "installed": engine_installed("local_mlx"),
848
862
  "installable": True,
849
863
  "install_label": ENGINE_INSTALLERS["local_mlx"]["label"],
864
+ "install_plan": _safe_engine_install_plan("local_mlx"),
850
865
  "models": mlx_models,
851
866
  },
852
867
  {
@@ -857,6 +872,7 @@ def engine_status() -> List[Dict]:
857
872
  "installed": ollama_installed,
858
873
  "installable": True,
859
874
  "install_label": ENGINE_INSTALLERS["ollama"]["label"],
875
+ "install_plan": _safe_engine_install_plan("ollama"),
860
876
  "models": ollama_models,
861
877
  },
862
878
  ]
@@ -872,6 +888,7 @@ def engine_status() -> List[Dict]:
872
888
  "support_reason": support["reason"],
873
889
  "installable": support["supported"] and spec["id"] in ENGINE_INSTALLERS,
874
890
  "install_label": ENGINE_INSTALLERS.get(spec["id"], {}).get("label"),
891
+ "install_plan": _safe_engine_install_plan(spec["id"]) if spec["id"] in ENGINE_INSTALLERS else None,
875
892
  "requires": spec["requires"],
876
893
  "models": (
877
894
  vllm_models if spec["id"] == "vllm"
@@ -900,6 +917,7 @@ def engine_status() -> List[Dict]:
900
917
  "installed": engine_installed(provider),
901
918
  "installable": True,
902
919
  "install_label": ENGINE_INSTALLERS[provider]["label"],
920
+ "install_plan": _safe_engine_install_plan(provider),
903
921
  "requires": env_key,
904
922
  "models": provider_models,
905
923
  })
@@ -920,6 +938,8 @@ def runtime_features() -> Dict:
920
938
  "graph_enabled": s.ENABLE_GRAPH,
921
939
  "autoload_models": s.AUTOLOAD_MODELS,
922
940
  "model_idle_unload_seconds": s.MODEL_IDLE_UNLOAD_SECONDS,
941
+ "allow_model_downloads": s.ALLOW_MODEL_DOWNLOADS,
942
+ "model_download_timeout": s.MODEL_DOWNLOAD_TIMEOUT,
923
943
  "model_memory_policy": r.model_memory_policy() if r else None,
924
944
  "allow_local_models": s.ALLOW_LOCAL_MODELS,
925
945
  "security": {
@@ -944,8 +964,8 @@ def runtime_features() -> Dict:
944
964
  },
945
965
  }
946
966
 
947
- def install_engine(engine: str) -> Dict:
948
- return _install_engine(engine)
967
+ def install_engine(engine: str, confirmation_token: Optional[str] = None) -> Dict:
968
+ return _install_engine(engine, confirmation_token=confirmation_token)
949
969
 
950
970
 
951
971
  def _resolve_model_alias(model_id: str, engine: Optional[str] = None) -> str:
@@ -144,7 +144,15 @@ class PlatformRuntime:
144
144
  def provider(goal: str):
145
145
  try:
146
146
  mems = self.store.search_memories(goal, user_email=user, workspace_id=scope).get("memories", [])
147
- return [str(m.get("content") or "")[:160] for m in mems[:5]]
147
+ ctx = [str(m.get("content") or "")[:180] for m in mems[:6]]
148
+ # Enrich with agent-synthesis memories preferentially for self-improving Brain feel
149
+ try:
150
+ allm = self.store.list_memories(user_email=user, workspace_id=scope).get("memories", [])
151
+ synth = [str(m.get("content") or "")[:160] for m in allm if "agent-synthesis" in (m.get("tags") or [])][:3]
152
+ ctx = synth + ctx
153
+ except Exception:
154
+ pass
155
+ return ctx[:8]
148
156
  except Exception:
149
157
  return []
150
158
  return provider