ltcai 8.9.0 → 9.1.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 +81 -58
- package/auto_setup.py +7 -904
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +221 -238
- package/docs/COMMUNITY_AND_PLUGINS.md +3 -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/ROADMAP_RECOMMENDATIONS.md +61 -36
- 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 +4 -14
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/embeddings.py +38 -2
- package/lattice_brain/graph/_kg_common.py +27 -462
- package/lattice_brain/graph/_kg_constants.py +243 -0
- package/lattice_brain/graph/_kg_fsutil.py +293 -0
- package/lattice_brain/graph/discovery.py +0 -948
- package/lattice_brain/graph/discovery_index.py +1126 -0
- 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 +141 -610
- package/lattice_brain/graph/retrieval_docgen.py +243 -0
- package/lattice_brain/graph/retrieval_vector.py +460 -0
- package/lattice_brain/graph/store.py +6 -0
- package/lattice_brain/ingestion.py +69 -4
- package/lattice_brain/portability.py +29 -23
- package/lattice_brain/quality.py +98 -4
- package/lattice_brain/runtime/agent_runtime.py +169 -7
- 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 +46 -0
- 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 +355 -952
- 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 +250 -0
- package/latticeai/api/chat_history.py +99 -0
- package/latticeai/api/chat_intents.py +302 -0
- package/latticeai/api/chat_stream.py +115 -0
- package/latticeai/api/computer_use.py +211 -40
- 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/marketplace.py +11 -0
- 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 +70 -29
- 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 +97 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +185 -405
- package/latticeai/core/agent.py +19 -9
- 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/invitations.py +1 -4
- package/latticeai/core/io_utils.py +45 -0
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/local_embeddings.py +2 -4
- package/latticeai/core/marketplace.py +33 -2
- package/latticeai/core/mcp_catalog.py +450 -0
- package/latticeai/core/mcp_registry.py +2 -441
- 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 +5 -14
- 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 +2 -21
- 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 +96 -40
- package/latticeai/models/model_providers.py +111 -0
- package/latticeai/models/router.py +189 -173
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/audit_runtime.py +27 -16
- package/latticeai/runtime/automation_runtime.py +22 -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/history_runtime.py +163 -0
- package/latticeai/runtime/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +163 -0
- package/latticeai/runtime/network_config_runtime.py +56 -0
- 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/sso_config_runtime.py +128 -0
- package/latticeai/runtime/stages.py +27 -0
- package/latticeai/runtime/user_key_runtime.py +106 -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 +268 -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 +92 -24
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +76 -11
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +25 -9
- package/latticeai/services/search_service.py +203 -28
- package/latticeai/services/setup_detection.py +80 -0
- 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 +7 -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 -1304
- 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-Bzz0bUyW.js +1 -0
- package/static/app/assets/Brain-Dj2J20YA.js +321 -0
- package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
- package/static/app/assets/Library-B03FP1Yx.js +1 -0
- package/static/app/assets/System-80lHW0Ux.js +1 -0
- package/static/app/assets/index-BiMofBTM.js +17 -0
- package/static/app/assets/index-Bmx9rzTc.css +2 -0
- package/static/app/assets/primitives-Q1A96_7v.js +1 -0
- package/static/app/assets/textarea-D13RtnTo.js +1 -0
- package/static/app/index.html +2 -2
- package/static/css/tokens.css +4 -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/sso_runtime.py +0 -52
- 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-fZokUnC0.js +0 -1
- package/static/app/assets/Brain-DtyuWubr.js +0 -321
- package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
- package/static/app/assets/Library-C9kyFkSt.js +0 -1
- package/static/app/assets/System-VbChmX7r.js +0 -1
- package/static/app/assets/index-DCh5AoXt.css +0 -2
- package/static/app/assets/index-DPdcPoF0.js +0 -17
- package/static/app/assets/primitives-DFeanEV6.js +0 -1
- package/static/app/assets/textarea-CD8UNKIy.js +0 -1
- package/tools/knowledge.py +0 -95
|
@@ -13,14 +13,12 @@ any cloud service. Two complementary mechanisms, both fully local:
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
import hashlib
|
|
17
16
|
import json
|
|
18
17
|
import os
|
|
19
18
|
import shutil
|
|
20
19
|
import sqlite3
|
|
21
20
|
import tempfile
|
|
22
21
|
import zipfile
|
|
23
|
-
from datetime import datetime, timezone
|
|
24
22
|
from pathlib import Path, PurePosixPath
|
|
25
23
|
from typing import Any, Dict, Optional
|
|
26
24
|
|
|
@@ -30,26 +28,16 @@ from .storage import (
|
|
|
30
28
|
PostgresEngine,
|
|
31
29
|
SQLiteToPostgresMigrator,
|
|
32
30
|
)
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _sha256_file(path: Path) -> str:
|
|
48
|
-
h = hashlib.sha256()
|
|
49
|
-
with open(path, "rb") as fh:
|
|
50
|
-
for block in iter(lambda: fh.read(65536), b""):
|
|
51
|
-
h.update(block)
|
|
52
|
-
return h.hexdigest()
|
|
40
|
+
return utc_now_iso().replace(":", "").replace("-", "").replace(".", "")[:15]
|
|
53
41
|
|
|
54
42
|
|
|
55
43
|
def _safe_zip_names(names) -> None:
|
|
@@ -168,15 +156,24 @@ class KGPortabilityService:
|
|
|
168
156
|
raise RuntimeError("Knowledge Graph is disabled (LATTICEAI_ENABLE_GRAPH).")
|
|
169
157
|
|
|
170
158
|
# ── logical export / import ──────────────────────────────────────────────
|
|
171
|
-
def export(
|
|
159
|
+
def export(
|
|
160
|
+
self,
|
|
161
|
+
*,
|
|
162
|
+
workspace_id: Optional[str] = None,
|
|
163
|
+
include_legacy_global: bool = False,
|
|
164
|
+
) -> Dict[str, Any]:
|
|
172
165
|
self._require()
|
|
173
|
-
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
|
+
)
|
|
174
170
|
header = {
|
|
175
171
|
"format": FORMAT,
|
|
176
172
|
"format_version": FORMAT_VERSION,
|
|
177
173
|
**self._kg.schema_versions(),
|
|
178
|
-
"exported_at":
|
|
174
|
+
"exported_at": utc_now_iso(),
|
|
179
175
|
"workspace_id": workspace_id,
|
|
176
|
+
"include_legacy_global": bool(include_legacy_global),
|
|
180
177
|
"counts": data.get("counts"),
|
|
181
178
|
}
|
|
182
179
|
artifact = {"header": header, **data}
|
|
@@ -184,8 +181,17 @@ class KGPortabilityService:
|
|
|
184
181
|
artifact["signature"] = self._identity.sign_manifest(header)
|
|
185
182
|
return artifact
|
|
186
183
|
|
|
187
|
-
def export_to_file(
|
|
188
|
-
|
|
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
|
+
)
|
|
189
195
|
self._exports_dir.mkdir(parents=True, exist_ok=True)
|
|
190
196
|
path = Path(path) if path else self._exports_dir / f"kg-export-{_stamp()}.json"
|
|
191
197
|
path.write_text(json.dumps(artifact, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
@@ -212,7 +218,7 @@ class KGPortabilityService:
|
|
|
212
218
|
if not dry_run:
|
|
213
219
|
try:
|
|
214
220
|
self._kg.record_provenance(
|
|
215
|
-
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()),
|
|
216
222
|
source_type="bundle_import",
|
|
217
223
|
pipeline="kg-portability",
|
|
218
224
|
owner=None,
|
|
@@ -240,7 +246,7 @@ class KGPortabilityService:
|
|
|
240
246
|
"format": BACKUP_FORMAT,
|
|
241
247
|
"format_version": FORMAT_VERSION,
|
|
242
248
|
**self._kg.schema_versions(),
|
|
243
|
-
"created_at":
|
|
249
|
+
"created_at": utc_now_iso(),
|
|
244
250
|
"db_sha256": _sha256_file(db_copy),
|
|
245
251
|
"has_blobs": Path(self._kg.blob_dir).exists(),
|
|
246
252
|
}
|
|
@@ -341,7 +347,7 @@ class KGPortabilityService:
|
|
|
341
347
|
"storage": self.storage_status().get("active", {}),
|
|
342
348
|
"snapshot": self.snapshot_metadata(),
|
|
343
349
|
"device_identity": self._identity.describe() if self._identity is not None else {},
|
|
344
|
-
"provenance": {"exported_at":
|
|
350
|
+
"provenance": {"exported_at": utc_now_iso(), "source": "kg-portability"},
|
|
345
351
|
}
|
|
346
352
|
archive = EncryptedBrainArchive(
|
|
347
353
|
BrainArchivePaths(
|
package/lattice_brain/quality.py
CHANGED
|
@@ -152,6 +152,28 @@ class MemoryCandidate:
|
|
|
152
152
|
conflicts: List[str] = field(default_factory=list)
|
|
153
153
|
|
|
154
154
|
class MemoryQualityManager:
|
|
155
|
+
_NEGATION_PATTERNS = (
|
|
156
|
+
"not",
|
|
157
|
+
"does not",
|
|
158
|
+
"don't",
|
|
159
|
+
"do not",
|
|
160
|
+
"싫어",
|
|
161
|
+
"원하지 않",
|
|
162
|
+
"하지 않",
|
|
163
|
+
"반대",
|
|
164
|
+
)
|
|
165
|
+
_POSITIVE_PATTERNS = (
|
|
166
|
+
"prefers",
|
|
167
|
+
"prefer",
|
|
168
|
+
"likes",
|
|
169
|
+
"like",
|
|
170
|
+
"wants",
|
|
171
|
+
"want",
|
|
172
|
+
"좋아",
|
|
173
|
+
"선호",
|
|
174
|
+
"원해",
|
|
175
|
+
)
|
|
176
|
+
|
|
155
177
|
def extract_candidates(self, memories: List[Dict]) -> List[MemoryCandidate]:
|
|
156
178
|
return [MemoryCandidate(m["id"], m["content"], m.get("score", 0.6), m.get("source", "mem")) for m in memories]
|
|
157
179
|
|
|
@@ -163,10 +185,16 @@ class MemoryQualityManager:
|
|
|
163
185
|
return sorted(cands, key=lambda x: x.score, reverse=True)
|
|
164
186
|
|
|
165
187
|
def dedupe(self, cands: List[MemoryCandidate], threshold: float = 0.85) -> List[MemoryCandidate]:
|
|
188
|
+
"""Advanced content dedup (KG scale slice).
|
|
189
|
+
|
|
190
|
+
Uses sha256 prefix + normalized length bucket for better collision resistance
|
|
191
|
+
than plain md5. Future: plug embedding cosine or simhash here.
|
|
192
|
+
"""
|
|
166
193
|
kept = []
|
|
167
194
|
seen = set()
|
|
168
195
|
for c in cands:
|
|
169
|
-
|
|
196
|
+
norm = " ".join(c.content.lower().split())[:200]
|
|
197
|
+
h = hashlib.sha256((norm + f"|{len(c.content)//50}").encode()).hexdigest()[:16]
|
|
170
198
|
if h not in seen:
|
|
171
199
|
seen.add(h)
|
|
172
200
|
kept.append(c)
|
|
@@ -182,12 +210,78 @@ class MemoryQualityManager:
|
|
|
182
210
|
return list(merged.values())
|
|
183
211
|
|
|
184
212
|
def detect_conflicts(self, cands: List[MemoryCandidate]) -> List[MemoryCandidate]:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
213
|
+
"""Flag local contradiction signals before proactive synthesis.
|
|
214
|
+
|
|
215
|
+
The heuristic stays deterministic/offline: direct negation marks a row
|
|
216
|
+
as risky, and pairwise token overlap catches common preference conflicts
|
|
217
|
+
such as "prefers light mode" vs "does not like light mode".
|
|
218
|
+
"""
|
|
219
|
+
for c in cands:
|
|
220
|
+
lowered = c.content.lower()
|
|
221
|
+
if any(pattern in lowered for pattern in self._NEGATION_PATTERNS):
|
|
188
222
|
c.conflicts.append("conflict:possible_negation")
|
|
223
|
+
|
|
224
|
+
signatures = [(c, self._content_signature(c.content)) for c in cands]
|
|
225
|
+
for left_index, (left, left_sig) in enumerate(signatures):
|
|
226
|
+
for right, right_sig in signatures[left_index + 1:]:
|
|
227
|
+
if not left_sig or len(left_sig & right_sig) < 2:
|
|
228
|
+
continue
|
|
229
|
+
left_negative = self._is_negative(left.content)
|
|
230
|
+
right_negative = self._is_negative(right.content)
|
|
231
|
+
left_positive = self._is_positive(left.content)
|
|
232
|
+
right_positive = self._is_positive(right.content)
|
|
233
|
+
if left_negative == right_negative:
|
|
234
|
+
continue
|
|
235
|
+
if not (left_positive or right_positive):
|
|
236
|
+
continue
|
|
237
|
+
left.conflicts.append(f"conflict:contradicts:{right.id}")
|
|
238
|
+
right.conflicts.append(f"conflict:contradicts:{left.id}")
|
|
189
239
|
return cands
|
|
190
240
|
|
|
241
|
+
def _is_negative(self, content: str) -> bool:
|
|
242
|
+
lowered = content.lower()
|
|
243
|
+
return any(pattern in lowered for pattern in self._NEGATION_PATTERNS)
|
|
244
|
+
|
|
245
|
+
def _is_positive(self, content: str) -> bool:
|
|
246
|
+
lowered = content.lower()
|
|
247
|
+
return any(pattern in lowered for pattern in self._POSITIVE_PATTERNS)
|
|
248
|
+
|
|
249
|
+
def _content_signature(self, content: str) -> set[str]:
|
|
250
|
+
stopwords = {
|
|
251
|
+
"a", "an", "and", "for", "i", "is", "it", "mode", "the", "to",
|
|
252
|
+
"user", "users", "does", "do", "not", "like", "likes", "prefer",
|
|
253
|
+
"prefers", "want", "wants",
|
|
254
|
+
}
|
|
255
|
+
tokens = set(re.findall(r"\w+", content.lower()))
|
|
256
|
+
return {token for token in tokens if len(token) > 2 and token not in stopwords}
|
|
257
|
+
|
|
258
|
+
# --- Large candidate #4 slice: proactive / temporal contradiction detection ---
|
|
259
|
+
def detect_temporal_contradictions(self, memories: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
260
|
+
"""Simple temporal + contradiction detector over memory list (proactive synthesis extension).
|
|
261
|
+
|
|
262
|
+
Looks at negation keywords across similar contents + differing timestamps.
|
|
263
|
+
Real version would walk graph historical projections + LLM judge.
|
|
264
|
+
Returns flagged items with 'contradiction' tag.
|
|
265
|
+
"""
|
|
266
|
+
flagged = []
|
|
267
|
+
neg_tokens = ("not ", "반대", "거짓", "no longer", "never", "중단", "사용하지")
|
|
268
|
+
pos_group = []
|
|
269
|
+
neg_group = []
|
|
270
|
+
for m in memories:
|
|
271
|
+
c = (m.get("content") or "").lower()
|
|
272
|
+
if any(t in c for t in neg_tokens):
|
|
273
|
+
neg_group.append(m)
|
|
274
|
+
else:
|
|
275
|
+
pos_group.append(m)
|
|
276
|
+
# cross check: if both pos and neg exist with time spread
|
|
277
|
+
all_t = [m.get("timestamp") or m.get("created_at") or 0 for m in memories]
|
|
278
|
+
if neg_group and pos_group and len(set(all_t)) > 1:
|
|
279
|
+
for g in (pos_group + neg_group):
|
|
280
|
+
gg = dict(g)
|
|
281
|
+
gg["proactive_flag"] = "contradiction:temporal_negation"
|
|
282
|
+
flagged.append(gg)
|
|
283
|
+
return flagged
|
|
284
|
+
|
|
191
285
|
def apply_retention(self, cands: List[MemoryCandidate], max_age_days: int = 90) -> List[MemoryCandidate]:
|
|
192
286
|
now = time.time()
|
|
193
287
|
return [c for c in cands if (now - c.timestamp) < max_age_days * 86400]
|
|
@@ -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,12 +61,8 @@ 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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _now() -> str:
|
|
65
|
-
return datetime.now().isoformat(timespec="seconds")
|
|
64
|
+
def _compact_text(value: Any, *, limit: int) -> str:
|
|
65
|
+
return " ".join(str(value or "").split())[:limit]
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
class AgentRuntimeUnavailable(RuntimeError):
|
|
@@ -80,6 +80,8 @@ class AgentRuntime:
|
|
|
80
80
|
max_retries_cap: int = 5,
|
|
81
81
|
hooks: Any = None,
|
|
82
82
|
allow_simulation_runs: bool = False,
|
|
83
|
+
memory_ingest: Optional[Callable[..., Dict[str, Any]]] = None,
|
|
84
|
+
review_sink: Any = None,
|
|
83
85
|
):
|
|
84
86
|
self._store = store
|
|
85
87
|
self._orchestrator_factory = orchestrator_factory
|
|
@@ -91,6 +93,11 @@ class AgentRuntime:
|
|
|
91
93
|
self._hooks = hooks
|
|
92
94
|
self._allow_simulation_runs = bool(allow_simulation_runs)
|
|
93
95
|
self._run_executor: Any = None
|
|
96
|
+
# Optional memory synthesis: successful agent runs produce durable
|
|
97
|
+
# Brain memories (long_term / workspace tier) so users *feel* the results
|
|
98
|
+
# in BrainBrief, MemoryRings, search and graph immediately.
|
|
99
|
+
self._memory_ingest = memory_ingest
|
|
100
|
+
self._review_sink = review_sink
|
|
94
101
|
|
|
95
102
|
def attach_executor(self, executor: Any) -> None:
|
|
96
103
|
self._run_executor = executor
|
|
@@ -415,6 +422,155 @@ class AgentRuntime:
|
|
|
415
422
|
"current_role": None,
|
|
416
423
|
}
|
|
417
424
|
|
|
425
|
+
def _synthesize_brain_memory(self, *, goal: str, result: Any, user_email: Optional[str], scope: Optional[str]) -> None:
|
|
426
|
+
"""Turn a successful agent run into durable Brain memory + graph nodes.
|
|
427
|
+
|
|
428
|
+
This is the key to users *strongly feeling* the scale of agent work:
|
|
429
|
+
delegated goals don't disappear into Act tab; they become part of the
|
|
430
|
+
Living Brain (searchable, visible in rings/brief, connected in graph).
|
|
431
|
+
"""
|
|
432
|
+
if not self._memory_ingest:
|
|
433
|
+
return
|
|
434
|
+
if getattr(result, "status", None) not in ("ok", "retried_ok"):
|
|
435
|
+
return
|
|
436
|
+
try:
|
|
437
|
+
output = _compact_text(getattr(result, "output", ""), limit=2200)
|
|
438
|
+
plan_steps = getattr(result, "plan", None) or []
|
|
439
|
+
sections = self._agent_synthesis_sections(goal=goal, output=output, plan_steps=plan_steps, result=result)
|
|
440
|
+
plan_summary = "; ".join(sections["plan_steps"][:4])
|
|
441
|
+
content = "\n\n".join([
|
|
442
|
+
f"[Agent synthesis] Goal: {_compact_text(goal, limit=240)}",
|
|
443
|
+
f"Outcome: {output}",
|
|
444
|
+
self._format_synthesis_section("Key facts", sections["facts"]),
|
|
445
|
+
self._format_synthesis_section("Decisions", sections["decisions"]),
|
|
446
|
+
self._format_synthesis_section("Follow-ups", sections["followups"]),
|
|
447
|
+
f"Plan: {plan_summary}" if plan_summary else "",
|
|
448
|
+
]).strip()
|
|
449
|
+
if not content or len(content) < 20:
|
|
450
|
+
return
|
|
451
|
+
tags = ["agent-synthesis", "delegated", "auto"]
|
|
452
|
+
self._memory_ingest(
|
|
453
|
+
kind="long_term",
|
|
454
|
+
content=content,
|
|
455
|
+
user_email=user_email,
|
|
456
|
+
tags=tags,
|
|
457
|
+
metadata={
|
|
458
|
+
"source": "agent_runtime",
|
|
459
|
+
"synthesis_version": 2,
|
|
460
|
+
"goal": _compact_text(goal, limit=200),
|
|
461
|
+
"roles": getattr(result, "roles_run", None),
|
|
462
|
+
"facts": sections["facts"],
|
|
463
|
+
"decisions": sections["decisions"],
|
|
464
|
+
"followups": sections["followups"],
|
|
465
|
+
},
|
|
466
|
+
graph=self._workspace_graph() if hasattr(self, "_workspace_graph") else None,
|
|
467
|
+
workspace_id=scope,
|
|
468
|
+
)
|
|
469
|
+
if sections["decisions"]:
|
|
470
|
+
self._memory_ingest(
|
|
471
|
+
kind="decisions",
|
|
472
|
+
content=f"Agent decision for {_compact_text(goal, limit=120)}: {'; '.join(sections['decisions'][:3])}",
|
|
473
|
+
user_email=user_email,
|
|
474
|
+
tags=["agent", "outcome", "decision"],
|
|
475
|
+
metadata={"source": "agent_runtime_synthesis", "synthesis_version": 2, "goal": _compact_text(goal, limit=200)},
|
|
476
|
+
workspace_id=scope,
|
|
477
|
+
)
|
|
478
|
+
if sections["followups"]:
|
|
479
|
+
self._memory_ingest(
|
|
480
|
+
kind="workspace",
|
|
481
|
+
content=f"Agent follow-ups for {_compact_text(goal, limit=120)}: {'; '.join(sections['followups'][:5])}",
|
|
482
|
+
user_email=user_email,
|
|
483
|
+
tags=["agent", "follow-up", "next-action"],
|
|
484
|
+
metadata={"source": "agent_runtime_followups", "synthesis_version": 2, "goal": _compact_text(goal, limit=200)},
|
|
485
|
+
workspace_id=scope,
|
|
486
|
+
)
|
|
487
|
+
self._enqueue_agent_followups(
|
|
488
|
+
goal=goal,
|
|
489
|
+
followups=sections["followups"],
|
|
490
|
+
result=result,
|
|
491
|
+
user_email=user_email,
|
|
492
|
+
scope=scope,
|
|
493
|
+
output=output,
|
|
494
|
+
)
|
|
495
|
+
except Exception:
|
|
496
|
+
# Synthesis must never break the run record.
|
|
497
|
+
pass
|
|
498
|
+
|
|
499
|
+
@staticmethod
|
|
500
|
+
def _format_synthesis_section(title: str, items: List[str]) -> str:
|
|
501
|
+
if not items:
|
|
502
|
+
return ""
|
|
503
|
+
return f"{title}:\n" + "\n".join(f"- {item}" for item in items[:5])
|
|
504
|
+
|
|
505
|
+
@staticmethod
|
|
506
|
+
def _agent_synthesis_sections(*, goal: str, output: str, plan_steps: List[Dict[str, Any]], result: Any) -> Dict[str, List[str]]:
|
|
507
|
+
plan_descriptions = [
|
|
508
|
+
_compact_text(step.get("description") or step.get("name") or step.get("id"), limit=120)
|
|
509
|
+
for step in plan_steps
|
|
510
|
+
if isinstance(step, dict) and (step.get("description") or step.get("name") or step.get("id"))
|
|
511
|
+
]
|
|
512
|
+
sentences = [
|
|
513
|
+
sentence.strip(" -•\t")
|
|
514
|
+
for sentence in output.replace("\n", ". ").split(".")
|
|
515
|
+
if sentence.strip(" -•\t")
|
|
516
|
+
]
|
|
517
|
+
facts = [_compact_text(sentence, limit=180) for sentence in sentences[:4]]
|
|
518
|
+
review = getattr(result, "review", None) if isinstance(getattr(result, "review", None), dict) else {}
|
|
519
|
+
plan_review = getattr(result, "plan_review", None) if isinstance(getattr(result, "plan_review", None), dict) else {}
|
|
520
|
+
decision_seed = review.get("decision") or review.get("status") or plan_review.get("decision") or getattr(result, "status", "")
|
|
521
|
+
decisions = [
|
|
522
|
+
_compact_text(f"Run finished with {decision_seed}", limit=160),
|
|
523
|
+
_compact_text(f"Goal accepted: {goal}", limit=180),
|
|
524
|
+
]
|
|
525
|
+
followups = [
|
|
526
|
+
item for item in plan_descriptions
|
|
527
|
+
if any(token in item.lower() for token in ("next", "follow", "review", "verify", "test", "ship", "publish", "document", "implement"))
|
|
528
|
+
][:5]
|
|
529
|
+
if not followups:
|
|
530
|
+
followups = plan_descriptions[:3]
|
|
531
|
+
return {
|
|
532
|
+
"facts": [item for item in facts if item],
|
|
533
|
+
"decisions": [item for item in decisions if item],
|
|
534
|
+
"followups": [item for item in followups if item],
|
|
535
|
+
"plan_steps": plan_descriptions,
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
def _enqueue_agent_followups(
|
|
539
|
+
self,
|
|
540
|
+
*,
|
|
541
|
+
goal: str,
|
|
542
|
+
followups: List[str],
|
|
543
|
+
result: Any,
|
|
544
|
+
user_email: Optional[str],
|
|
545
|
+
scope: Optional[str],
|
|
546
|
+
output: str,
|
|
547
|
+
) -> None:
|
|
548
|
+
if self._review_sink is None:
|
|
549
|
+
return
|
|
550
|
+
for index, followup in enumerate(followups[:5], start=1):
|
|
551
|
+
try:
|
|
552
|
+
self._review_sink.create(
|
|
553
|
+
title=_compact_text(followup, limit=96) or f"Agent follow-up {index}",
|
|
554
|
+
summary=_compact_text(f"From goal: {goal}", limit=420),
|
|
555
|
+
source="agent_followup",
|
|
556
|
+
kind="task_draft",
|
|
557
|
+
payload={
|
|
558
|
+
"goal": _compact_text(goal, limit=300),
|
|
559
|
+
"followup": _compact_text(followup, limit=300),
|
|
560
|
+
"output_preview": _compact_text(output, limit=800),
|
|
561
|
+
"roles": getattr(result, "roles_run", None),
|
|
562
|
+
},
|
|
563
|
+
provenance={
|
|
564
|
+
"agent_id": getattr(result, "agent_id", ""),
|
|
565
|
+
"source_detail": "agent_runtime_followup",
|
|
566
|
+
"status": getattr(result, "status", ""),
|
|
567
|
+
},
|
|
568
|
+
user_email=user_email,
|
|
569
|
+
workspace_id=scope,
|
|
570
|
+
)
|
|
571
|
+
except Exception:
|
|
572
|
+
continue
|
|
573
|
+
|
|
418
574
|
def _post_run_hooks(
|
|
419
575
|
self,
|
|
420
576
|
*,
|
|
@@ -572,6 +728,9 @@ class AgentRuntime:
|
|
|
572
728
|
status=updated.get("status") or result.status,
|
|
573
729
|
retries=result.retries,
|
|
574
730
|
)
|
|
731
|
+
# Large-scale user-visible impact: successful runs enrich the Brain permanently.
|
|
732
|
+
if (updated.get("status") or result.status) in ("ok", "retried_ok"):
|
|
733
|
+
self._synthesize_brain_memory(goal=goal, result=result, user_email=user_email, scope=scope)
|
|
575
734
|
post_dispatch = self._post_run_hooks(
|
|
576
735
|
run_id=run_id,
|
|
577
736
|
result=result,
|
|
@@ -651,6 +810,9 @@ class AgentRuntime:
|
|
|
651
810
|
status=result.status,
|
|
652
811
|
retries=result.retries,
|
|
653
812
|
)
|
|
813
|
+
# Large-scale user-visible impact: successful runs enrich the Brain permanently.
|
|
814
|
+
if result.status in ("ok", "retried_ok"):
|
|
815
|
+
self._synthesize_brain_memory(goal=goal, result=result, user_email=user_email, scope=scope)
|
|
654
816
|
|
|
655
817
|
run_id = run.get("id") or run.get("run_id") if isinstance(run, dict) else None
|
|
656
818
|
post_dispatch = self._post_run_hooks(
|
|
@@ -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 = "
|
|
24
|
+
MULTI_AGENT_VERSION = "9.1.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"]
|