ltcai 10.0.0 → 10.3.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 +43 -32
- package/docs/CHANGELOG.md +204 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/HYBRID_CLOUD_KG_STREAMING.md +101 -0
- package/docs/MYPY_BACKLOG.md +98 -0
- package/docs/ONBOARDING.md +1 -1
- 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/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +5 -4
- package/lattice_brain/conversations.py +14 -3
- package/lattice_brain/embeddings.py +12 -2
- package/lattice_brain/graph/_kg_common.py +5 -5
- package/lattice_brain/graph/_kg_fsutil.py +5 -4
- package/lattice_brain/graph/discovery.py +4 -3
- package/lattice_brain/graph/discovery_index.py +0 -1
- package/lattice_brain/graph/documents.py +3 -2
- package/lattice_brain/graph/fusion.py +4 -0
- package/lattice_brain/graph/ingest.py +12 -2
- package/lattice_brain/graph/projection.py +3 -2
- package/lattice_brain/graph/provenance.py +5 -3
- package/lattice_brain/graph/rerank.py +4 -1
- package/lattice_brain/graph/retrieval.py +0 -1
- package/lattice_brain/graph/retrieval_docgen.py +0 -1
- package/lattice_brain/graph/retrieval_reads.py +0 -1
- package/lattice_brain/graph/retrieval_vector.py +0 -1
- package/lattice_brain/graph/schema.py +4 -3
- package/lattice_brain/graph/store.py +18 -4
- package/lattice_brain/graph/write_master.py +46 -1
- package/lattice_brain/ingestion.py +4 -1
- package/lattice_brain/portability.py +5 -2
- package/lattice_brain/quality.py +12 -5
- package/lattice_brain/quiet.py +43 -0
- package/lattice_brain/runtime/agent_runtime.py +19 -10
- package/lattice_brain/runtime/hooks.py +3 -2
- package/lattice_brain/runtime/multi_agent.py +2 -3
- package/lattice_brain/sensitivity.py +94 -0
- package/lattice_brain/storage/base.py +30 -2
- package/lattice_brain/storage/migration.py +3 -2
- package/lattice_brain/storage/postgres.py +2 -2
- package/lattice_brain/storage/sqlite.py +6 -4
- package/lattice_brain/workflow.py +4 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agents.py +4 -2
- package/latticeai/api/auth.py +5 -1
- package/latticeai/api/automation_intelligence.py +2 -1
- package/latticeai/api/browser.py +3 -2
- package/latticeai/api/chat.py +28 -17
- package/latticeai/api/chat_agent_http.py +22 -8
- package/latticeai/api/chat_contracts.py +4 -0
- package/latticeai/api/chat_documents.py +6 -2
- package/latticeai/api/chat_hybrid.py +82 -0
- package/latticeai/api/computer_use.py +8 -3
- package/latticeai/api/knowledge_graph.py +1 -1
- package/latticeai/api/mcp.py +4 -4
- package/latticeai/api/models.py +5 -2
- package/latticeai/api/network_boundary.py +220 -0
- package/latticeai/api/permissions.py +0 -1
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/security_dashboard.py +1 -1
- package/latticeai/api/setup.py +16 -3
- package/latticeai/api/static_routes.py +2 -1
- package/latticeai/api/tools.py +12 -8
- package/latticeai/api/voice_capture.py +3 -1
- package/latticeai/api/workflow_designer.py +1 -1
- package/latticeai/api/workspace.py +1 -2
- package/latticeai/app_factory.py +174 -145
- package/latticeai/cli/entrypoint.py +6 -4
- package/latticeai/core/agent.py +55 -495
- package/latticeai/core/agent_eval.py +2 -2
- package/latticeai/core/agent_helpers.py +493 -0
- package/latticeai/core/agent_prompts.py +0 -1
- package/latticeai/core/agent_registry.py +3 -1
- package/latticeai/core/agent_state.py +41 -0
- package/latticeai/core/audit.py +1 -1
- package/latticeai/core/builtin_hooks.py +2 -1
- package/latticeai/core/config.py +0 -1
- package/latticeai/core/embedding_providers.py +12 -1
- package/latticeai/core/file_generation.py +3 -0
- package/latticeai/core/invitations.py +4 -1
- package/latticeai/core/io_utils.py +3 -1
- package/latticeai/core/legacy_compatibility.py +1 -2
- package/latticeai/core/local_embeddings.py +12 -1
- package/latticeai/core/marketplace.py +1 -2
- package/latticeai/core/mcp_registry.py +0 -1
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +1 -1
- package/latticeai/core/network_boundary.py +168 -0
- package/latticeai/core/oidc.py +3 -0
- package/latticeai/core/permission_mode.py +6 -6
- package/latticeai/core/plugins.py +3 -2
- package/latticeai/core/policy.py +0 -1
- package/latticeai/core/quiet.py +84 -0
- package/latticeai/core/realtime.py +3 -2
- package/latticeai/core/run_store.py +4 -2
- package/latticeai/core/security.py +4 -0
- package/latticeai/core/users.py +5 -3
- package/latticeai/core/workspace_graph_trace.py +3 -0
- package/latticeai/core/workspace_os.py +65 -273
- package/latticeai/core/workspace_os_constants.py +126 -0
- package/latticeai/core/workspace_os_state.py +180 -0
- package/latticeai/core/workspace_os_utils.py +3 -1
- package/latticeai/core/workspace_permissions.py +1 -0
- package/latticeai/core/workspace_snapshots.py +3 -1
- package/latticeai/core/workspace_timeline.py +3 -1
- package/latticeai/integrations/telegram_bot.py +25 -16
- package/latticeai/models/router.py +6 -3
- package/latticeai/runtime/access_runtime.py +3 -1
- package/latticeai/runtime/audit_runtime.py +3 -2
- package/latticeai/runtime/chat_wiring.py +4 -1
- package/latticeai/runtime/history_runtime.py +1 -1
- package/latticeai/runtime/history_writer.py +138 -0
- package/latticeai/runtime/lifespan_runtime.py +3 -1
- package/latticeai/runtime/network_boundary_wiring.py +124 -0
- package/latticeai/runtime/persistence_runtime.py +3 -1
- package/latticeai/runtime/router_registration.py +11 -1
- package/latticeai/services/architecture_readiness.py +1 -2
- package/latticeai/services/change_proposals.py +2 -1
- package/latticeai/services/cloud_egress_audit.py +85 -0
- package/latticeai/services/cloud_extraction.py +129 -0
- package/latticeai/services/cloud_streaming.py +266 -0
- package/latticeai/services/cloud_token_guard.py +84 -0
- package/latticeai/services/command_center.py +2 -1
- package/latticeai/services/folder_watch.py +3 -0
- package/latticeai/services/funnel_metrics.py +3 -1
- package/latticeai/services/hybrid_chat.py +265 -0
- package/latticeai/services/hybrid_context.py +228 -0
- package/latticeai/services/hybrid_policy.py +178 -0
- package/latticeai/services/memory_service.py +1 -1
- package/latticeai/services/model_catalog.py +10 -1
- package/latticeai/services/model_engines.py +35 -14
- package/latticeai/services/model_loading.py +3 -2
- package/latticeai/services/model_runtime.py +68 -17
- package/latticeai/services/multimodal_streaming.py +123 -0
- package/latticeai/services/network_boundary_service.py +154 -0
- package/latticeai/services/openai_compatible_adapter.py +100 -0
- package/latticeai/services/p_reinforce.py +4 -0
- package/latticeai/services/platform_runtime.py +9 -4
- package/latticeai/services/process_audit.py +0 -1
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/run_executor.py +3 -2
- package/latticeai/services/search_service.py +1 -4
- package/latticeai/services/setup_detection.py +2 -1
- package/latticeai/services/tool_dispatch.py +7 -2
- package/latticeai/services/upload_service.py +2 -1
- package/latticeai/setup/auto_setup.py +10 -7
- package/latticeai/setup/wizard.py +15 -11
- package/latticeai/tools/__init__.py +3 -3
- package/latticeai/tools/commands.py +7 -7
- package/latticeai/tools/computer.py +3 -2
- package/latticeai/tools/documents.py +10 -9
- package/latticeai/tools/filesystem.py +7 -4
- package/latticeai/tools/knowledge.py +2 -0
- package/latticeai/tools/local_files.py +1 -1
- package/latticeai/tools/network.py +2 -1
- package/package.json +7 -3
- package/scripts/bench_agent_smoke.py +0 -1
- package/scripts/bench_models.py +0 -1
- package/scripts/brain_quality_eval.py +7 -2
- package/scripts/bump_version.py +2 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/migrate_brain_storage.py +5 -1
- package/scripts/profile_kg.py +2 -7
- package/scripts/verify_hf_model_registry.py +2 -2
- 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 +37 -37
- package/static/app/assets/{Act-BtCREeN1.js → Act-DrKIGZeJ.js} +1 -1
- package/static/app/assets/{AdminConsole-TPeeN18T.js → AdminConsole-CEV1VefA.js} +1 -1
- package/static/app/assets/{Brain-BKs6JAp0.js → Brain-BZSXnwWU.js} +1 -1
- package/static/app/assets/{BrainHome-BPGOvSd6.js → BrainHome-CEBd86k6.js} +1 -1
- package/static/app/assets/{BrainSignals-CtzQZ15J.js → BrainSignals-O4s6R7av.js} +1 -1
- package/static/app/assets/{Capture-1_NaHWqB.js → Capture-AGbOxsg-.js} +1 -1
- package/static/app/assets/{CommandPalette-pqvQOXe4.js → CommandPalette-CeqBjCn2.js} +1 -1
- package/static/app/assets/Library-CXnbNiQW.js +1 -0
- package/static/app/assets/{LivingBrain-DlQ20Q75.js → LivingBrain-D9Iei2ZJ.js} +1 -1
- package/static/app/assets/{ProductFlow-BZvGDRi_.js → ProductFlow-JlXiDfMM.js} +1 -1
- package/static/app/assets/{ReviewCard-BWgI0D2s.js → ReviewCard-QQly2bTV.js} +1 -1
- package/static/app/assets/System-Bp9ZY4ER.js +1 -0
- package/static/app/assets/{activity-Dlfk8YC7.js → activity-CxRPOQ2F.js} +1 -1
- package/static/app/assets/{bot-CDvUB76P.js → bot-1bcsWB2s.js} +1 -1
- package/static/app/assets/{brain-xczrohrt.js → brain-CxaeKCRM.js} +1 -1
- package/static/app/assets/{button-SOdH3Oyf.js → button-ZYFqGan7.js} +1 -1
- package/static/app/assets/{circle-pause-CG1ythH4.js → circle-pause-B5NOIqvk.js} +1 -1
- package/static/app/assets/{circle-play-HXwvjS6W.js → circle-play-BiJVFXf7.js} +1 -1
- package/static/app/assets/{cpu-B0d-rGyk.js → cpu-Vvq0OKTr.js} +1 -1
- package/static/app/assets/{download-BGIkTQL6.js → download-SEcXzkx1.js} +1 -1
- package/static/app/assets/{folder-open-Dst_Z0_K.js → folder-open-Dw1ff_Qa.js} +1 -1
- package/static/app/assets/{hard-drive-D53MsWkV.js → hard-drive-B00fR7_r.js} +1 -1
- package/static/app/assets/index-CEkL4_M-.css +2 -0
- package/static/app/assets/{index-C_IrlQMV.js → index-Dm9VXl1o.js} +3 -3
- package/static/app/assets/{input-C5m0riF6.js → input-DtdWYQEa.js} +1 -1
- package/static/app/assets/{network-C5a-E5iS.js → network-CpjBu0kI.js} +1 -1
- package/static/app/assets/{primitives-vNXYf58F.js → primitives-CwsudBwc.js} +1 -1
- package/static/app/assets/search-CwEJWBEA.js +1 -0
- package/static/app/assets/{shield-alert-Cc-WVXqN.js → shield-alert-CpjBc-gQ.js} +1 -1
- package/static/app/assets/{textarea-BkZ0EqVO.js → textarea-DUPyjHQs.js} +1 -1
- package/static/app/assets/{useFocusTrap-CTtKbAOU.js → useFocusTrap-BWcMtCWH.js} +1 -1
- package/static/app/assets/{useQuery-Dx1fi4Wu.js → useQuery-Dg5fPqTS.js} +1 -1
- package/static/app/assets/{users-BFpQXtEF.js → users-BgcdTQGM.js} +1 -1
- package/static/app/assets/{utils-BA_lmW3J.js → utils-a3YXFtEO.js} +2 -2
- package/static/app/assets/workspace-BMUEiXUm.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Library-DhvoPvC7.js +0 -1
- package/static/app/assets/System-CSMdYLMy.js +0 -1
- package/static/app/assets/index-FxDusbr0.css +0 -2
- package/static/app/assets/search-DhbSgW6m.js +0 -1
- package/static/app/assets/workspace-DBPB0jkX.js +0 -1
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""Pure helpers for the single-agent runtime.
|
|
2
|
+
|
|
3
|
+
Extracted from :mod:`latticeai.core.agent` so the state machine module
|
|
4
|
+
owns only the loop. Every function here is deterministic and free of
|
|
5
|
+
I/O / side effects (aside from reading environment in the budget
|
|
6
|
+
``from_env`` constructors).
|
|
7
|
+
|
|
8
|
+
Public names are re-exported from :mod:`latticeai.core.agent` so callers
|
|
9
|
+
keep importing from the original location.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, FrozenSet, List, Mapping, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
from latticeai.core.agent_state import AgentState
|
|
22
|
+
from latticeai.core.file_generation import infer_file_target, infer_project_manifest
|
|
23
|
+
|
|
24
|
+
# ── action parsing ─────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
_THINK_BLOCK_RE = re.compile(
|
|
27
|
+
r"<(think|thinking|reasoning)>.*?</\1>", flags=re.DOTALL | re.IGNORECASE
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def extract_action_details(raw: str) -> Tuple[Dict, List[str]]:
|
|
32
|
+
"""Parse one JSON action object out of an LLM response (tolerant of fences/prose).
|
|
33
|
+
|
|
34
|
+
Returns ``(action, repairs)`` where ``repairs`` names every tolerance that
|
|
35
|
+
was needed — the loop trace and the weak-model robustness harness consume
|
|
36
|
+
it to measure how much help a given model needs.
|
|
37
|
+
"""
|
|
38
|
+
repairs: List[str] = []
|
|
39
|
+
# Small local models often prepend <think>...</think> reasoning that can
|
|
40
|
+
# itself contain braces — drop it before locating the action object.
|
|
41
|
+
text = _THINK_BLOCK_RE.sub("", raw).strip()
|
|
42
|
+
if text != str(raw).strip():
|
|
43
|
+
repairs.append("think_strip")
|
|
44
|
+
fenced = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=re.DOTALL)
|
|
45
|
+
if fenced:
|
|
46
|
+
text = fenced.group(1).strip()
|
|
47
|
+
repairs.append("fence")
|
|
48
|
+
elif not text.startswith("{"):
|
|
49
|
+
start = text.find("{")
|
|
50
|
+
end = text.rfind("}")
|
|
51
|
+
if start >= 0 and end > start:
|
|
52
|
+
text = text[start : end + 1]
|
|
53
|
+
repairs.append("slice")
|
|
54
|
+
|
|
55
|
+
action: Any = None
|
|
56
|
+
try:
|
|
57
|
+
action = json.loads(text)
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
# Second chance for the most common small-model JSON slips: trailing
|
|
60
|
+
# commas before a closing brace/bracket.
|
|
61
|
+
repaired = re.sub(r",\s*([}\]])", r"\1", text)
|
|
62
|
+
try:
|
|
63
|
+
action = json.loads(repaired)
|
|
64
|
+
repairs.append("trailing_comma")
|
|
65
|
+
except json.JSONDecodeError as exc:
|
|
66
|
+
# Last chance: weak models sometimes emit a Python dict literal
|
|
67
|
+
# (single quotes, True/False/None). ast.literal_eval parses that
|
|
68
|
+
# deterministically without evaluating code.
|
|
69
|
+
try:
|
|
70
|
+
literal = ast.literal_eval(text)
|
|
71
|
+
except (ValueError, SyntaxError):
|
|
72
|
+
raise ValueError(f"Agent did not return valid JSON: {exc}") from exc
|
|
73
|
+
if not isinstance(literal, dict):
|
|
74
|
+
raise ValueError(f"Agent did not return valid JSON: {exc}") from exc
|
|
75
|
+
action = literal
|
|
76
|
+
repairs.append("python_literal")
|
|
77
|
+
|
|
78
|
+
if not isinstance(action, dict) or "action" not in action:
|
|
79
|
+
raise ValueError("Agent JSON must include an action field.")
|
|
80
|
+
return action, repairs
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def extract_action(raw: str) -> Dict:
|
|
84
|
+
"""Back-compat wrapper over :func:`extract_action_details`."""
|
|
85
|
+
action, _ = extract_action_details(raw)
|
|
86
|
+
return action
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── plan normalization ─────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
_FILE_CREATE_PLAN_ACTIONS = frozenset({"write_file", "generate_file"})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _plan_misses_manifest(steps: List[Dict[str, Any]], manifest: Dict[str, Any]) -> bool:
|
|
95
|
+
"""True when a pure file-writing plan fails to cover the manifest's file types.
|
|
96
|
+
|
|
97
|
+
Only pure file-creation plans are candidates for rewriting — a plan with
|
|
98
|
+
read/search steps reflects real planner intent and stays untouched.
|
|
99
|
+
"""
|
|
100
|
+
if any(s.get("action") not in _FILE_CREATE_PLAN_ACTIONS for s in steps):
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def _ext(path: Any) -> str:
|
|
104
|
+
text = str(path or "")
|
|
105
|
+
dot = text.rfind(".")
|
|
106
|
+
return text[dot:].lower() if dot >= 0 else ""
|
|
107
|
+
|
|
108
|
+
planned_exts = {
|
|
109
|
+
_ext((s.get("args") or {}).get("path")) for s in steps
|
|
110
|
+
}
|
|
111
|
+
manifest_exts = {_ext(spec.get("path")) for spec in manifest.get("files", [])}
|
|
112
|
+
return not manifest_exts.issubset(planned_exts)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def normalize_plan(plan: Any, user_message: str) -> Tuple[Dict[str, Any], List[str]]:
|
|
116
|
+
"""Enforce the minimal plan schema so execution never starts adrift.
|
|
117
|
+
|
|
118
|
+
A weak planner that returns junk steps / a missing goal previously flowed
|
|
119
|
+
straight into the executor, which then had to reconstruct intent from the
|
|
120
|
+
raw request. Normalization keeps the loop honest: ``goal`` is always a
|
|
121
|
+
non-empty string, ``steps`` only contains dicts with an ``action``, and an
|
|
122
|
+
empty plan for an obvious file-creation request gets a deterministic
|
|
123
|
+
single ``write_file`` step instead of leaving the executor to improvise.
|
|
124
|
+
|
|
125
|
+
Returns ``(plan, fixes)`` where ``fixes`` names every applied repair —
|
|
126
|
+
the loop trace records them so plan quality is observable per model.
|
|
127
|
+
"""
|
|
128
|
+
fixes: List[str] = []
|
|
129
|
+
if not isinstance(plan, dict):
|
|
130
|
+
plan = {}
|
|
131
|
+
fixes.append("plan_not_object")
|
|
132
|
+
plan = dict(plan)
|
|
133
|
+
|
|
134
|
+
goal = str(plan.get("goal") or "").strip()
|
|
135
|
+
if not goal:
|
|
136
|
+
plan["goal"] = user_message
|
|
137
|
+
fixes.append("goal_defaulted")
|
|
138
|
+
|
|
139
|
+
raw_steps = plan.get("steps")
|
|
140
|
+
steps = [
|
|
141
|
+
s for s in (raw_steps if isinstance(raw_steps, list) else [])
|
|
142
|
+
if isinstance(s, dict) and s.get("action")
|
|
143
|
+
]
|
|
144
|
+
if raw_steps and steps != raw_steps:
|
|
145
|
+
fixes.append("steps_filtered")
|
|
146
|
+
|
|
147
|
+
# Manifest-aware planning (review Wave 0.4): when the request is a
|
|
148
|
+
# recognized multi-file project, the deterministic manifest — not the
|
|
149
|
+
# planner's improvisation — decides the file set, exactly like the direct
|
|
150
|
+
# chat path. Rewrites apply only when the plan is empty or is a pure
|
|
151
|
+
# file-writing plan that misses part of the manifest, so a planner that
|
|
152
|
+
# already covered every requested file type is left untouched.
|
|
153
|
+
manifest = infer_project_manifest(user_message)
|
|
154
|
+
if manifest:
|
|
155
|
+
manifest_steps = [{
|
|
156
|
+
"action": "write_file",
|
|
157
|
+
"args": {"path": spec["path"]},
|
|
158
|
+
"description": spec["brief"],
|
|
159
|
+
} for spec in manifest["files"]]
|
|
160
|
+
if not steps:
|
|
161
|
+
steps = manifest_steps
|
|
162
|
+
fixes.append("manifest_steps")
|
|
163
|
+
elif _plan_misses_manifest(steps, manifest):
|
|
164
|
+
steps = manifest_steps
|
|
165
|
+
fixes.append("manifest_rewrite")
|
|
166
|
+
|
|
167
|
+
if not steps:
|
|
168
|
+
inferred = infer_file_target(user_message)
|
|
169
|
+
if inferred:
|
|
170
|
+
steps = [{
|
|
171
|
+
"action": "write_file",
|
|
172
|
+
"args": {"path": inferred},
|
|
173
|
+
"description": f"Create {inferred} for: {user_message[:120]}",
|
|
174
|
+
}]
|
|
175
|
+
fixes.append("heuristic_file_step")
|
|
176
|
+
plan["steps"] = steps
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
estimated = int(plan.get("estimated_steps") or 0)
|
|
180
|
+
except (TypeError, ValueError):
|
|
181
|
+
estimated = 0
|
|
182
|
+
fixes.append("estimated_steps_invalid")
|
|
183
|
+
plan["estimated_steps"] = max(1, estimated, len(steps))
|
|
184
|
+
plan["requires_approval"] = bool(plan.get("requires_approval", False))
|
|
185
|
+
if not isinstance(plan.get("rollback_strategy"), str):
|
|
186
|
+
plan["rollback_strategy"] = "none"
|
|
187
|
+
return plan, fixes
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ── learnings filter ───────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
_TRIVIAL_LEARNING_RE = re.compile(
|
|
193
|
+
r"^(파일(을|이)?\s*(만들|생성|작성|저장)|작업(을|이)?\s*(완료|성공)|성공적으로"
|
|
194
|
+
r"|task\s+(was\s+)?complet|file\s+(was\s+)?(creat|written|saved)"
|
|
195
|
+
r"|(successfully\s+)?(created|completed|finished|done)\b)",
|
|
196
|
+
re.IGNORECASE,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def filter_learnings(learnings: List[Any]) -> List[str]:
|
|
201
|
+
"""Drop trivial/duplicate learnings before they enter the brain.
|
|
202
|
+
|
|
203
|
+
"파일을 만들었다"-class statements restate what the transcript already
|
|
204
|
+
records and pollute recall. A learning survives when it is long enough to
|
|
205
|
+
carry information and is not a bare completion announcement.
|
|
206
|
+
"""
|
|
207
|
+
kept: List[str] = []
|
|
208
|
+
seen: set = set()
|
|
209
|
+
for raw in learnings or []:
|
|
210
|
+
text = str(raw or "").strip()
|
|
211
|
+
if len(text) < 12:
|
|
212
|
+
continue
|
|
213
|
+
if _TRIVIAL_LEARNING_RE.match(text) and len(text) < 48:
|
|
214
|
+
continue
|
|
215
|
+
key = text.lower()
|
|
216
|
+
if key in seen:
|
|
217
|
+
continue
|
|
218
|
+
seen.add(key)
|
|
219
|
+
kept.append(text)
|
|
220
|
+
return kept
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ── transcript shaping ─────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
def _truncate_strings(value: Any, limit: int) -> Any:
|
|
226
|
+
"""Deep-copy ``value`` with every string capped at ``limit`` chars.
|
|
227
|
+
|
|
228
|
+
Long tool outputs (file bodies, command output) dominate executor prompt
|
|
229
|
+
size without adding decision-relevant signal. The cap keeps the head of
|
|
230
|
+
each string and names how much was dropped, so the model still sees what
|
|
231
|
+
the value was — never a silent hole.
|
|
232
|
+
"""
|
|
233
|
+
if isinstance(value, str):
|
|
234
|
+
if len(value) <= limit:
|
|
235
|
+
return value
|
|
236
|
+
return value[:limit] + f"…[+{len(value) - limit} chars]"
|
|
237
|
+
if isinstance(value, dict):
|
|
238
|
+
return {k: _truncate_strings(v, limit) for k, v in value.items()}
|
|
239
|
+
if isinstance(value, list):
|
|
240
|
+
return [_truncate_strings(v, limit) for v in value]
|
|
241
|
+
return value
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def compact_transcript(
|
|
245
|
+
transcript: List[Dict[str, Any]],
|
|
246
|
+
*,
|
|
247
|
+
window: int = 8,
|
|
248
|
+
result_chars: int = 700,
|
|
249
|
+
) -> List[Dict[str, Any]]:
|
|
250
|
+
"""Bounded executor view of a transcript (review Wave 0.3).
|
|
251
|
+
|
|
252
|
+
The executor prompt previously embedded the *entire* transcript JSON every
|
|
253
|
+
step — O(steps²) token growth that starved :class:`PhaseBudgets` on long
|
|
254
|
+
runs and buried weak models in stale detail. This view keeps the most
|
|
255
|
+
recent ``window`` steps in full (with string values capped at
|
|
256
|
+
``result_chars``) and reduces every older step to a one-line summary, so
|
|
257
|
+
the prompt stays bounded while no step disappears entirely.
|
|
258
|
+
"""
|
|
259
|
+
steps = list(transcript or [])
|
|
260
|
+
if len(steps) <= window:
|
|
261
|
+
return [_truncate_strings(step, result_chars) for step in steps]
|
|
262
|
+
older, recent = steps[:-window], steps[-window:]
|
|
263
|
+
summarized: List[Dict[str, Any]] = [{
|
|
264
|
+
"summarized_older_steps": len(older),
|
|
265
|
+
"note": "older steps compacted — full detail retained in the run record",
|
|
266
|
+
}]
|
|
267
|
+
for step in older:
|
|
268
|
+
entry: Dict[str, Any] = {"state": step.get("state")}
|
|
269
|
+
for key in ("action", "verdict", "retry_attempt"):
|
|
270
|
+
if step.get(key) is not None:
|
|
271
|
+
entry[key] = step.get(key)
|
|
272
|
+
if step.get("error"):
|
|
273
|
+
entry["error"] = str(step["error"])[:160]
|
|
274
|
+
elif isinstance(step.get("result"), dict):
|
|
275
|
+
entry["ok"] = True
|
|
276
|
+
path = step["result"].get("path") or (step.get("args") or {}).get("path")
|
|
277
|
+
if path:
|
|
278
|
+
entry["path"] = str(path)
|
|
279
|
+
summarized.append(entry)
|
|
280
|
+
summarized.extend(_truncate_strings(step, result_chars) for step in recent)
|
|
281
|
+
return summarized
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def files_written(
|
|
285
|
+
transcript: List[Dict[str, Any]],
|
|
286
|
+
file_create_actions: FrozenSet[str],
|
|
287
|
+
) -> List[str]:
|
|
288
|
+
"""Ordered unique paths of files this run successfully wrote (review L5).
|
|
289
|
+
|
|
290
|
+
Later executor steps get this as explicit context, so "만들고 이어서
|
|
291
|
+
설명해" multi-step work sees its own output instead of a stale
|
|
292
|
+
workspace picture.
|
|
293
|
+
"""
|
|
294
|
+
seen: List[str] = []
|
|
295
|
+
for step in transcript:
|
|
296
|
+
if step.get("state") != AgentState.EXECUTING.value:
|
|
297
|
+
continue
|
|
298
|
+
if step.get("action") not in file_create_actions:
|
|
299
|
+
continue
|
|
300
|
+
if not isinstance(step.get("result"), dict):
|
|
301
|
+
continue
|
|
302
|
+
path = step["result"].get("path") or (step.get("args") or {}).get("path")
|
|
303
|
+
if path and str(path) not in seen:
|
|
304
|
+
seen.append(str(path))
|
|
305
|
+
return seen
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def artifact_checklist(
|
|
309
|
+
transcript: List[Dict[str, Any]],
|
|
310
|
+
file_create_actions: FrozenSet[str],
|
|
311
|
+
) -> List[Dict[str, Any]]:
|
|
312
|
+
"""Deterministic artifact facts for the critic (review L4).
|
|
313
|
+
|
|
314
|
+
The critic previously judged file work from prose alone; this surfaces
|
|
315
|
+
the sanitize/repair honesty flags per written file so a repaired
|
|
316
|
+
placeholder can never pass as a fulfilled request unchecked.
|
|
317
|
+
"""
|
|
318
|
+
checklist: List[Dict[str, Any]] = []
|
|
319
|
+
for step in transcript:
|
|
320
|
+
if step.get("state") != AgentState.EXECUTING.value:
|
|
321
|
+
continue
|
|
322
|
+
if step.get("action") not in file_create_actions:
|
|
323
|
+
continue
|
|
324
|
+
if not isinstance(step.get("result"), dict):
|
|
325
|
+
continue
|
|
326
|
+
path = step["result"].get("path") or (step.get("args") or {}).get("path")
|
|
327
|
+
if not path:
|
|
328
|
+
continue
|
|
329
|
+
sanitize_meta = step.get("content_sanitize") or {}
|
|
330
|
+
checklist.append({
|
|
331
|
+
"path": str(path),
|
|
332
|
+
"sanitized": bool(sanitize_meta.get("sanitized")),
|
|
333
|
+
"repaired": bool(sanitize_meta.get("repaired")),
|
|
334
|
+
})
|
|
335
|
+
return checklist
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# Explicit requirement lines a user writes out: "- 다크모드", "1. 검색 기능",
|
|
339
|
+
# "* dark mode". Free prose is deliberately NOT parsed — a wrong requirement
|
|
340
|
+
# is worse than no requirement.
|
|
341
|
+
_REQUIREMENT_LINE_RE = re.compile(r"^\s*(?:[-*•]|\d+[.)])\s+(.{3,120})$", re.MULTILINE)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def requirement_coverage(
|
|
345
|
+
user_message: str,
|
|
346
|
+
transcript: List[Dict[str, Any]],
|
|
347
|
+
file_create_actions: FrozenSet[str],
|
|
348
|
+
) -> Dict[str, Any]:
|
|
349
|
+
"""Did the run produce what the request actually asked for? (review 루프 §2)
|
|
350
|
+
|
|
351
|
+
The critic judged prose against prose, so "make an HTML+CSS+JS todo app"
|
|
352
|
+
could pass with one file written. This is the deterministic half of the
|
|
353
|
+
answer, built from two sources that are honest about their own limits:
|
|
354
|
+
|
|
355
|
+
* **manifest files** — when the request maps to a known multi-file project
|
|
356
|
+
(:func:`infer_project_manifest`), every declared path must have been
|
|
357
|
+
written. A missing manifest file is a *hard* miss: it is not a matter of
|
|
358
|
+
taste whether ``style.css`` exists.
|
|
359
|
+
* **explicit requirement lines** — bullet/numbered lines the user wrote
|
|
360
|
+
out. These are reported to the critic as a checklist but never block on
|
|
361
|
+
their own: matching a feature to a transcript is a judgement call, and
|
|
362
|
+
guessing it wrong would either fake completion or block real work.
|
|
363
|
+
|
|
364
|
+
Returns ``{"files": {...}, "requirements": [...], "missing_files": [...],
|
|
365
|
+
"complete": bool}`` where ``complete`` is false only when a declared
|
|
366
|
+
manifest file is missing.
|
|
367
|
+
"""
|
|
368
|
+
written = files_written(transcript, file_create_actions)
|
|
369
|
+
written_names = {Path(path).name.lower() for path in written}
|
|
370
|
+
manifest = infer_project_manifest(user_message) or {}
|
|
371
|
+
declared = [str(spec.get("path") or "") for spec in manifest.get("files", [])]
|
|
372
|
+
missing = [
|
|
373
|
+
path for path in declared
|
|
374
|
+
if path and Path(path).name.lower() not in written_names
|
|
375
|
+
]
|
|
376
|
+
requirements = [
|
|
377
|
+
line.strip()
|
|
378
|
+
for line in _REQUIREMENT_LINE_RE.findall(str(user_message or ""))
|
|
379
|
+
][:10]
|
|
380
|
+
return {
|
|
381
|
+
"files": {"declared": declared, "written": written},
|
|
382
|
+
"missing_files": missing,
|
|
383
|
+
"requirements": requirements,
|
|
384
|
+
"complete": not missing,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def format_requirement_coverage(coverage: Dict[str, Any]) -> str:
|
|
389
|
+
"""Requirement facts for the critic prompt, or "" when there is nothing."""
|
|
390
|
+
lines: List[str] = []
|
|
391
|
+
declared = coverage["files"]["declared"]
|
|
392
|
+
if declared:
|
|
393
|
+
written = set(coverage["files"]["written"])
|
|
394
|
+
lines.append("Requested files (deterministic, from the request):")
|
|
395
|
+
for path in declared:
|
|
396
|
+
got = any(Path(item).name.lower() == Path(path).name.lower() for item in written)
|
|
397
|
+
lines.append(f"- {path}: {'written' if got else 'MISSING'}")
|
|
398
|
+
if coverage["requirements"]:
|
|
399
|
+
lines.append(
|
|
400
|
+
"Requirements the user listed explicitly — check each one against "
|
|
401
|
+
"the artifacts, not against the plan:"
|
|
402
|
+
)
|
|
403
|
+
lines.extend(f"- {item}" for item in coverage["requirements"])
|
|
404
|
+
return "\n\n" + "\n".join(lines) if lines else ""
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def format_artifact_checklist(checklist: List[Dict[str, Any]]) -> str:
|
|
408
|
+
lines = []
|
|
409
|
+
for item in checklist:
|
|
410
|
+
state = (
|
|
411
|
+
"auto-REPAIRED scaffold" if item["repaired"]
|
|
412
|
+
else ("sanitized model output" if item["sanitized"] else "written as produced")
|
|
413
|
+
)
|
|
414
|
+
lines.append(f"- {item['path']}: {state}")
|
|
415
|
+
return (
|
|
416
|
+
"Artifact checklist (deterministic, from the transcript):\n"
|
|
417
|
+
+ "\n".join(lines)
|
|
418
|
+
+ "\nVerify each artifact actually fulfills the user's request. An "
|
|
419
|
+
"auto-repaired scaffold is NOT completion unless its content "
|
|
420
|
+
"satisfies what was asked."
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ── budgets ────────────────────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
@dataclass(frozen=True)
|
|
427
|
+
class TranscriptBudget:
|
|
428
|
+
"""Executor/critic prompt shaping caps (review Wave 0.3).
|
|
429
|
+
|
|
430
|
+
``window`` full recent steps for the executor; per-string caps keep tool
|
|
431
|
+
output bodies from dominating either prompt. Overridable through the same
|
|
432
|
+
``Config.from_env`` pattern as :class:`PhaseBudgets`.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
window: int = 8
|
|
436
|
+
result_chars: int = 700
|
|
437
|
+
verify_chars: int = 1200
|
|
438
|
+
|
|
439
|
+
@classmethod
|
|
440
|
+
def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "TranscriptBudget":
|
|
441
|
+
from latticeai.core.config import _int
|
|
442
|
+
|
|
443
|
+
if env is None:
|
|
444
|
+
import os
|
|
445
|
+
|
|
446
|
+
env = os.environ
|
|
447
|
+
|
|
448
|
+
def cap(key: str, default: int, floor: int) -> int:
|
|
449
|
+
return max(floor, _int(env, key, default))
|
|
450
|
+
|
|
451
|
+
return cls(
|
|
452
|
+
window=cap("LATTICEAI_AGENT_TRANSCRIPT_WINDOW", cls.window, 2),
|
|
453
|
+
result_chars=cap("LATTICEAI_AGENT_TRANSCRIPT_CHARS", cls.result_chars, 120),
|
|
454
|
+
verify_chars=cap("LATTICEAI_AGENT_VERIFY_CHARS", cls.verify_chars, 200),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@dataclass(frozen=True)
|
|
459
|
+
class PhaseBudgets:
|
|
460
|
+
"""Per-phase token budgets for the agent loop.
|
|
461
|
+
|
|
462
|
+
One shared budget let a weak model burn everything on planning prose and
|
|
463
|
+
reach EXECUTE with nothing left. Each role phase now has its own cap, so
|
|
464
|
+
a verbose planner can never starve execution or verification. Defaults
|
|
465
|
+
match the historical hardcoded values; every cap is overridable through
|
|
466
|
+
the ``Config.from_env`` environment pattern.
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
plan_tokens: int = 1024
|
|
470
|
+
execute_tokens: int = 4096
|
|
471
|
+
verify_tokens: int = 512
|
|
472
|
+
memory_tokens: int = 256
|
|
473
|
+
|
|
474
|
+
@classmethod
|
|
475
|
+
def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "PhaseBudgets":
|
|
476
|
+
from latticeai.core.config import _int
|
|
477
|
+
|
|
478
|
+
if env is None:
|
|
479
|
+
import os
|
|
480
|
+
|
|
481
|
+
env = os.environ
|
|
482
|
+
|
|
483
|
+
def cap(key: str, default: int) -> int:
|
|
484
|
+
# A misconfigured/absurd value must not brick the loop: floor at a
|
|
485
|
+
# budget that still fits one JSON action object.
|
|
486
|
+
return max(128, _int(env, key, default))
|
|
487
|
+
|
|
488
|
+
return cls(
|
|
489
|
+
plan_tokens=cap("LATTICEAI_AGENT_PLAN_TOKENS", cls.plan_tokens),
|
|
490
|
+
execute_tokens=cap("LATTICEAI_AGENT_EXECUTE_TOKENS", cls.execute_tokens),
|
|
491
|
+
verify_tokens=cap("LATTICEAI_AGENT_VERIFY_TOKENS", cls.verify_tokens),
|
|
492
|
+
memory_tokens=cap("LATTICEAI_AGENT_MEMORY_TOKENS", cls.memory_tokens),
|
|
493
|
+
)
|
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from latticeai.core.tool_registry import TOOL_CATALOG_BRIEF
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
PLANNER_PROMPT = """You are the PLANNER role in Lattice AI's multi-role agent harness.
|
|
9
8
|
Your ONLY job: analyze the request and produce a structured execution plan.
|
|
10
9
|
You do NOT call tools or write code.
|
|
@@ -27,6 +27,8 @@ from lattice_brain.runtime.multi_agent import (
|
|
|
27
27
|
MULTI_AGENT_VERSION,
|
|
28
28
|
ROLE_AGENT_IDS,
|
|
29
29
|
)
|
|
30
|
+
from latticeai.core.quiet import quiet
|
|
31
|
+
|
|
30
32
|
from .timeutil import now_iso as _now
|
|
31
33
|
|
|
32
34
|
AGENT_TYPES = ("planner", "researcher", "executor", "reviewer", "release", "custom")
|
|
@@ -76,7 +78,7 @@ class AgentRegistry:
|
|
|
76
78
|
data.setdefault("config_overrides", {})
|
|
77
79
|
return data
|
|
78
80
|
except Exception:
|
|
79
|
-
|
|
81
|
+
quiet()
|
|
80
82
|
return {"custom": [], "config_overrides": {}}
|
|
81
83
|
|
|
82
84
|
def _save(self) -> None:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""The agent loop's state vocabulary.
|
|
2
|
+
|
|
3
|
+
Its own module because both sides of the runtime need it and neither can own
|
|
4
|
+
it: :mod:`latticeai.core.agent` holds the state machine, and
|
|
5
|
+
:mod:`latticeai.core.agent_helpers` holds the pure functions the machine calls.
|
|
6
|
+
If the enum lived in ``agent``, the helpers would have to import the module
|
|
7
|
+
that imports them — so they would fall back to writing ``"EXECUTING"`` as a
|
|
8
|
+
bare string, and a rename of the enum value would silently stop matching.
|
|
9
|
+
|
|
10
|
+
Both names stay importable from :mod:`latticeai.core.agent`, which is where
|
|
11
|
+
every existing caller expects them.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import FrozenSet
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentState(str, Enum):
|
|
21
|
+
IDLE = "IDLE"
|
|
22
|
+
PLANNING = "PLANNING"
|
|
23
|
+
WAITING_APPROVAL = "WAITING_APPROVAL"
|
|
24
|
+
EXECUTING = "EXECUTING"
|
|
25
|
+
VERIFYING = "VERIFYING"
|
|
26
|
+
FAILED = "FAILED"
|
|
27
|
+
ROLLBACK = "ROLLBACK"
|
|
28
|
+
# Terminal, non-success: the run ended but completion could not be
|
|
29
|
+
# verified (critic unavailable/unparseable, or a PASS with no execution
|
|
30
|
+
# evidence). Never presented as success — the user must check the result.
|
|
31
|
+
NEEDS_REVIEW = "NEEDS_REVIEW"
|
|
32
|
+
DONE = "DONE"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Terminal states — the agent loop exits when reaching one of these
|
|
36
|
+
AGENT_TERMINAL_STATES: FrozenSet[AgentState] = frozenset(
|
|
37
|
+
{AgentState.DONE, AgentState.FAILED, AgentState.NEEDS_REVIEW}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = ["AgentState", "AGENT_TERMINAL_STATES"]
|
package/latticeai/core/audit.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Audit logging, sensitivity analysis, and admin reporting."""
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
import json
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import threading
|
|
8
|
-
import hashlib
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Callable, Dict, List, Optional
|
|
11
11
|
|
|
@@ -14,7 +14,8 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
from typing import Any, Callable
|
|
16
16
|
|
|
17
|
-
from latticeai.core.security import SECRET_KEY_HINTS
|
|
17
|
+
from latticeai.core.security import SECRET_KEY_HINTS
|
|
18
|
+
from latticeai.core.security import redact_secrets as redact_secret_values
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def register_builtin_hook_runners(
|
package/latticeai/core/config.py
CHANGED
|
@@ -238,7 +238,18 @@ class EmbeddingProvider:
|
|
|
238
238
|
return list(struct.unpack(f"<{count}f", payload[: count * 4]))
|
|
239
239
|
|
|
240
240
|
def similarity(self, left: Iterable[float], right: Iterable[float]) -> float:
|
|
241
|
-
|
|
241
|
+
# strict=True: a dimension mismatch means the two vectors came from
|
|
242
|
+
# different embedding models. Truncating to the shorter one produces a
|
|
243
|
+
# plausible-looking similarity that is meaningless — exactly the silent
|
|
244
|
+
# wrongness this codebase keeps finding. Callers that can hit a model
|
|
245
|
+
# swap already handle failure and fall back to lexical search.
|
|
246
|
+
left_v, right_v = list(left), list(right)
|
|
247
|
+
if len(left_v) != len(right_v):
|
|
248
|
+
raise ValueError(
|
|
249
|
+
f"embedding dimension mismatch: {len(left_v)} vs {len(right_v)}; "
|
|
250
|
+
"the vector index was built with a different model"
|
|
251
|
+
)
|
|
252
|
+
return float(sum(a * b for a, b in zip(left_v, right_v, strict=True)))
|
|
242
253
|
|
|
243
254
|
# ── observability ─────────────────────────────────────────────────────
|
|
244
255
|
def health(self) -> Dict[str, Any]:
|
|
@@ -31,6 +31,8 @@ import json
|
|
|
31
31
|
import re
|
|
32
32
|
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
|
|
33
33
|
|
|
34
|
+
from latticeai.core.quiet import quiet
|
|
35
|
+
|
|
34
36
|
# ── extraction ──────────────────────────────────────────────────────────
|
|
35
37
|
|
|
36
38
|
_THINK_BLOCK_RE = re.compile(
|
|
@@ -171,6 +173,7 @@ def _slice_json_document(content: str) -> Optional[str]:
|
|
|
171
173
|
try:
|
|
172
174
|
json.loads(candidate)
|
|
173
175
|
except (ValueError, TypeError):
|
|
176
|
+
quiet()
|
|
174
177
|
continue
|
|
175
178
|
if best is None or len(candidate) > len(best):
|
|
176
179
|
best = candidate
|
|
@@ -9,8 +9,11 @@ from datetime import datetime, timedelta
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Dict, List, Optional
|
|
11
11
|
|
|
12
|
+
from latticeai.core.quiet import quiet
|
|
13
|
+
|
|
12
14
|
from .timeutil import local_now as _now
|
|
13
15
|
|
|
16
|
+
|
|
14
17
|
def _iso(dt: datetime) -> str:
|
|
15
18
|
return dt.isoformat(timespec="seconds")
|
|
16
19
|
|
|
@@ -39,7 +42,7 @@ class InvitationStore:
|
|
|
39
42
|
data.setdefault("invitations", [])
|
|
40
43
|
return data
|
|
41
44
|
except Exception:
|
|
42
|
-
|
|
45
|
+
quiet()
|
|
43
46
|
return {"version": 1, "invitations": []}
|
|
44
47
|
|
|
45
48
|
def _save(self, data: Dict[str, Any]) -> None:
|
|
@@ -9,6 +9,8 @@ from datetime import datetime
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Dict, Optional
|
|
11
11
|
|
|
12
|
+
from latticeai.core.quiet import quiet
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
def atomic_write_json(path: Path, payload: Dict[str, Any]) -> None:
|
|
14
16
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -22,7 +24,7 @@ def atomic_write_json(path: Path, payload: Dict[str, Any]) -> None:
|
|
|
22
24
|
except OSError:
|
|
23
25
|
# Windows and unusual filesystems may not expose POSIX mode bits; the
|
|
24
26
|
# atomic write is still the safest supported fallback there.
|
|
25
|
-
|
|
27
|
+
quiet()
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def parse_iso(value: Optional[str]) -> Optional[datetime]:
|