feat: prepare 2.2.0 with UI and execution fixes
Add dark theme, richer Markdown and readable plugin results. Improve Android input evidence, response integrity and diagnostics. Unify lifecycle, guidance receipts and evidence sources across clients. Keep direct action dispatch without an added AI quota approval layer. Align version metadata at 2.2.0 and bundle Helper 1.2.2. Validation: 3063 backend tests before the version bump; 44 after. Frontend: 317 tests and production build passed.
This commit is contained in:
1 parent
83bcfd54d5
commit
fac4ccb760
146 files changed
+5689
-2033
No files matched your search
@@ -1018,6 +1018,7 @@ class DataEngine:
|
||||
status: str = "success",
|
||||
duration: float | None = None,
|
||||
trace_id: UUID | None = None,
|
||||
durable: bool = False,
|
||||
) -> UUID:
|
||||
"""Record a trace (agent, tool, or log), non-blocking."""
|
||||
if not self.current_session_id:
|
||||
@@ -1046,7 +1047,12 @@ class DataEngine:
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
self._run_in_background(self.storage.create_trace, trace)
|
||||
if durable:
|
||||
# Execution budgets must be committed before the device receives
|
||||
# an input. Reuse the trace database and normal event publication.
|
||||
self.storage.create_trace(trace)
|
||||
else:
|
||||
self._run_in_background(self.storage.create_trace, trace)
|
||||
|
||||
# Publish event
|
||||
trace_dict = trace.model_dump()
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Describe recorded provenance without turning a source into a verified assertion."""
|
||||
|
||||
import hashlib
|
||||
from uuid import UUID
|
||||
import json
|
||||
|
||||
|
||||
SOURCE_KINDS = {"android", "computer", "terminal", "api", "file", "other"}
|
||||
|
||||
|
||||
def _step_reference(step: dict) -> dict:
|
||||
value = step.get("step_id")
|
||||
return {
|
||||
"step_id": str(value) if isinstance(value, (str, UUID)) else None,
|
||||
"step_number": step.get("step_number"),
|
||||
}
|
||||
|
||||
|
||||
def recorded_sources(steps: list[dict], *, limit: int = 100) -> dict:
|
||||
sources = {}
|
||||
unattributed = []
|
||||
for step in steps:
|
||||
result = step.get("last_execution_result")
|
||||
execution = result.get("execution") if isinstance(result, dict) else None
|
||||
records = execution if isinstance(execution, list) else step.get("action_taken")
|
||||
records = records if isinstance(records, list) else [records]
|
||||
found = False
|
||||
missing = False
|
||||
for record in records:
|
||||
if not isinstance(record, dict) or not isinstance(record.get("source"), dict):
|
||||
missing = True
|
||||
continue
|
||||
source = record["source"]
|
||||
raw_kind = source.get("kind")
|
||||
kind = raw_kind if isinstance(raw_kind, str) and raw_kind in SOURCE_KINDS else "other"
|
||||
target = source.get("target_id")
|
||||
if not isinstance(target, str) or not target:
|
||||
missing = True
|
||||
continue
|
||||
found = True
|
||||
plugin = str(record.get("plugin_id") or "")[:100]
|
||||
key = (plugin, kind, target[:256])
|
||||
if key not in sources:
|
||||
sources[key] = {
|
||||
"id": hashlib.sha256(json.dumps(key).encode()).hexdigest()[:20],
|
||||
"kind": kind,
|
||||
"plugin_id": plugin,
|
||||
"target_id": target[:256],
|
||||
"label": str(source.get("label") or "")[:500],
|
||||
"category": "interface" if kind in {"android", "computer"} else "auxiliary",
|
||||
"references": [],
|
||||
"reference_count": 0,
|
||||
}
|
||||
reference = _step_reference(step)
|
||||
for field in ("call_id", "source_id"):
|
||||
if isinstance(record.get(field), str) and record[field]:
|
||||
reference[field] = record[field][:256]
|
||||
if isinstance(record.get("read_only"), bool):
|
||||
reference["read_only"] = record["read_only"]
|
||||
entry = sources[key]
|
||||
entry["reference_count"] += 1
|
||||
if len(entry["references"]) < 8:
|
||||
entry["references"].append(reference)
|
||||
if not found or missing:
|
||||
# A screenshot or a model-authored URL does not supply missing provenance.
|
||||
unattributed.append(_step_reference(step))
|
||||
values = list(sources.values())
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"sources": values[:limit],
|
||||
"total_sources": len(values),
|
||||
"sources_omitted": max(0, len(values) - limit),
|
||||
"unattributed_step_count": len(unattributed),
|
||||
"unattributed_references": unattributed[:8],
|
||||
"note": "来源清单仅来自执行记录,不代表断言已验证。界面证据与接口、终端、文件来源分开;界面结果不能单独证明后端持久状态。逐项结论及未验证边界以正式验收依据为准。缺失来源不按网址或业务名称推断执行平台。",
|
||||
}
|
||||
@@ -963,6 +963,19 @@ class StorageManager:
|
||||
)
|
||||
return traces
|
||||
|
||||
def get_latest_trace_payload(self, session_id: UUID | str, name: str) -> dict | None:
|
||||
"""Read a durable task-owned execution ledger from the existing trace store."""
|
||||
with self._get_connection() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT payload FROM traces WHERE session_id = ? AND name = ? "
|
||||
"ORDER BY timestamp DESC LIMIT 1",
|
||||
(str(session_id), name),
|
||||
).fetchone()
|
||||
if not row or not row["payload"]:
|
||||
return None
|
||||
value = json.loads(row["payload"])
|
||||
return value if isinstance(value, dict) else None
|
||||
|
||||
def get_steps_with_traces(self, session_id: UUID) -> list[tuple[StepRecord, list[TraceRecord]]]:
|
||||
"""Retrieve all steps and their associated traces for a session."""
|
||||
steps = self.get_steps(session_id)
|
||||
|
||||
Reference in new issue
Block a user