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.
- package/README.md +67 -43
- package/auto_setup.py +11 -1
- package/docs/CHANGELOG.md +86 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +2 -2
- package/docs/DEVELOPMENT.md +8 -8
- package/docs/LEGACY_COMPATIBILITY.md +1 -1
- package/docs/ONBOARDING.md +2 -2
- package/docs/OPERATIONS.md +1 -1
- package/docs/PERFORMANCE.md +106 -0
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/docs/mcp-tools.md +1 -1
- package/kg_schema.py +11 -1
- package/knowledge_graph.py +12 -2
- package/knowledge_graph_api.py +11 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/proactive.py +583 -0
- package/lattice_brain/graph/retrieval.py +211 -7
- package/lattice_brain/graph/retrieval_vector.py +102 -0
- package/lattice_brain/ingestion.py +360 -4
- package/lattice_brain/quality.py +44 -9
- package/lattice_brain/runtime/multi_agent.py +38 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +27 -2
- package/latticeai/api/change_proposals.py +89 -0
- package/latticeai/api/chat_agent_http.py +2 -0
- package/latticeai/api/review_queue.py +66 -2
- package/latticeai/app_factory.py +23 -0
- package/latticeai/cli/entrypoint.py +3 -1
- package/latticeai/core/agent.py +273 -101
- package/latticeai/core/agent_eval.py +411 -0
- package/latticeai/core/agent_trace.py +104 -0
- package/latticeai/core/legacy_compatibility.py +15 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/tool_governor.py +101 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/router_registration.py +2 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence.py +97 -1
- package/latticeai/services/change_proposals.py +322 -0
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/review_queue.py +47 -3
- package/latticeai/tools/__init__.py +6 -1
- package/llm_router.py +10 -1
- package/local_knowledge_api.py +10 -1
- package/ltcai_cli.py +11 -1
- package/mcp_registry.py +10 -1
- package/p_reinforce.py +10 -1
- package/package.json +1 -1
- package/scripts/agent_eval.py +46 -0
- package/scripts/brain_quality_eval.py +1 -1
- package/scripts/check_current_release_docs.mjs +2 -1
- package/scripts/profile_kg.py +360 -0
- package/setup_wizard.py +10 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -11
- package/static/app/assets/{Act-Cdfqx4wN.js → Act-B6c39ays.js} +2 -1
- package/static/app/assets/{Brain-w_tAuyg6.js → Brain-D7Qg4k6M.js} +1 -1
- package/static/app/assets/{Capture-iJwi9LmS.js → Capture-VF_di68r.js} +1 -1
- package/static/app/assets/{Library-Do-jjhzi.js → Library-D_Gis2PA.js} +1 -1
- package/static/app/assets/{System-DFg_q3E6.js → System-C5s5H2ov.js} +1 -1
- package/static/app/assets/{index-BN6HIWVC.css → index-85wQvEie.css} +1 -1
- package/static/app/assets/index-DJC_2oub.js +18 -0
- package/static/app/assets/{primitives-ZTUlU7pR.js → primitives-DL4Nip8C.js} +1 -1
- package/static/app/assets/{textarea-CMfhqPhE.js → textarea-woZfCXHy.js} +1 -1
- package/static/app/index.html +2 -2
- package/static/sw.js +1 -1
- 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(
|
|
113
|
-
|
|
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):
|
package/latticeai/app_factory.py
CHANGED
|
@@ -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
|
-
|
|
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,
|