ltcai 9.5.0 → 9.7.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 (71) hide show
  1. package/README.md +67 -43
  2. package/auto_setup.py +11 -1
  3. package/docs/CHANGELOG.md +86 -0
  4. package/docs/COMMUNITY_AND_PLUGINS.md +2 -2
  5. package/docs/DEVELOPMENT.md +8 -8
  6. package/docs/LEGACY_COMPATIBILITY.md +1 -1
  7. package/docs/ONBOARDING.md +2 -2
  8. package/docs/OPERATIONS.md +1 -1
  9. package/docs/PERFORMANCE.md +106 -0
  10. package/docs/TRUST_MODEL.md +1 -1
  11. package/docs/WHY_LATTICE.md +1 -1
  12. package/docs/kg-schema.md +1 -1
  13. package/docs/mcp-tools.md +1 -1
  14. package/kg_schema.py +11 -1
  15. package/knowledge_graph.py +12 -2
  16. package/knowledge_graph_api.py +11 -1
  17. package/lattice_brain/__init__.py +1 -1
  18. package/lattice_brain/graph/proactive.py +583 -0
  19. package/lattice_brain/graph/retrieval.py +211 -7
  20. package/lattice_brain/graph/retrieval_vector.py +102 -0
  21. package/lattice_brain/ingestion.py +360 -4
  22. package/lattice_brain/quality.py +44 -9
  23. package/lattice_brain/runtime/multi_agent.py +38 -2
  24. package/latticeai/__init__.py +1 -1
  25. package/latticeai/api/brain_intelligence.py +27 -2
  26. package/latticeai/api/change_proposals.py +89 -0
  27. package/latticeai/api/chat_agent_http.py +2 -0
  28. package/latticeai/api/review_queue.py +66 -2
  29. package/latticeai/app_factory.py +23 -0
  30. package/latticeai/cli/entrypoint.py +3 -1
  31. package/latticeai/core/agent.py +273 -101
  32. package/latticeai/core/agent_eval.py +411 -0
  33. package/latticeai/core/agent_trace.py +104 -0
  34. package/latticeai/core/legacy_compatibility.py +15 -1
  35. package/latticeai/core/marketplace.py +1 -1
  36. package/latticeai/core/tool_governor.py +101 -0
  37. package/latticeai/core/workspace_os.py +1 -1
  38. package/latticeai/runtime/router_registration.py +2 -0
  39. package/latticeai/services/architecture_readiness.py +1 -1
  40. package/latticeai/services/brain_intelligence.py +97 -1
  41. package/latticeai/services/change_proposals.py +322 -0
  42. package/latticeai/services/product_readiness.py +1 -1
  43. package/latticeai/services/review_queue.py +47 -3
  44. package/latticeai/tools/__init__.py +6 -1
  45. package/llm_router.py +10 -1
  46. package/local_knowledge_api.py +10 -1
  47. package/ltcai_cli.py +11 -1
  48. package/mcp_registry.py +10 -1
  49. package/p_reinforce.py +10 -1
  50. package/package.json +1 -1
  51. package/scripts/agent_eval.py +46 -0
  52. package/scripts/brain_quality_eval.py +1 -1
  53. package/scripts/check_current_release_docs.mjs +2 -1
  54. package/scripts/profile_kg.py +360 -0
  55. package/setup_wizard.py +10 -1
  56. package/src-tauri/Cargo.lock +1 -1
  57. package/src-tauri/Cargo.toml +1 -1
  58. package/src-tauri/tauri.conf.json +1 -1
  59. package/static/app/asset-manifest.json +11 -11
  60. package/static/app/assets/{Act-Cdfqx4wN.js → Act-B6c39ays.js} +2 -1
  61. package/static/app/assets/{Brain-w_tAuyg6.js → Brain-D7Qg4k6M.js} +1 -1
  62. package/static/app/assets/{Capture-iJwi9LmS.js → Capture-VF_di68r.js} +1 -1
  63. package/static/app/assets/{Library-Do-jjhzi.js → Library-D_Gis2PA.js} +1 -1
  64. package/static/app/assets/{System-DFg_q3E6.js → System-C5s5H2ov.js} +1 -1
  65. package/static/app/assets/{index-BN6HIWVC.css → index-85wQvEie.css} +1 -1
  66. package/static/app/assets/index-DJC_2oub.js +18 -0
  67. package/static/app/assets/{primitives-ZTUlU7pR.js → primitives-DL4Nip8C.js} +1 -1
  68. package/static/app/assets/{textarea-CMfhqPhE.js → textarea-woZfCXHy.js} +1 -1
  69. package/static/app/index.html +2 -2
  70. package/static/sw.js +1 -1
  71. package/static/app/assets/index-aJuRi-Xo.js +0 -17
