ltcai 9.4.0 → 9.6.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 +63 -40
- package/docs/CHANGELOG.md +57 -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/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/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/change_proposals.py +60 -0
- package/latticeai/api/chat_agent_http.py +2 -0
- package/latticeai/api/command_center.py +51 -0
- package/latticeai/app_factory.py +39 -0
- package/latticeai/cli/entrypoint.py +3 -1
- package/latticeai/core/agent.py +127 -13
- package/latticeai/core/agent_eval.py +260 -0
- package/latticeai/core/agent_trace.py +104 -0
- package/latticeai/core/legacy_compatibility.py +1 -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/services/architecture_readiness.py +1 -1
- package/latticeai/services/change_proposals.py +270 -0
- package/latticeai/services/command_center.py +433 -0
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/review_queue.py +4 -1
- package/latticeai/tools/__init__.py +6 -1
- package/package.json +1 -1
- package/scripts/agent_eval.py +46 -0
- package/scripts/check_current_release_docs.mjs +2 -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-t9oveJO7.js → Act-BkOEmwBi.js} +1 -1
- package/static/app/assets/{Brain-DP-gpcEJ.js → Brain-C9ITUsQ_.js} +1 -1
- package/static/app/assets/{Capture-DYknDKy8.js → Capture-C-ppTeud.js} +1 -1
- package/static/app/assets/{Library-DLyc_g8c.js → Library-CGQbgWTu.js} +1 -1
- package/static/app/assets/{System-BZgJ7tGu.js → System-djmj0n2_.js} +1 -1
- package/static/app/assets/{index-Cl4S_9Id.css → index-85wQvEie.css} +1 -1
- package/static/app/assets/index-AF0-4XVv.js +18 -0
- package/static/app/assets/{primitives-DawfkPR4.js → primitives-jbb2qv4Q.js} +1 -1
- package/static/app/assets/{textarea-a4Ir3SZS.js → textarea-CFoo0OxJ.js} +1 -1
- package/static/app/index.html +2 -2
- package/static/sw.js +1 -1
- package/static/app/assets/index-BeQ77vPs.js +0 -17
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Command Center: daily briefing + universal command search (v9.5.0).
|
|
2
|
+
|
|
3
|
+
The Brain accumulates a lot of surfaces — knowledge, conversations,
|
|
4
|
+
automations, review queue, health. The Command Center condenses them into two
|
|
5
|
+
deterministic, local, read-only entry points:
|
|
6
|
+
|
|
7
|
+
* **briefing** — one payload answering "what does my Brain know about today?":
|
|
8
|
+
recent knowledge, conversation activity, automation state, pending reviews,
|
|
9
|
+
a health snapshot, and top automation suggestions. Each section degrades
|
|
10
|
+
independently — a missing backend never breaks the briefing.
|
|
11
|
+
* **search** — one query across every surface at once: knowledge nodes
|
|
12
|
+
(graph keyword search), the user's own conversations, and installed
|
|
13
|
+
automations. Powers the Cmd+K command palette.
|
|
14
|
+
* **quick_actions** — state-derived next steps ("N items waiting for review",
|
|
15
|
+
"enable your draft automation") with stable ids, so the UI can render
|
|
16
|
+
one-click jumps without guessing.
|
|
17
|
+
|
|
18
|
+
Everything here is scoped to the requesting user and workspace, mirrors the
|
|
19
|
+
scoped-read conventions of the automation intelligence service, and never
|
|
20
|
+
calls a model.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
from latticeai.core.timeutil import now_iso as _now
|
|
29
|
+
|
|
30
|
+
LOGGER = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_RECENT_NODE_LIMIT = 6
|
|
33
|
+
_SEARCH_HISTORY_LIMIT = 2000
|
|
34
|
+
_BRIEFING_HISTORY_LIMIT = 2000
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _clip(text: Any, limit: int = 160) -> str:
|
|
38
|
+
value = str(text or "").strip()
|
|
39
|
+
return value[:limit]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CommandCenterService:
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
conversation_store: Any = None,
|
|
47
|
+
knowledge_graph: Any = None,
|
|
48
|
+
store: Any = None,
|
|
49
|
+
search_service: Any = None,
|
|
50
|
+
brain_intelligence: Any = None,
|
|
51
|
+
automation_intelligence: Any = None,
|
|
52
|
+
review_queue: Any = None,
|
|
53
|
+
enable_graph: bool = True,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._conversations = conversation_store
|
|
56
|
+
self._kg = knowledge_graph
|
|
57
|
+
self._store = store
|
|
58
|
+
self._search = search_service
|
|
59
|
+
self._brain = brain_intelligence
|
|
60
|
+
self._automation = automation_intelligence
|
|
61
|
+
self._review_queue = review_queue
|
|
62
|
+
self._enable_graph = bool(enable_graph and knowledge_graph is not None)
|
|
63
|
+
|
|
64
|
+
# ── scoped reads ─────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
def _scope_kwargs(self, workspace_id: Optional[str]) -> Dict[str, Any]:
|
|
67
|
+
return {
|
|
68
|
+
"allowed_workspaces": {workspace_id} if workspace_id is not None else None,
|
|
69
|
+
"include_legacy_global": workspace_id is None,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def _history(
|
|
73
|
+
self, *, user_email: Optional[str], workspace_id: Optional[str], limit: int
|
|
74
|
+
) -> List[Dict[str, Any]]:
|
|
75
|
+
if self._conversations is None:
|
|
76
|
+
return []
|
|
77
|
+
try:
|
|
78
|
+
return list(
|
|
79
|
+
self._conversations.history(
|
|
80
|
+
user_email=user_email,
|
|
81
|
+
limit=limit,
|
|
82
|
+
**self._scope_kwargs(workspace_id),
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
except Exception:
|
|
86
|
+
LOGGER.exception("command center history read failed")
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
def _workflows(self, *, workspace_id: Optional[str]) -> List[Dict[str, Any]]:
|
|
90
|
+
if self._store is None:
|
|
91
|
+
return []
|
|
92
|
+
try:
|
|
93
|
+
return list(self._store.list_workflows(workspace_id=workspace_id).get("workflows") or [])
|
|
94
|
+
except Exception:
|
|
95
|
+
LOGGER.exception("command center workflow read failed")
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
# ── briefing sections ────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
def _knowledge_section(self, *, workspace_id: Optional[str]) -> Dict[str, Any]:
|
|
101
|
+
if not self._enable_graph or not hasattr(self._kg, "graph"):
|
|
102
|
+
return {"available": False, "recent": []}
|
|
103
|
+
try:
|
|
104
|
+
snapshot = self._kg.graph(limit=50, **self._scope_kwargs(workspace_id))
|
|
105
|
+
except Exception:
|
|
106
|
+
LOGGER.exception("command center knowledge read failed")
|
|
107
|
+
return {"available": False, "recent": []}
|
|
108
|
+
nodes = list(snapshot.get("nodes") or [])
|
|
109
|
+
recent = [
|
|
110
|
+
{
|
|
111
|
+
"id": node.get("id"),
|
|
112
|
+
"title": _clip(node.get("title"), 120),
|
|
113
|
+
"type": node.get("type"),
|
|
114
|
+
"updated_at": node.get("updated_at"),
|
|
115
|
+
}
|
|
116
|
+
for node in nodes[:_RECENT_NODE_LIMIT]
|
|
117
|
+
]
|
|
118
|
+
return {"available": True, "recent": recent, "sampled_nodes": len(nodes)}
|
|
119
|
+
|
|
120
|
+
def _conversation_section(
|
|
121
|
+
self, *, user_email: Optional[str], workspace_id: Optional[str]
|
|
122
|
+
) -> Dict[str, Any]:
|
|
123
|
+
items = self._history(
|
|
124
|
+
user_email=user_email, workspace_id=workspace_id, limit=_BRIEFING_HISTORY_LIMIT
|
|
125
|
+
)
|
|
126
|
+
if not items:
|
|
127
|
+
return {"available": self._conversations is not None, "messages": 0, "questions": 0}
|
|
128
|
+
user_items = [item for item in items if item.get("role") == "user"]
|
|
129
|
+
last = items[-1]
|
|
130
|
+
return {
|
|
131
|
+
"available": True,
|
|
132
|
+
"messages": len(items),
|
|
133
|
+
"questions": len(user_items),
|
|
134
|
+
"last_active": str(last.get("timestamp") or ""),
|
|
135
|
+
"last_question": _clip(
|
|
136
|
+
next(
|
|
137
|
+
(item.get("content") for item in reversed(user_items)),
|
|
138
|
+
"",
|
|
139
|
+
),
|
|
140
|
+
120,
|
|
141
|
+
),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
def _automation_section(self, *, workspace_id: Optional[str]) -> Dict[str, Any]:
|
|
145
|
+
if self._store is None:
|
|
146
|
+
return {"available": False, "total": 0, "enabled": 0, "drafts": 0}
|
|
147
|
+
workflows = self._workflows(workspace_id=workspace_id)
|
|
148
|
+
enabled = 0
|
|
149
|
+
drafts = 0
|
|
150
|
+
for workflow in workflows:
|
|
151
|
+
metadata = (workflow or {}).get("metadata") or {}
|
|
152
|
+
if metadata.get("automation_state") == "enabled":
|
|
153
|
+
enabled += 1
|
|
154
|
+
elif metadata.get("automation_state") == "draft_disabled":
|
|
155
|
+
drafts += 1
|
|
156
|
+
return {
|
|
157
|
+
"available": True,
|
|
158
|
+
"total": len(workflows),
|
|
159
|
+
"enabled": enabled,
|
|
160
|
+
"drafts": drafts,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def _review_section(
|
|
164
|
+
self, *, user_email: Optional[str], workspace_id: Optional[str]
|
|
165
|
+
) -> Dict[str, Any]:
|
|
166
|
+
if self._review_queue is None:
|
|
167
|
+
return {"available": False, "pending": 0}
|
|
168
|
+
try:
|
|
169
|
+
listed = self._review_queue.list(
|
|
170
|
+
workspace_id=workspace_id, user_email=user_email, status="pending"
|
|
171
|
+
)
|
|
172
|
+
except Exception:
|
|
173
|
+
LOGGER.exception("command center review read failed")
|
|
174
|
+
return {"available": False, "pending": 0}
|
|
175
|
+
return {"available": True, "pending": len(listed.get("items") or [])}
|
|
176
|
+
|
|
177
|
+
def _health_section(
|
|
178
|
+
self, *, user_email: Optional[str], workspace_id: Optional[str]
|
|
179
|
+
) -> Dict[str, Any]:
|
|
180
|
+
if self._brain is None:
|
|
181
|
+
return {"available": False}
|
|
182
|
+
try:
|
|
183
|
+
report = self._brain.health_report(
|
|
184
|
+
user_email=user_email, workspace_id=workspace_id
|
|
185
|
+
)
|
|
186
|
+
except Exception:
|
|
187
|
+
LOGGER.exception("command center health read failed")
|
|
188
|
+
return {"available": False}
|
|
189
|
+
overall = report.get("overall") or {}
|
|
190
|
+
return {
|
|
191
|
+
"available": overall.get("score") is not None,
|
|
192
|
+
"grade": overall.get("grade"),
|
|
193
|
+
"score": overall.get("score"),
|
|
194
|
+
"recommended_actions": list(report.get("recommended_actions") or [])[:3],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
def _suggestion_section(
|
|
198
|
+
self, *, user_email: Optional[str], workspace_id: Optional[str]
|
|
199
|
+
) -> Dict[str, Any]:
|
|
200
|
+
if self._automation is None:
|
|
201
|
+
return {"available": False, "count": 0, "top": []}
|
|
202
|
+
try:
|
|
203
|
+
payload = self._automation.suggestions(
|
|
204
|
+
user_email=user_email, workspace_id=workspace_id
|
|
205
|
+
)
|
|
206
|
+
except Exception:
|
|
207
|
+
LOGGER.exception("command center suggestion read failed")
|
|
208
|
+
return {"available": False, "count": 0, "top": []}
|
|
209
|
+
suggestions = [
|
|
210
|
+
item
|
|
211
|
+
for item in (payload.get("suggestions") or [])
|
|
212
|
+
if not item.get("installed")
|
|
213
|
+
]
|
|
214
|
+
top = [
|
|
215
|
+
{
|
|
216
|
+
"id": item.get("id"),
|
|
217
|
+
"kind": item.get("kind"),
|
|
218
|
+
"title": _clip(item.get("title"), 120),
|
|
219
|
+
}
|
|
220
|
+
for item in suggestions[:3]
|
|
221
|
+
]
|
|
222
|
+
return {"available": True, "count": len(suggestions), "top": top}
|
|
223
|
+
|
|
224
|
+
# ── quick actions ────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
def _quick_actions(self, sections: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
227
|
+
"""State-derived next steps with stable ids and app hash targets."""
|
|
228
|
+
actions: List[Dict[str, Any]] = []
|
|
229
|
+
review = sections.get("review") or {}
|
|
230
|
+
if review.get("pending"):
|
|
231
|
+
actions.append(
|
|
232
|
+
{
|
|
233
|
+
"id": "review-pending",
|
|
234
|
+
"kind": "review",
|
|
235
|
+
"count": review["pending"],
|
|
236
|
+
"target": "/act/review",
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
automations = sections.get("automations") or {}
|
|
240
|
+
if automations.get("drafts"):
|
|
241
|
+
actions.append(
|
|
242
|
+
{
|
|
243
|
+
"id": "enable-drafts",
|
|
244
|
+
"kind": "automation",
|
|
245
|
+
"count": automations["drafts"],
|
|
246
|
+
"target": "/act/workflows",
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
suggestions = sections.get("suggestions") or {}
|
|
250
|
+
if suggestions.get("count"):
|
|
251
|
+
actions.append(
|
|
252
|
+
{
|
|
253
|
+
"id": "install-suggestion",
|
|
254
|
+
"kind": "suggestion",
|
|
255
|
+
"count": suggestions["count"],
|
|
256
|
+
"target": "/act/workflows",
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
knowledge = sections.get("knowledge") or {}
|
|
260
|
+
if knowledge.get("available") and not knowledge.get("recent"):
|
|
261
|
+
actions.append(
|
|
262
|
+
{
|
|
263
|
+
"id": "connect-knowledge",
|
|
264
|
+
"kind": "capture",
|
|
265
|
+
"count": 0,
|
|
266
|
+
"target": "/capture/files",
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
health = sections.get("health") or {}
|
|
270
|
+
if health.get("available") and isinstance(health.get("score"), (int, float)) and health["score"] < 70:
|
|
271
|
+
actions.append(
|
|
272
|
+
{
|
|
273
|
+
"id": "check-health",
|
|
274
|
+
"kind": "health",
|
|
275
|
+
"count": 0,
|
|
276
|
+
"target": "/brain/graph",
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
if not actions:
|
|
280
|
+
actions.append(
|
|
281
|
+
{"id": "ask-brain", "kind": "chat", "count": 0, "target": "/brain"}
|
|
282
|
+
)
|
|
283
|
+
return actions
|
|
284
|
+
|
|
285
|
+
def briefing(
|
|
286
|
+
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
287
|
+
) -> Dict[str, Any]:
|
|
288
|
+
sections = {
|
|
289
|
+
"knowledge": self._knowledge_section(workspace_id=workspace_id),
|
|
290
|
+
"conversations": self._conversation_section(
|
|
291
|
+
user_email=user_email, workspace_id=workspace_id
|
|
292
|
+
),
|
|
293
|
+
"automations": self._automation_section(workspace_id=workspace_id),
|
|
294
|
+
"review": self._review_section(
|
|
295
|
+
user_email=user_email, workspace_id=workspace_id
|
|
296
|
+
),
|
|
297
|
+
"health": self._health_section(
|
|
298
|
+
user_email=user_email, workspace_id=workspace_id
|
|
299
|
+
),
|
|
300
|
+
"suggestions": self._suggestion_section(
|
|
301
|
+
user_email=user_email, workspace_id=workspace_id
|
|
302
|
+
),
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
"generated_at": _now(),
|
|
306
|
+
"sections": sections,
|
|
307
|
+
"quick_actions": self._quick_actions(sections),
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# ── universal search ─────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
def _search_knowledge(
|
|
313
|
+
self, query: str, *, workspace_id: Optional[str], limit: int
|
|
314
|
+
) -> List[Dict[str, Any]]:
|
|
315
|
+
if self._search is None or not self._enable_graph:
|
|
316
|
+
return []
|
|
317
|
+
try:
|
|
318
|
+
payload = self._search.keyword_search(
|
|
319
|
+
query, limit=limit, **self._scope_kwargs(workspace_id)
|
|
320
|
+
)
|
|
321
|
+
except Exception:
|
|
322
|
+
LOGGER.exception("command center knowledge search failed")
|
|
323
|
+
return []
|
|
324
|
+
return [
|
|
325
|
+
{
|
|
326
|
+
"id": item.get("id"),
|
|
327
|
+
"title": _clip(item.get("title"), 120),
|
|
328
|
+
"summary": _clip(item.get("summary"), 160),
|
|
329
|
+
"type": item.get("type"),
|
|
330
|
+
}
|
|
331
|
+
for item in (payload.get("results") or [])[:limit]
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
def _search_conversations(
|
|
335
|
+
self,
|
|
336
|
+
query: str,
|
|
337
|
+
*,
|
|
338
|
+
user_email: Optional[str],
|
|
339
|
+
workspace_id: Optional[str],
|
|
340
|
+
limit: int,
|
|
341
|
+
) -> List[Dict[str, Any]]:
|
|
342
|
+
needle = query.lower()
|
|
343
|
+
matches: List[Dict[str, Any]] = []
|
|
344
|
+
seen_conversations: set = set()
|
|
345
|
+
items = self._history(
|
|
346
|
+
user_email=user_email, workspace_id=workspace_id, limit=_SEARCH_HISTORY_LIMIT
|
|
347
|
+
)
|
|
348
|
+
for item in reversed(items):
|
|
349
|
+
content = str(item.get("content") or "")
|
|
350
|
+
if needle not in content.lower():
|
|
351
|
+
continue
|
|
352
|
+
conversation_id = str(item.get("conversation_id") or "")
|
|
353
|
+
if conversation_id and conversation_id in seen_conversations:
|
|
354
|
+
continue
|
|
355
|
+
if conversation_id:
|
|
356
|
+
seen_conversations.add(conversation_id)
|
|
357
|
+
matches.append(
|
|
358
|
+
{
|
|
359
|
+
"conversation_id": conversation_id,
|
|
360
|
+
"role": item.get("role"),
|
|
361
|
+
"snippet": _clip(content, 140),
|
|
362
|
+
"timestamp": str(item.get("timestamp") or ""),
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
if len(matches) >= limit:
|
|
366
|
+
break
|
|
367
|
+
return matches
|
|
368
|
+
|
|
369
|
+
def _search_automations(
|
|
370
|
+
self, query: str, *, workspace_id: Optional[str], limit: int
|
|
371
|
+
) -> List[Dict[str, Any]]:
|
|
372
|
+
needle = query.lower()
|
|
373
|
+
matches: List[Dict[str, Any]] = []
|
|
374
|
+
for workflow in self._workflows(workspace_id=workspace_id):
|
|
375
|
+
name = str(workflow.get("name") or "")
|
|
376
|
+
if needle not in name.lower():
|
|
377
|
+
continue
|
|
378
|
+
metadata = (workflow or {}).get("metadata") or {}
|
|
379
|
+
matches.append(
|
|
380
|
+
{
|
|
381
|
+
"id": workflow.get("id"),
|
|
382
|
+
"name": _clip(name, 120),
|
|
383
|
+
"enabled": metadata.get("automation_state") == "enabled",
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
if len(matches) >= limit:
|
|
387
|
+
break
|
|
388
|
+
return matches
|
|
389
|
+
|
|
390
|
+
def search(
|
|
391
|
+
self,
|
|
392
|
+
query: str,
|
|
393
|
+
*,
|
|
394
|
+
user_email: Optional[str] = None,
|
|
395
|
+
workspace_id: Optional[str] = None,
|
|
396
|
+
limit: int = 8,
|
|
397
|
+
) -> Dict[str, Any]:
|
|
398
|
+
query = str(query or "").strip()
|
|
399
|
+
limit = max(1, min(int(limit or 8), 20))
|
|
400
|
+
if not query:
|
|
401
|
+
return {"query": "", "groups": [], "generated_at": _now()}
|
|
402
|
+
groups = [
|
|
403
|
+
{
|
|
404
|
+
"kind": "knowledge",
|
|
405
|
+
"items": self._search_knowledge(
|
|
406
|
+
query, workspace_id=workspace_id, limit=limit
|
|
407
|
+
),
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"kind": "conversation",
|
|
411
|
+
"items": self._search_conversations(
|
|
412
|
+
query,
|
|
413
|
+
user_email=user_email,
|
|
414
|
+
workspace_id=workspace_id,
|
|
415
|
+
limit=limit,
|
|
416
|
+
),
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"kind": "automation",
|
|
420
|
+
"items": self._search_automations(
|
|
421
|
+
query, workspace_id=workspace_id, limit=limit
|
|
422
|
+
),
|
|
423
|
+
},
|
|
424
|
+
]
|
|
425
|
+
return {
|
|
426
|
+
"query": query,
|
|
427
|
+
"groups": [group for group in groups if group["items"]],
|
|
428
|
+
"total": sum(len(group["items"]) for group in groups),
|
|
429
|
+
"generated_at": _now(),
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
__all__ = ["CommandCenterService"]
|
|
@@ -28,7 +28,10 @@ OPEN_STATUSES = {"pending", "snoozed"}
|
|
|
28
28
|
TERMINAL_STATUSES = {"approved", "dismissed"}
|
|
29
29
|
ALL_STATUSES = OPEN_STATUSES | TERMINAL_STATUSES
|
|
30
30
|
|
|
31
|
-
REVIEW_SOURCES = frozenset({
|
|
31
|
+
REVIEW_SOURCES = frozenset({
|
|
32
|
+
"workflow_run", "trigger", "kg_change_digest", "chat_followup",
|
|
33
|
+
"agent_followup", "change_proposal",
|
|
34
|
+
})
|
|
32
35
|
|
|
33
36
|
# Which source statuses each action is allowed from.
|
|
34
37
|
_ALLOWED_FROM: Dict[str, set] = {
|
|
@@ -124,6 +124,11 @@ def _resolve_path(path: str = "") -> Path:
|
|
|
124
124
|
return candidate
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
def resolve_workspace_path(path: str = "") -> Path:
|
|
128
|
+
"""Public alias of the sandboxed workspace path resolver (v9.6.0)."""
|
|
129
|
+
return _resolve_path(path)
|
|
130
|
+
|
|
131
|
+
|
|
127
132
|
def _relative(path: Path) -> str:
|
|
128
133
|
return str(path.relative_to(AGENT_ROOT))
|
|
129
134
|
|
|
@@ -259,7 +264,7 @@ def execute_tool(action: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
259
264
|
|
|
260
265
|
|
|
261
266
|
__all__ = [
|
|
262
|
-
"AGENT_ROOT", "ToolError", "ensure_agent_root",
|
|
267
|
+
"AGENT_ROOT", "ToolError", "ensure_agent_root", "resolve_workspace_path",
|
|
263
268
|
"list_dir", "workspace_tree", "read_file", "write_file", "edit_file", "grep",
|
|
264
269
|
"search_files", "inspect_html", "preview_url", "create_web_project",
|
|
265
270
|
"todo_read", "todo_write",
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Agent-loop evaluation gate (v9.6.0).
|
|
3
|
+
|
|
4
|
+
Runs the deterministic scenario suite in ``latticeai.core.agent_eval``
|
|
5
|
+
against the real SingleAgentRuntime state machine (scripted model, fake
|
|
6
|
+
ports) and fails the release when any scenario regresses.
|
|
7
|
+
|
|
8
|
+
Usage: .venv/bin/python scripts/agent_eval.py [--verbose]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
18
|
+
|
|
19
|
+
from latticeai.core.agent_eval import run_agent_eval # noqa: E402
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> int:
|
|
23
|
+
verbose = "--verbose" in sys.argv
|
|
24
|
+
report = run_agent_eval()
|
|
25
|
+
headline = {
|
|
26
|
+
"scenarios": report["scenarios"],
|
|
27
|
+
"passed": report["passed"],
|
|
28
|
+
"success_rate": report["success_rate"],
|
|
29
|
+
"parse_errors": report["parse_errors"],
|
|
30
|
+
"parse_recovered": report["parse_recovered"],
|
|
31
|
+
"recovery_rate": report["recovery_rate"],
|
|
32
|
+
}
|
|
33
|
+
if verbose:
|
|
34
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
35
|
+
failures = [r for r in report["results"] if not r["ok"]]
|
|
36
|
+
if failures:
|
|
37
|
+
print(f"agent-loop-eval: FAIL {headline}")
|
|
38
|
+
for result in failures:
|
|
39
|
+
print(f" ✗ {result['name']}: {'; '.join(result['failures'])}")
|
|
40
|
+
return 1
|
|
41
|
+
print(f"agent-loop-eval: OK {headline}")
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
raise SystemExit(main())
|
|
@@ -6,11 +6,12 @@ const root = process.cwd();
|
|
|
6
6
|
const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
7
7
|
const version = pkg.version;
|
|
8
8
|
const releaseDir = `output/release/v${version}`;
|
|
9
|
-
const releaseTheme = "
|
|
9
|
+
const releaseTheme = "Trusted Agent Loop";
|
|
10
10
|
const title = `${version} — ${releaseTheme}`;
|
|
11
11
|
const escapedVersion = version.replaceAll(".", "\\.");
|
|
12
12
|
|
|
13
13
|
const currentReleaseFiles = [
|
|
14
|
+
"AGENTS.md",
|
|
14
15
|
"README.md",
|
|
15
16
|
"ARCHITECTURE.md",
|
|
16
17
|
"FEATURE_STATUS.md",
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "9.
|
|
2
|
+
"version": "9.6.0",
|
|
3
3
|
"generated_at": "vite",
|
|
4
4
|
"entrypoints": {
|
|
5
|
-
"app": "/static/app/assets/index-
|
|
5
|
+
"app": "/static/app/assets/index-AF0-4XVv.js"
|
|
6
6
|
},
|
|
7
7
|
"assets": {
|
|
8
8
|
"../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
|
|
9
|
-
"_primitives-
|
|
10
|
-
"_textarea-
|
|
11
|
-
"index.html": "/static/app/assets/index-
|
|
12
|
-
"assets/index-
|
|
13
|
-
"src/pages/Act.tsx": "/static/app/assets/Act-
|
|
14
|
-
"src/pages/Brain.tsx": "/static/app/assets/Brain-
|
|
15
|
-
"src/pages/Capture.tsx": "/static/app/assets/Capture-
|
|
16
|
-
"src/pages/Library.tsx": "/static/app/assets/Library-
|
|
17
|
-
"src/pages/System.tsx": "/static/app/assets/System-
|
|
9
|
+
"_primitives-jbb2qv4Q.js": "/static/app/assets/primitives-jbb2qv4Q.js",
|
|
10
|
+
"_textarea-CFoo0OxJ.js": "/static/app/assets/textarea-CFoo0OxJ.js",
|
|
11
|
+
"index.html": "/static/app/assets/index-AF0-4XVv.js",
|
|
12
|
+
"assets/index-85wQvEie.css": "/static/app/assets/index-85wQvEie.css",
|
|
13
|
+
"src/pages/Act.tsx": "/static/app/assets/Act-BkOEmwBi.js",
|
|
14
|
+
"src/pages/Brain.tsx": "/static/app/assets/Brain-C9ITUsQ_.js",
|
|
15
|
+
"src/pages/Capture.tsx": "/static/app/assets/Capture-C-ppTeud.js",
|
|
16
|
+
"src/pages/Library.tsx": "/static/app/assets/Library-CGQbgWTu.js",
|
|
17
|
+
"src/pages/System.tsx": "/static/app/assets/System-djmj0n2_.js"
|
|
18
18
|
}
|
|
19
19
|
}
|