ltcai 9.0.0 → 9.2.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 +88 -46
- package/auto_setup.py +7 -853
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +178 -2
- package/docs/COMMUNITY_AND_PLUGINS.md +4 -2
- package/docs/DEVELOPMENT.md +53 -14
- package/docs/LEGACY_COMPATIBILITY.md +4 -4
- package/docs/ONBOARDING.md +10 -2
- package/docs/OPERATIONS.md +8 -4
- package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
- package/docs/TRUST_MODEL.md +5 -2
- package/docs/WHY_LATTICE.md +15 -10
- package/docs/WORKFLOW_DESIGNER.md +22 -0
- package/docs/kg-schema.md +13 -2
- package/docs/mcp-tools.md +17 -6
- package/docs/privacy.md +19 -3
- package/docs/public-deploy.md +32 -3
- package/docs/security-model.md +46 -11
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +1 -6
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/graph/_kg_fsutil.py +1 -5
- package/lattice_brain/graph/discovery_index.py +174 -20
- package/lattice_brain/graph/documents.py +44 -9
- package/lattice_brain/graph/ingest.py +47 -20
- package/lattice_brain/graph/provenance.py +13 -6
- package/lattice_brain/graph/retrieval.py +140 -39
- package/lattice_brain/graph/retrieval_docgen.py +37 -4
- package/lattice_brain/ingestion.py +4 -7
- package/lattice_brain/portability.py +28 -14
- package/lattice_brain/runtime/agent_runtime.py +5 -9
- package/lattice_brain/runtime/hooks.py +30 -13
- package/lattice_brain/runtime/multi_agent.py +27 -8
- package/lattice_brain/runtime/statuses.py +10 -0
- package/lattice_brain/utils.py +20 -2
- package/lattice_brain/workflow.py +1 -5
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agent_registry.py +10 -8
- package/latticeai/api/agents.py +22 -3
- package/latticeai/api/auth.py +78 -16
- package/latticeai/api/browser.py +303 -34
- package/latticeai/api/chat.py +320 -850
- package/latticeai/api/chat_agent_http.py +356 -0
- package/latticeai/api/chat_contracts.py +54 -0
- package/latticeai/api/chat_documents.py +256 -0
- package/latticeai/api/chat_helpers.py +28 -1
- package/latticeai/api/chat_history.py +99 -0
- package/latticeai/api/chat_intents.py +316 -0
- package/latticeai/api/chat_stream.py +115 -0
- package/latticeai/api/computer_use.py +62 -9
- package/latticeai/api/health.py +9 -3
- package/latticeai/api/hooks.py +17 -6
- package/latticeai/api/knowledge_graph.py +78 -14
- package/latticeai/api/local_files.py +7 -2
- package/latticeai/api/mcp.py +102 -23
- package/latticeai/api/models.py +72 -33
- package/latticeai/api/network.py +5 -5
- package/latticeai/api/permissions.py +67 -26
- package/latticeai/api/plugins.py +21 -7
- package/latticeai/api/portability.py +5 -5
- package/latticeai/api/realtime.py +33 -5
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +123 -24
- package/latticeai/api/tools.py +96 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +110 -52
- package/latticeai/core/agent.py +50 -13
- package/latticeai/core/agent_prompts.py +5 -0
- package/latticeai/core/agent_registry.py +1 -5
- package/latticeai/core/config.py +23 -3
- package/latticeai/core/context_builder.py +21 -3
- package/latticeai/core/file_generation.py +451 -0
- package/latticeai/core/invitations.py +1 -4
- package/latticeai/core/io_utils.py +9 -1
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/plugins.py +15 -0
- package/latticeai/core/policy.py +1 -1
- package/latticeai/core/realtime.py +38 -15
- package/latticeai/core/sessions.py +7 -2
- package/latticeai/core/timeutil.py +10 -0
- package/latticeai/core/tool_registry.py +90 -18
- package/latticeai/core/users.py +1 -5
- package/latticeai/core/workspace_graph_trace.py +31 -5
- package/latticeai/core/workspace_memory.py +3 -1
- package/latticeai/core/workspace_os.py +18 -7
- package/latticeai/core/workspace_os_utils.py +0 -5
- package/latticeai/core/workspace_permissions.py +2 -1
- package/latticeai/core/workspace_plugins.py +1 -1
- package/latticeai/core/workspace_runs.py +14 -5
- package/latticeai/core/workspace_skills.py +1 -1
- package/latticeai/core/workspace_snapshots.py +2 -1
- package/latticeai/core/workspace_timeline.py +2 -1
- package/latticeai/integrations/telegram_bot.py +94 -43
- package/latticeai/models/router.py +130 -36
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/automation_runtime.py +13 -7
- package/latticeai/runtime/brain_runtime.py +19 -7
- package/latticeai/runtime/chat_wiring.py +2 -0
- package/latticeai/runtime/config_runtime.py +58 -26
- package/latticeai/runtime/context_runtime.py +16 -4
- package/latticeai/runtime/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +62 -72
- package/latticeai/runtime/platform_runtime_wiring.py +4 -3
- package/latticeai/runtime/review_wiring.py +1 -1
- package/latticeai/runtime/router_registration.py +34 -1
- package/latticeai/runtime/security_runtime.py +110 -13
- package/latticeai/runtime/stages.py +27 -0
- package/latticeai/server_app.py +5 -1
- package/latticeai/services/architecture_readiness.py +74 -5
- package/latticeai/services/brain_automation.py +3 -26
- package/latticeai/services/chat_service.py +205 -15
- package/latticeai/services/local_knowledge.py +423 -0
- package/latticeai/services/memory_service.py +55 -21
- package/latticeai/services/model_engines.py +48 -33
- package/latticeai/services/model_errors.py +17 -0
- package/latticeai/services/model_loading.py +28 -25
- package/latticeai/services/model_runtime.py +228 -162
- package/latticeai/services/p_reinforce.py +22 -3
- package/latticeai/services/platform_runtime.py +83 -23
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +12 -0
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +4 -9
- package/latticeai/services/search_service.py +203 -28
- package/latticeai/services/tool_dispatch.py +28 -5
- package/latticeai/services/triggers.py +53 -4
- package/latticeai/services/upload_service.py +13 -2
- package/latticeai/setup/__init__.py +25 -0
- package/latticeai/setup/auto_setup.py +857 -0
- package/latticeai/setup/wizard.py +1264 -0
- package/latticeai/tools/__init__.py +278 -0
- package/{tools → latticeai/tools}/commands.py +67 -7
- package/{tools → latticeai/tools}/computer.py +1 -1
- package/{tools → latticeai/tools}/documents.py +1 -1
- package/{tools → latticeai/tools}/filesystem.py +3 -3
- package/latticeai/tools/knowledge.py +178 -0
- package/{tools → latticeai/tools}/local_files.py +1 -1
- package/{tools → latticeai/tools}/network.py +0 -1
- package/local_knowledge_api.py +4 -342
- package/package.json +22 -2
- package/scripts/brain_quality_eval.py +3 -1
- package/scripts/bump_version.py +3 -0
- package/scripts/capture_release_evidence.mjs +180 -0
- package/scripts/check_current_release_docs.mjs +142 -0
- package/scripts/check_i18n_literals.mjs +107 -31
- package/scripts/check_openapi_drift.mjs +56 -0
- package/scripts/export_openapi.py +67 -7
- package/scripts/i18n_literal_allowlist.json +22 -24
- package/scripts/run_integration_tests.mjs +72 -10
- package/scripts/validate_release_artifacts.py +49 -7
- package/scripts/wheel_smoke.py +5 -0
- package/setup_wizard.py +3 -1260
- 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-C3dBrWE-.js +1 -0
- package/static/app/assets/Brain-DBYgdcjt.js +321 -0
- package/static/app/assets/Capture-Cf3hqRtN.js +1 -0
- package/static/app/assets/Library-CFfkNn3s.js +1 -0
- package/static/app/assets/System-BOurbT-v.js +1 -0
- package/static/app/assets/index-A3M9sElj.js +17 -0
- package/static/app/assets/index-Bmx9rzTc.css +2 -0
- package/static/app/assets/primitives-DcUUmhdC.js +1 -0
- package/static/app/assets/textarea-BklR6zN4.js +1 -0
- package/static/app/index.html +2 -2
- package/static/sw.js +1 -1
- package/tools/__init__.py +21 -269
- package/docs/CODE_REVIEW_2026-07-06.md +0 -764
- package/latticeai/runtime/app_context_runtime.py +0 -13
- package/latticeai/runtime/tail_wiring.py +0 -21
- package/scripts/com.pts.claudecode.discord.plist +0 -31
- package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
- package/scripts/start-pts-claudecode-discord.sh +0 -51
- package/static/app/assets/Act-21lIXx2E.js +0 -1
- package/static/app/assets/Brain-BqUd5UJJ.js +0 -321
- package/static/app/assets/Capture-BA7Z2Q1u.js +0 -1
- package/static/app/assets/Library-bFMtyni3.js +0 -1
- package/static/app/assets/System-K6krGCqn.js +0 -1
- package/static/app/assets/index-C4R3ws30.js +0 -17
- package/static/app/assets/index-ChSeOB02.css +0 -2
- package/static/app/assets/primitives-sQU3it5I.js +0 -1
- package/static/app/assets/textarea-DK3Fd_lR.js +0 -1
- package/tools/knowledge.py +0 -95
|
@@ -19,7 +19,6 @@ import shutil
|
|
|
19
19
|
import sqlite3
|
|
20
20
|
import tempfile
|
|
21
21
|
import zipfile
|
|
22
|
-
from datetime import datetime, timezone
|
|
23
22
|
from pathlib import Path, PurePosixPath
|
|
24
23
|
from typing import Any, Dict, Optional
|
|
25
24
|
|
|
@@ -30,18 +29,15 @@ from .storage import (
|
|
|
30
29
|
SQLiteToPostgresMigrator,
|
|
31
30
|
)
|
|
32
31
|
from .utils import sha256_file as _sha256_file
|
|
32
|
+
from .utils import utc_now_iso
|
|
33
33
|
|
|
34
34
|
FORMAT = "latticeai.kg.export"
|
|
35
35
|
FORMAT_VERSION = 1
|
|
36
36
|
BACKUP_FORMAT = "latticeai.kg.backup"
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def _now_iso() -> str:
|
|
40
|
-
return datetime.now(timezone.utc).isoformat()
|
|
41
|
-
|
|
42
|
-
|
|
43
39
|
def _stamp() -> str:
|
|
44
|
-
return
|
|
40
|
+
return utc_now_iso().replace(":", "").replace("-", "").replace(".", "")[:15]
|
|
45
41
|
|
|
46
42
|
|
|
47
43
|
def _safe_zip_names(names) -> None:
|
|
@@ -160,15 +156,24 @@ class KGPortabilityService:
|
|
|
160
156
|
raise RuntimeError("Knowledge Graph is disabled (LATTICEAI_ENABLE_GRAPH).")
|
|
161
157
|
|
|
162
158
|
# ── logical export / import ──────────────────────────────────────────────
|
|
163
|
-
def export(
|
|
159
|
+
def export(
|
|
160
|
+
self,
|
|
161
|
+
*,
|
|
162
|
+
workspace_id: Optional[str] = None,
|
|
163
|
+
include_legacy_global: bool = False,
|
|
164
|
+
) -> Dict[str, Any]:
|
|
164
165
|
self._require()
|
|
165
|
-
data = self._kg.export_graph_data(
|
|
166
|
+
data = self._kg.export_graph_data(
|
|
167
|
+
workspace_id=workspace_id,
|
|
168
|
+
include_legacy_global=include_legacy_global,
|
|
169
|
+
)
|
|
166
170
|
header = {
|
|
167
171
|
"format": FORMAT,
|
|
168
172
|
"format_version": FORMAT_VERSION,
|
|
169
173
|
**self._kg.schema_versions(),
|
|
170
|
-
"exported_at":
|
|
174
|
+
"exported_at": utc_now_iso(),
|
|
171
175
|
"workspace_id": workspace_id,
|
|
176
|
+
"include_legacy_global": bool(include_legacy_global),
|
|
172
177
|
"counts": data.get("counts"),
|
|
173
178
|
}
|
|
174
179
|
artifact = {"header": header, **data}
|
|
@@ -176,8 +181,17 @@ class KGPortabilityService:
|
|
|
176
181
|
artifact["signature"] = self._identity.sign_manifest(header)
|
|
177
182
|
return artifact
|
|
178
183
|
|
|
179
|
-
def export_to_file(
|
|
180
|
-
|
|
184
|
+
def export_to_file(
|
|
185
|
+
self,
|
|
186
|
+
path=None,
|
|
187
|
+
*,
|
|
188
|
+
workspace_id: Optional[str] = None,
|
|
189
|
+
include_legacy_global: bool = False,
|
|
190
|
+
) -> Dict[str, Any]:
|
|
191
|
+
artifact = self.export(
|
|
192
|
+
workspace_id=workspace_id,
|
|
193
|
+
include_legacy_global=include_legacy_global,
|
|
194
|
+
)
|
|
181
195
|
self._exports_dir.mkdir(parents=True, exist_ok=True)
|
|
182
196
|
path = Path(path) if path else self._exports_dir / f"kg-export-{_stamp()}.json"
|
|
183
197
|
path.write_text(json.dumps(artifact, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
@@ -204,7 +218,7 @@ class KGPortabilityService:
|
|
|
204
218
|
if not dry_run:
|
|
205
219
|
try:
|
|
206
220
|
self._kg.record_provenance(
|
|
207
|
-
node_id="import:" + str((artifact.get("header") or {}).get("exported_at") or
|
|
221
|
+
node_id="import:" + str((artifact.get("header") or {}).get("exported_at") or utc_now_iso()),
|
|
208
222
|
source_type="bundle_import",
|
|
209
223
|
pipeline="kg-portability",
|
|
210
224
|
owner=None,
|
|
@@ -232,7 +246,7 @@ class KGPortabilityService:
|
|
|
232
246
|
"format": BACKUP_FORMAT,
|
|
233
247
|
"format_version": FORMAT_VERSION,
|
|
234
248
|
**self._kg.schema_versions(),
|
|
235
|
-
"created_at":
|
|
249
|
+
"created_at": utc_now_iso(),
|
|
236
250
|
"db_sha256": _sha256_file(db_copy),
|
|
237
251
|
"has_blobs": Path(self._kg.blob_dir).exists(),
|
|
238
252
|
}
|
|
@@ -333,7 +347,7 @@ class KGPortabilityService:
|
|
|
333
347
|
"storage": self.storage_status().get("active", {}),
|
|
334
348
|
"snapshot": self.snapshot_metadata(),
|
|
335
349
|
"device_identity": self._identity.describe() if self._identity is not None else {},
|
|
336
|
-
"provenance": {"exported_at":
|
|
350
|
+
"provenance": {"exported_at": utc_now_iso(), "source": "kg-portability"},
|
|
337
351
|
}
|
|
338
352
|
archive = EncryptedBrainArchive(
|
|
339
353
|
BrainArchivePaths(
|
|
@@ -28,7 +28,6 @@ it, the frontend) now depends on this boundary instead of internal paths.
|
|
|
28
28
|
|
|
29
29
|
from __future__ import annotations
|
|
30
30
|
|
|
31
|
-
from datetime import datetime
|
|
32
31
|
from typing import Any, Callable, Dict, List, Optional
|
|
33
32
|
|
|
34
33
|
from .multi_agent import (
|
|
@@ -45,6 +44,11 @@ from .contracts import (
|
|
|
45
44
|
run_record_contract,
|
|
46
45
|
runtime_boundary_contract,
|
|
47
46
|
)
|
|
47
|
+
from .statuses import (
|
|
48
|
+
RUN_ACTIVE_STATUSES as _ACTIVE_STATUSES,
|
|
49
|
+
RUN_TERMINAL_STATUSES as _TERMINAL_STATUSES,
|
|
50
|
+
)
|
|
51
|
+
from ..utils import now_iso as _now
|
|
48
52
|
|
|
49
53
|
ROLE_DESCRIPTIONS = {
|
|
50
54
|
"researcher": "Gathers workspace context and memory for the goal.",
|
|
@@ -57,14 +61,6 @@ ROLE_DESCRIPTIONS = {
|
|
|
57
61
|
# Run statuses the orchestrator can emit that mean "still working". The default
|
|
58
62
|
# orchestrator runs synchronously, so persisted runs are always terminal; this
|
|
59
63
|
# set lets the runtime report live work if a future async runner lands.
|
|
60
|
-
_ACTIVE_STATUSES = {"running", "in_progress", "queued", "retrying", "cancelling"}
|
|
61
|
-
_TERMINAL_STATUSES = {"ok", "retried_ok", "failed", "rejected", "cancelled", "interrupted", "partial"}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _now() -> str:
|
|
65
|
-
return datetime.now().isoformat(timespec="seconds")
|
|
66
|
-
|
|
67
|
-
|
|
68
64
|
def _compact_text(value: Any, *, limit: int) -> str:
|
|
69
65
|
return " ".join(str(value or "").split())[:limit]
|
|
70
66
|
|
|
@@ -25,6 +25,7 @@ order.
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
27
|
import json
|
|
28
|
+
import logging
|
|
28
29
|
import os
|
|
29
30
|
import shlex
|
|
30
31
|
import subprocess
|
|
@@ -32,10 +33,14 @@ import tempfile
|
|
|
32
33
|
import threading
|
|
33
34
|
import time
|
|
34
35
|
from collections import deque
|
|
35
|
-
from datetime import datetime
|
|
36
36
|
from pathlib import Path
|
|
37
37
|
from typing import Any, Callable, Dict, List, Optional
|
|
38
38
|
|
|
39
|
+
from ..utils import now_iso as _now
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
LOGGER = logging.getLogger(__name__)
|
|
43
|
+
|
|
39
44
|
|
|
40
45
|
HOOK_KINDS = (
|
|
41
46
|
"pre_run",
|
|
@@ -63,10 +68,6 @@ LEGACY_KIND_ALIASES = {
|
|
|
63
68
|
HOOK_STATUSES = ("ok", "blocked", "error", "skipped", "advisory")
|
|
64
69
|
|
|
65
70
|
|
|
66
|
-
def _now() -> str:
|
|
67
|
-
return datetime.now().isoformat(timespec="seconds")
|
|
68
|
-
|
|
69
|
-
|
|
70
71
|
class HookContext:
|
|
71
72
|
"""Mutable execution context handed to every hook in a dispatch.
|
|
72
73
|
|
|
@@ -213,7 +214,8 @@ def dispatch_tool(
|
|
|
213
214
|
return run_fn()
|
|
214
215
|
try:
|
|
215
216
|
arg_keys = list(args.keys()) if isinstance(args, dict) else []
|
|
216
|
-
except Exception:
|
|
217
|
+
except Exception as exc:
|
|
218
|
+
LOGGER.debug("tool argument metadata could not be inspected: %s", exc)
|
|
217
219
|
arg_keys = []
|
|
218
220
|
pre = hooks.fire_hook(
|
|
219
221
|
"pre_tool", f"tool.{tool_name}",
|
|
@@ -341,8 +343,8 @@ class HooksRegistry:
|
|
|
341
343
|
data.setdefault("custom", [])
|
|
342
344
|
data.setdefault("overrides", {})
|
|
343
345
|
return data
|
|
344
|
-
except
|
|
345
|
-
|
|
346
|
+
except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
347
|
+
LOGGER.warning("hook registry state is unreadable at %s: %s", self._path, exc)
|
|
346
348
|
return {"custom": [], "overrides": {}}
|
|
347
349
|
|
|
348
350
|
def _save(self) -> None:
|
|
@@ -626,6 +628,7 @@ class HooksRegistry:
|
|
|
626
628
|
user_email=user_email, workspace_id=workspace_id, metadata=metadata,
|
|
627
629
|
)
|
|
628
630
|
except Exception as exc: # pragma: no cover - defensive
|
|
631
|
+
LOGGER.exception("hook dispatch failed before execution: %s", kind)
|
|
629
632
|
return {"kind": kind, "event": event or kind, "ran": 0, "blocked": False,
|
|
630
633
|
"block_reason": "", "error": str(exc), "results": [], "generated_at": _now()}
|
|
631
634
|
|
|
@@ -694,7 +697,21 @@ class HooksRegistry:
|
|
|
694
697
|
if not argv:
|
|
695
698
|
return "skipped", "empty command", "", False
|
|
696
699
|
ctx_json = json.dumps(context.as_dict(), ensure_ascii=False)
|
|
697
|
-
|
|
700
|
+
# Command hooks run with an intentionally small environment. In
|
|
701
|
+
# particular, provider keys, database credentials, session secrets,
|
|
702
|
+
# and arbitrary PYTHON*/NODE* injection variables from the server
|
|
703
|
+
# process must never be inherited by a child process. The retained
|
|
704
|
+
# entries are the minimum needed for executable lookup, home-relative
|
|
705
|
+
# tools, locale handling, temporary files, and Windows process startup.
|
|
706
|
+
allowed_env_keys = {
|
|
707
|
+
"PATH", "HOME", "LANG", "LC_ALL", "LC_CTYPE",
|
|
708
|
+
"TMPDIR", "TMP", "TEMP", "SYSTEMROOT", "WINDIR", "PATHEXT",
|
|
709
|
+
}
|
|
710
|
+
env = {
|
|
711
|
+
key: value
|
|
712
|
+
for key, value in os.environ.items()
|
|
713
|
+
if key.upper() in allowed_env_keys
|
|
714
|
+
}
|
|
698
715
|
env.update({
|
|
699
716
|
"LATTICE_HOOK_KIND": context.kind,
|
|
700
717
|
"LATTICE_HOOK_EVENT": context.event,
|
|
@@ -726,8 +743,8 @@ class HooksRegistry:
|
|
|
726
743
|
data = json.load(fh)
|
|
727
744
|
if isinstance(data, list):
|
|
728
745
|
return data
|
|
729
|
-
except
|
|
730
|
-
|
|
746
|
+
except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
747
|
+
LOGGER.warning("hook run history is unreadable at %s: %s", self._runs_path, exc)
|
|
731
748
|
return []
|
|
732
749
|
|
|
733
750
|
def _save_runs(self) -> None:
|
|
@@ -737,8 +754,8 @@ class HooksRegistry:
|
|
|
737
754
|
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
738
755
|
json.dump(list(self._runs), fh, ensure_ascii=False, indent=2)
|
|
739
756
|
os.replace(tmp, self._runs_path)
|
|
740
|
-
except
|
|
741
|
-
|
|
757
|
+
except OSError as exc: # pragma: no cover - the run log is best-effort
|
|
758
|
+
LOGGER.warning("hook run history write failed at %s: %s", self._runs_path, exc)
|
|
742
759
|
|
|
743
760
|
def _record_run(self, result: HookResult, context: HookContext) -> None:
|
|
744
761
|
entry = result.as_dict()
|
|
@@ -15,13 +15,13 @@ installations can exercise the full Planner -> Executor -> Reviewer loop.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
from dataclasses import dataclass, field
|
|
18
|
-
from datetime import datetime
|
|
19
18
|
from typing import Any, Callable, Dict, List, Optional
|
|
20
19
|
|
|
21
20
|
from .contracts import multi_agent_contract
|
|
21
|
+
from ..utils import now_iso as _now
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
MULTI_AGENT_VERSION = "9.
|
|
24
|
+
MULTI_AGENT_VERSION = "9.2.0"
|
|
25
25
|
|
|
26
26
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
27
27
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
|
@@ -50,10 +50,6 @@ REVIEW_OUTCOMES = ("approve", "reject", "retry")
|
|
|
50
50
|
_SECRET_KEYS = ("secret", "token", "password", "api_key", "apikey", "credential")
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def _now() -> str:
|
|
54
|
-
return datetime.now().isoformat(timespec="seconds")
|
|
55
|
-
|
|
56
|
-
|
|
57
53
|
def _redact(value: Any) -> Any:
|
|
58
54
|
"""Return a JSON-safe value with obvious secret fields redacted."""
|
|
59
55
|
if isinstance(value, dict):
|
|
@@ -668,7 +664,7 @@ class MultiAgentOrchestrator:
|
|
|
668
664
|
result = self.role_runner(role, ctx) or {}
|
|
669
665
|
status = result.get("status", "ok")
|
|
670
666
|
except Exception as exc:
|
|
671
|
-
result = {"error": str(exc)}
|
|
667
|
+
result = {"status": "error", "error": str(exc)}
|
|
672
668
|
status = "error"
|
|
673
669
|
if role == "reviewer":
|
|
674
670
|
review = dict(ctx.review or result)
|
|
@@ -739,9 +735,32 @@ class MultiAgentOrchestrator:
|
|
|
739
735
|
role = pipeline[index]
|
|
740
736
|
if previous is not None:
|
|
741
737
|
ctx.handoff(previous, role)
|
|
742
|
-
self._run_role(role, ctx)
|
|
738
|
+
role_result = self._run_role(role, ctx)
|
|
743
739
|
roles_run.append(role)
|
|
744
740
|
|
|
741
|
+
# A role exception is terminal for this attempt. Continuing would
|
|
742
|
+
# let downstream roles review stale or missing output and could
|
|
743
|
+
# incorrectly convert a failed run into an approved one.
|
|
744
|
+
if str(role_result.get("status") or "").lower() == "error":
|
|
745
|
+
reason = str(role_result.get("error") or f"{role} role failed")
|
|
746
|
+
existing_review = dict(ctx.review or {})
|
|
747
|
+
ctx.review = existing_review or {
|
|
748
|
+
"outcome": "reject",
|
|
749
|
+
"verdict": "fail",
|
|
750
|
+
"reason": reason,
|
|
751
|
+
"notes": [f"{role} did not complete"],
|
|
752
|
+
"reviewed_at": _now(),
|
|
753
|
+
}
|
|
754
|
+
ctx.timeline.append(
|
|
755
|
+
{
|
|
756
|
+
"event": "execution_failed",
|
|
757
|
+
"role": role,
|
|
758
|
+
"reason": reason,
|
|
759
|
+
"timestamp": _now(),
|
|
760
|
+
}
|
|
761
|
+
)
|
|
762
|
+
break
|
|
763
|
+
|
|
745
764
|
if role == "reviewer":
|
|
746
765
|
review = dict(ctx.review or {})
|
|
747
766
|
outcome = _review_outcome(review)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Canonical lifecycle states shared by persisted agent and workflow runs."""
|
|
2
|
+
|
|
3
|
+
RUN_ACTIVE_STATUSES = frozenset(
|
|
4
|
+
{"queued", "running", "in_progress", "retrying", "cancelling"}
|
|
5
|
+
)
|
|
6
|
+
RUN_TERMINAL_STATUSES = frozenset(
|
|
7
|
+
{"ok", "retried_ok", "failed", "rejected", "cancelled", "interrupted", "partial"}
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = ["RUN_ACTIVE_STATUSES", "RUN_TERMINAL_STATUSES"]
|
package/lattice_brain/utils.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
|
-
from datetime import datetime
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
@@ -17,6 +17,24 @@ def parse_iso(value: Optional[str]) -> Optional[datetime]:
|
|
|
17
17
|
return None
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def local_now() -> datetime:
|
|
21
|
+
"""Return local wall-clock time for legacy local persistence formats."""
|
|
22
|
+
|
|
23
|
+
return datetime.now()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def now_iso(*, timespec: str = "seconds") -> str:
|
|
27
|
+
"""Return one consistently formatted local timestamp."""
|
|
28
|
+
|
|
29
|
+
return local_now().isoformat(timespec=timespec)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def utc_now_iso(*, timespec: str = "auto") -> str:
|
|
33
|
+
"""Return an offset-aware UTC timestamp."""
|
|
34
|
+
|
|
35
|
+
return datetime.now(timezone.utc).isoformat(timespec=timespec)
|
|
36
|
+
|
|
37
|
+
|
|
20
38
|
def sha256_file(path: Path) -> str:
|
|
21
39
|
digest = hashlib.sha256()
|
|
22
40
|
with open(path, "rb") as fh:
|
|
@@ -25,4 +43,4 @@ def sha256_file(path: Path) -> str:
|
|
|
25
43
|
return digest.hexdigest()
|
|
26
44
|
|
|
27
45
|
|
|
28
|
-
__all__ = ["parse_iso", "sha256_file"]
|
|
46
|
+
__all__ = ["local_now", "now_iso", "parse_iso", "sha256_file", "utc_now_iso"]
|
|
@@ -24,10 +24,10 @@ a linear node chain so existing workflow history keeps working.
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
26
|
from dataclasses import dataclass, field
|
|
27
|
-
from datetime import datetime
|
|
28
27
|
from typing import Any, Callable, Dict, List, Optional
|
|
29
28
|
|
|
30
29
|
from lattice_brain.runtime.contracts import runtime_boundary_contract, workflow_run_contract
|
|
30
|
+
from lattice_brain.utils import now_iso as _now
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
WORKFLOW_ENGINE_VERSION = "2.2.0"
|
|
@@ -59,10 +59,6 @@ class WorkflowError(Exception):
|
|
|
59
59
|
"""Raised for invalid workflow definitions."""
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
def _now() -> str:
|
|
63
|
-
return datetime.now().isoformat(timespec="seconds")
|
|
64
|
-
|
|
65
|
-
|
|
66
62
|
def normalize_definition(workflow: Dict[str, Any]) -> Dict[str, Any]:
|
|
67
63
|
"""Return a node-based definition, lifting legacy ``steps`` lists if needed.
|
|
68
64
|
|
package/latticeai/__init__.py
CHANGED
package/latticeai/api/admin.py
CHANGED
|
@@ -259,7 +259,7 @@ def create_admin_router(
|
|
|
259
259
|
{"id": "model_egress", "label": "Model egress",
|
|
260
260
|
"value": "Local-only by default (no external inference in local mode)", "enforced": True},
|
|
261
261
|
{"id": "invite_gate", "label": "Invite gate",
|
|
262
|
-
"value": "
|
|
262
|
+
"value": "Signed access gate" if invite_gate_enabled else "Disabled",
|
|
263
263
|
"enforced": bool(invite_gate_enabled)},
|
|
264
264
|
{"id": "log_retention", "label": "Log retention",
|
|
265
265
|
"value": "90 day local audit window with manual export before pruning", "enforced": True},
|
|
@@ -14,6 +14,7 @@ from fastapi import APIRouter, HTTPException, Request
|
|
|
14
14
|
from pydantic import BaseModel
|
|
15
15
|
|
|
16
16
|
from latticeai.core.agent_registry import AgentRegistry
|
|
17
|
+
from latticeai.core.security import redact_secrets
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class AgentRegisterRequest(BaseModel):
|
|
@@ -34,6 +35,7 @@ def create_agent_registry_router(
|
|
|
34
35
|
*,
|
|
35
36
|
registry: AgentRegistry,
|
|
36
37
|
require_user: Callable[[Request], str],
|
|
38
|
+
require_admin: Callable[[Request], Any],
|
|
37
39
|
append_audit_event: Callable[..., None],
|
|
38
40
|
) -> APIRouter:
|
|
39
41
|
router = APIRouter()
|
|
@@ -41,7 +43,7 @@ def create_agent_registry_router(
|
|
|
41
43
|
@router.get("/agents/api/registry")
|
|
42
44
|
async def list_registry(request: Request, type: Optional[str] = None):
|
|
43
45
|
require_user(request)
|
|
44
|
-
return registry.list(agent_type=type)
|
|
46
|
+
return redact_secrets(registry.list(agent_type=type))
|
|
45
47
|
|
|
46
48
|
@router.get("/agents/api/registry/capabilities")
|
|
47
49
|
async def registry_capabilities(request: Request):
|
|
@@ -51,11 +53,11 @@ def create_agent_registry_router(
|
|
|
51
53
|
@router.get("/agents/api/registry/discover")
|
|
52
54
|
async def registry_discover(request: Request, capability: str = ""):
|
|
53
55
|
require_user(request)
|
|
54
|
-
return {"capability": capability, "agents": registry.discover(capability)}
|
|
56
|
+
return {"capability": capability, "agents": redact_secrets(registry.discover(capability))}
|
|
55
57
|
|
|
56
58
|
@router.post("/agents/api/registry")
|
|
57
59
|
async def register_agent(req: AgentRegisterRequest, request: Request):
|
|
58
|
-
user =
|
|
60
|
+
user, _ = require_admin(request)
|
|
59
61
|
try:
|
|
60
62
|
entry = registry.register(
|
|
61
63
|
name=req.name,
|
|
@@ -68,7 +70,7 @@ def create_agent_registry_router(
|
|
|
68
70
|
except ValueError as exc:
|
|
69
71
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
70
72
|
append_audit_event("agent_register", user_email=user, agent_id=entry["id"], type=entry["type"])
|
|
71
|
-
return {"agent": entry}
|
|
73
|
+
return {"agent": redact_secrets(entry)}
|
|
72
74
|
|
|
73
75
|
@router.get("/agents/api/registry/{agent_id:path}")
|
|
74
76
|
async def get_agent(agent_id: str, request: Request):
|
|
@@ -76,21 +78,21 @@ def create_agent_registry_router(
|
|
|
76
78
|
agent = registry.get(agent_id)
|
|
77
79
|
if agent is None:
|
|
78
80
|
raise HTTPException(status_code=404, detail=f"Agent not found: {agent_id}")
|
|
79
|
-
return {"agent": agent}
|
|
81
|
+
return {"agent": redact_secrets(agent)}
|
|
80
82
|
|
|
81
83
|
@router.patch("/agents/api/registry/{agent_id:path}")
|
|
82
84
|
async def update_agent(agent_id: str, req: AgentConfigRequest, request: Request):
|
|
83
|
-
user =
|
|
85
|
+
user, _ = require_admin(request)
|
|
84
86
|
try:
|
|
85
87
|
agent = registry.update_config(agent_id, req.config, enabled=req.enabled)
|
|
86
88
|
except KeyError as exc:
|
|
87
89
|
raise HTTPException(status_code=404, detail=f"Agent not found: {agent_id}") from exc
|
|
88
90
|
append_audit_event("agent_config", user_email=user, agent_id=agent_id)
|
|
89
|
-
return {"agent": agent}
|
|
91
|
+
return {"agent": redact_secrets(agent)}
|
|
90
92
|
|
|
91
93
|
@router.delete("/agents/api/registry/{agent_id:path}")
|
|
92
94
|
async def remove_agent(agent_id: str, request: Request):
|
|
93
|
-
user =
|
|
95
|
+
user, _ = require_admin(request)
|
|
94
96
|
try:
|
|
95
97
|
result = registry.remove(agent_id)
|
|
96
98
|
except KeyError as exc:
|
package/latticeai/api/agents.py
CHANGED
|
@@ -17,6 +17,24 @@ from pydantic import BaseModel
|
|
|
17
17
|
from latticeai.api.ui_redirects import app_redirect
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
_CORE_EXECUTION_ROLES = ["planner", "executor", "reviewer"]
|
|
21
|
+
_MEMORY_GROUNDED_ROLES = ["researcher", *_CORE_EXECUTION_ROLES]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _memory_grounded_roles(roles: List[str]) -> Optional[List[str]]:
|
|
25
|
+
"""Ground standard user-initiated agent runs in Brain recall first.
|
|
26
|
+
|
|
27
|
+
Explicit specialist/custom pipelines keep their requested shape. The
|
|
28
|
+
desktop Brain and Work surfaces both send the historical three-role core
|
|
29
|
+
sequence, so normalizing it at the API boundary upgrades existing clients
|
|
30
|
+
without a frontend-only compatibility branch.
|
|
31
|
+
"""
|
|
32
|
+
requested = list(roles or _CORE_EXECUTION_ROLES)
|
|
33
|
+
if requested == _CORE_EXECUTION_ROLES:
|
|
34
|
+
return list(_MEMORY_GROUNDED_ROLES)
|
|
35
|
+
return requested or None
|
|
36
|
+
|
|
37
|
+
|
|
20
38
|
class AgentRunRequest(BaseModel):
|
|
21
39
|
goal: str
|
|
22
40
|
roles: List[str] = []
|
|
@@ -160,13 +178,14 @@ def create_agents_router(
|
|
|
160
178
|
async def agent_run(req: AgentRunRequest, request: Request):
|
|
161
179
|
current_user = require_user(request)
|
|
162
180
|
scope = gate_write(request)
|
|
181
|
+
grounded_roles = _memory_grounded_roles(req.roles)
|
|
163
182
|
try:
|
|
164
183
|
if run_executor is not None:
|
|
165
184
|
return await run_executor.start_agent(
|
|
166
185
|
req.goal,
|
|
167
186
|
user_email=current_user or None,
|
|
168
187
|
scope=scope,
|
|
169
|
-
roles=
|
|
188
|
+
roles=grounded_roles,
|
|
170
189
|
inputs=req.inputs,
|
|
171
190
|
max_retries=req.max_retries,
|
|
172
191
|
)
|
|
@@ -180,7 +199,7 @@ def create_agents_router(
|
|
|
180
199
|
req.goal,
|
|
181
200
|
user_email=current_user or None,
|
|
182
201
|
scope=scope,
|
|
183
|
-
roles=
|
|
202
|
+
roles=grounded_roles,
|
|
184
203
|
inputs=req.inputs,
|
|
185
204
|
max_retries=req.max_retries,
|
|
186
205
|
)
|
|
@@ -199,7 +218,7 @@ def create_agents_router(
|
|
|
199
218
|
return runtime.preview(
|
|
200
219
|
req.goal,
|
|
201
220
|
scope=scope,
|
|
202
|
-
roles=req.roles
|
|
221
|
+
roles=_memory_grounded_roles(req.roles),
|
|
203
222
|
inputs=req.inputs,
|
|
204
223
|
max_retries=req.max_retries,
|
|
205
224
|
)
|