@@ -0,0 +1,89 @@
1
+ """Change proposal API router (v9.6.0).
2
+
3
+ Read/approve/reject surface over
4
+ :class:`~latticeai.services.change_proposals.ChangeProposalService`. The
5
+ items live in the shared review queue (source ``change_proposal``), so the
6
+ Act review center shows them too; this router adds the proposal-specific
7
+ semantics: **approve applies the staged content exactly as reviewed**,
8
+ reject discards it, and nothing touches disk while a proposal is pending.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Callable, Optional
14
+
15
+ from fastapi import APIRouter, HTTPException, Request
16
+ from pydantic import BaseModel
17
+
18
+ from latticeai.services.change_proposals import ChangeProposalService
19
+
20
+
21
+ class RejectProposalRequest(BaseModel):
22
+ """Optional rejection context — kept in the item's provenance."""
23
+
24
+ reason: str = ""
25
+
26
+
27
+ def create_change_proposals_router(
28
+ *,
29
+ service: ChangeProposalService,
30
+ require_user: Callable[[Request], str],
31
+ gate_read: Callable[[Request], Optional[str]],
32
+ gate_write: Callable[[Request], Optional[str]],
33
+ ) -> APIRouter:
34
+ router = APIRouter()
35
+
36
+ @router.get("/api/proposals")
37
+ async def list_proposals(request: Request):
38
+ user = require_user(request)
39
+ scope = gate_read(request)
40
+ return service.pending(user_email=user, workspace_id=scope)
41
+
42
+ # NOTE: declared before /api/proposals/{item_id} so "counts" never
43
+ # resolves as an item id.
44
+ @router.get("/api/proposals/counts")
45
+ async def proposal_counts(request: Request):
46
+ user = require_user(request)
47
+ scope = gate_read(request)
48
+ return service.counts(user_email=user, workspace_id=scope)
49
+
50
+ @router.get("/api/proposals/{item_id}")
51
+ async def get_proposal(item_id: str, request: Request):
52
+ require_user(request)
53
+ scope = gate_read(request)
54
+ try:
55
+ return service.get_proposal(item_id, workspace_id=scope)
56
+ except (KeyError, FileNotFoundError) as exc:
57
+ raise HTTPException(status_code=404, detail=str(exc))
58
+
59
+ @router.post("/api/proposals/{item_id}/approve")
60
+ async def approve_proposal(item_id: str, request: Request):
61
+ user = require_user(request)
62
+ scope = gate_write(request)
63
+ try:
64
+ return service.approve_and_apply(
65
+ item_id, user_email=user, workspace_id=scope
66
+ )
67
+ except (KeyError, FileNotFoundError) as exc:
68
+ raise HTTPException(status_code=404, detail=str(exc))
69
+ except ValueError as exc:
70
+ raise HTTPException(status_code=400, detail=str(exc))
71
+
72
+ @router.post("/api/proposals/{item_id}/reject")
73
+ async def reject_proposal(
74
+ item_id: str, request: Request, req: Optional[RejectProposalRequest] = None
75
+ ):
76
+ user = require_user(request)
77
+ scope = gate_write(request)
78
+ try:
79
+ return service.reject(
80
+ item_id, user_email=user, workspace_id=scope,
81
+ reason=(req.reason if req else ""),
82
+ )
83
+ except (KeyError, FileNotFoundError) as exc:
84
+ raise HTTPException(status_code=404, detail=str(exc))
85
+
86
+ return router
87
+
88
+
89
+ __all__ = ["create_change_proposals_router", "RejectProposalRequest"]
@@ -229,6 +229,7 @@ class AgentHTTPController:
229
229
  "planning_model": req.planning_model or self.model_router.current_model_id,
230
230
  "executing_model": req.executing_model or self.model_router.current_model_id,
231
231
  "reviewing_model": req.reviewing_model or self.model_router.current_model_id,
232
+ "loop": ctx.trace.summary(),
232
233
  }
233
234
 
234
235
  self.runtime.approve(ctx, current_user, approved_by_human=False)
@@ -306,6 +307,7 @@ class AgentHTTPController:
306
307
  "state_history": ctx.state_history,
307
308
  "final_state": ctx.state.value,
308
309
  "created_files": collect_created_files(ctx.transcript),
310
+ "loop": ctx.trace.summary(),
309
311
  }
310
312
 
311
313
  async def resume(
@@ -56,6 +56,18 @@ class SnoozeRequest(BaseModel):
56
56
  until: str
57
57
 
58
58
 
59
+ class DismissRequest(BaseModel):
60
+ """Optional dismissal context (e.g. why a change proposal was rejected)."""
61
+
62
+ reason: str = ""
63
+
64
+
65
+ class ReviewCounts(BaseModel):
66
+ pending: int = 0
67
+ snoozed: int = 0
68
+ pending_by_source: Dict[str, int] = Field(default_factory=dict)
69
+
70
+
59
71
  def create_review_queue_router(
60
72
  *,
61
73
  service: ReviewQueueService,
@@ -64,7 +76,13 @@ def create_review_queue_router(
64
76
  gate_write: Callable[[Request], Optional[str]],
65
77
  run_review_item: Callable[..., Any],
66
78
  append_audit_event: Callable[..., None],
79
+ change_proposals: Any = None,
67
80
  ) -> APIRouter:
81
+ """``change_proposals`` (optional) closes the governance loop: approving a
82
+ ``change_proposal`` item from the Review Center applies the staged content
83
+ via :class:`~latticeai.services.change_proposals.ChangeProposalService`
84
+ instead of merely flipping the status — the same single application path
85
+ the /api/proposals surface uses."""
68
86
  router = APIRouter()
69
87
 
70
88
  @router.get("/automation/reviews", response_model=ReviewItemList)
@@ -95,6 +113,14 @@ def create_review_queue_router(
95
113
  append_audit_event("review_item_created", user_email=user, item_id=item["id"])
96
114
  return item
97
115
 
116
+ # NOTE: declared before /automation/reviews/{item_id} so "counts" never
117
+ # resolves as an item id.
118
+ @router.get("/automation/reviews/counts", response_model=ReviewCounts)
119
+ async def review_counts(request: Request):
120
+ user = require_user(request)
121
+ scope = gate_read(request)
122
+ return service.counts(workspace_id=scope, user_email=user)
123
+
98
124
  @router.get("/automation/reviews/{item_id}", response_model=ReviewItem)
99
125
  async def get_item(item_id: str, request: Request):
100
126
  require_user(request)
@@ -106,11 +132,49 @@ def create_review_queue_router(
106
132
 
107
133
  @router.post("/automation/reviews/{item_id}/approve", response_model=ReviewItem)
108
134
  async def approve_item(item_id: str, request: Request):
135
+ user = require_user(request)
136
+ scope = gate_write(request)
137
+ # change_proposal items must apply the staged content on approve —
138
+ # otherwise the item flips to "approved" while nothing hits disk.
139
+ if change_proposals is not None:
140
+ try:
141
+ stored = service.get(item_id, workspace_id=scope)
142
+ except FileNotFoundError as exc:
143
+ raise HTTPException(status_code=404, detail="review item not found") from exc
144
+ if stored.get("source") == "change_proposal":
145
+ if stored.get("effective_status") not in ("pending", "snoozed"):
146
+ raise HTTPException(
147
+ status_code=409,
148
+ detail=f"cannot 'approve' a review item in status {stored.get('status')!r}",
149
+ )
150
+ try:
151
+ applied = change_proposals.approve_and_apply(
152
+ item_id, user_email=user, workspace_id=scope
153
+ )
154
+ except (KeyError, FileNotFoundError) as exc:
155
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
156
+ except ValueError as exc:
157
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
158
+ append_audit_event("review_item_approve", user_email=user, item_id=item_id)
159
+ return applied["item"]
109
160
  return _act(request, item_id, "approve")
110
161
 
111
162
  @router.post("/automation/reviews/{item_id}/dismiss", response_model=ReviewItem)
112
- async def dismiss_item(item_id: str, request: Request):
113
- return _act(request, item_id, "dismiss")
163
+ async def dismiss_item(
164
+ item_id: str, request: Request, req: Optional[DismissRequest] = None
165
+ ):
166
+ user = require_user(request)
167
+ scope = gate_write(request)
168
+ try:
169
+ item = service.dismiss(
170
+ item_id, workspace_id=scope, reason=(req.reason if req else None)
171
+ )
172
+ except FileNotFoundError as exc:
173
+ raise HTTPException(status_code=404, detail="review item not found") from exc
174
+ except InvalidReviewTransition as exc:
175
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
176
+ append_audit_event("review_item_dismiss", user_email=user, item_id=item_id)
177
+ return item
114
178
 
115
179
  @router.post("/automation/reviews/{item_id}/snooze", response_model=ReviewItem)
116
180
  async def snooze_item(item_id: str, req: SnoozeRequest, request: Request):
@@ -169,6 +169,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
169
169
  from latticeai.api.brain_intelligence import create_brain_intelligence_router
170
170
  from latticeai.api.command_center import create_command_center_router
171
171
  from latticeai.services.command_center import CommandCenterService
172
+ from latticeai.api.change_proposals import create_change_proposals_router
173
+ from latticeai.services.change_proposals import ChangeProposalService
174
+ from latticeai.tools import resolve_workspace_path
172
175
  from latticeai.api.memory import create_memory_router
173
176
  from latticeai.api.browser import create_browser_router
174
177
  from latticeai.api.portability import create_portability_router
@@ -1051,6 +1054,23 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1051
1054
  gate_read=PLATFORM.gate_read,
1052
1055
  )
1053
1056
  )
1057
+ CHANGE_PROPOSALS = ChangeProposalService(
1058
+ review_queue=REVIEW_QUEUE,
1059
+ resolve_path=resolve_workspace_path,
1060
+ audit=append_audit_event,
1061
+ )
1062
+ # Proposal-first mutations: the agent loop consults the governor so
1063
+ # additive creates run with minimal friction while changes/deletions of
1064
+ # existing files are staged for review instead of applied.
1065
+ CHAT_AGENT_RUNTIME.deps.change_governor = CHANGE_PROPOSALS
1066
+ app.include_router(
1067
+ create_change_proposals_router(
1068
+ service=CHANGE_PROPOSALS,
1069
+ require_user=require_user,
1070
+ gate_read=PLATFORM.gate_read,
1071
+ gate_write=PLATFORM.gate_write,
1072
+ )
1073
+ )
1054
1074
 
1055
1075
 
1056
1076
  # ── Health & Info ──────────────────────────────────────────────────────────────
@@ -1174,6 +1194,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1174
1194
  gate_write=PLATFORM.gate_write,
1175
1195
  run_review_item=run_review_item,
1176
1196
  append_audit_event=append_audit_event,
1197
+ # Approving a change_proposal from the Review Center applies the
1198
+ # staged content through the same service the agent governor uses.
1199
+ change_proposals=CHANGE_PROPOSALS,
1177
1200
  create_browser_router=create_browser_router,
1178
1201
  ingestion_pipeline=INGESTION_PIPELINE,
1179
1202
  workspace_service=WORKSPACE_SERVICE,
@@ -157,7 +157,9 @@ def _start_tunnel(port: int) -> str | None:
157
157
  log_path = Path.home() / ".latticeai" / "tunnel.log"
158
158
  log_path.parent.mkdir(parents=True, exist_ok=True)
159
159
 
160
- proc = subprocess.Popen(
160
+ # Detached on purpose: the tunnel outlives this helper; the log file is
161
+ # the observable surface.
162
+ subprocess.Popen(
161
163
  [bin_path, "tunnel", "--url", f"http://localhost:{port}"],
162
164
  stdout=open(log_path, "w"),
163
165
  stderr=subprocess.STDOUT,