feat: add live H1-H7 trace explorer
This commit is contained in:
@@ -92,6 +92,12 @@ def metrics_path() -> str:
|
||||
return guarded_override(os.environ["CASAN_CHAT_METRICS_LOG"]) if os.environ.get("CASAN_CHAT_METRICS_LOG") else tenant_path("telemetry/cost/metrics.jsonl", "logs/cost/metrics.jsonl")
|
||||
|
||||
|
||||
def trace_events_path(trace_id: str) -> str:
|
||||
override = os.environ.get("CASAN_TRACE_EVENTS_DIR")
|
||||
directory = guarded_override(override) if override else tenant_path("telemetry/trace-events", "logs/trace-events")
|
||||
return os.path.join(directory, f"{trace_id}.jsonl")
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
@@ -365,6 +371,22 @@ def append_jsonl(path: str, rec):
|
||||
fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
def record_trace_event(trace_id: str, gate_id: str, status: str, reason: str, evidence=None):
|
||||
"""Append a privacy-minimised event used by the live H1-H7 explorer."""
|
||||
try:
|
||||
append_jsonl(trace_events_path(trace_id), {
|
||||
"timestamp": now_iso(),
|
||||
"trace_id": trace_id,
|
||||
"gate_id": gate_id,
|
||||
"status": status,
|
||||
"reason": reason,
|
||||
"evidence": evidence or {},
|
||||
})
|
||||
except OSError:
|
||||
# Observability must never turn an otherwise safe read-only answer into a failure.
|
||||
pass
|
||||
|
||||
|
||||
def load_head() -> str:
|
||||
try:
|
||||
return open(head_path(), encoding="utf-8").read().strip() or GENESIS_HASH
|
||||
@@ -486,10 +508,16 @@ def ask(args):
|
||||
chat_id = args.chat_id or "chat-default"
|
||||
turn_id = args.turn_id or f"turn-{trace_id[:12]}"
|
||||
tenant_id = args.tenant or "default"
|
||||
record_trace_event(trace_id, "H1-context", "running", "Classifying prompt contract")
|
||||
record_trace_event(trace_id, "H1-context", "pass" if router.get("mode") in ("READ_ONLY", "ANALYSIS") else "blocked", router.get("reason", "Prompt classified"), {
|
||||
"mode": router.get("mode", "BLOCK"),
|
||||
"risk": router.get("risk", "high"),
|
||||
})
|
||||
|
||||
def finish(decision: str, answer: str, sources=None, safe_message="", synthesis=None):
|
||||
elapsed = int((datetime.now(timezone.utc) - started).total_seconds() * 1000)
|
||||
sources = sources or []
|
||||
record_trace_event(trace_id, "H5-governance", "running", "Writing append-only decision audit")
|
||||
rec = record_turn({
|
||||
"timestamp": now_iso(),
|
||||
"trace_id": trace_id,
|
||||
@@ -508,7 +536,22 @@ def ask(args):
|
||||
"synthesis_mode": (synthesis or {}).get("mode", "deterministic"),
|
||||
"sources": [{"path": s.get("path"), "line": s.get("line")} for s in sources],
|
||||
})
|
||||
record_trace_event(trace_id, "H5-governance", "pass", "Decision audit recorded", {
|
||||
"decision": decision,
|
||||
"audit_seq": rec["seq"],
|
||||
"audit_hash": rec["record_hash"],
|
||||
})
|
||||
record_trace_event(trace_id, "H6-agentops", "running", "Recording runtime metrics")
|
||||
record_metrics(trace_id, safe_message or message, answer, "success" if decision == "ANSWERED" else "failed", elapsed, synthesis)
|
||||
record_trace_event(trace_id, "H6-agentops", "pass" if decision == "ANSWERED" else "error", "Runtime metrics recorded", {
|
||||
"latency_ms": elapsed,
|
||||
"status": "success" if decision == "ANSWERED" else "failed",
|
||||
"synthesis_mode": (synthesis or {}).get("mode", "deterministic"),
|
||||
})
|
||||
record_trace_event(trace_id, "H7-orchestration", "pass" if decision == "ANSWERED" else "blocked", "Harness turn completed" if decision == "ANSWERED" else "Harness stopped with governed outcome", {
|
||||
"decision": decision,
|
||||
"certified": decision == "ANSWERED",
|
||||
})
|
||||
return {
|
||||
"success": decision == "ANSWERED",
|
||||
"chat_id": chat_id,
|
||||
@@ -532,8 +575,10 @@ def ask(args):
|
||||
|
||||
role = "analysis" if router.get("mode") == "ANALYSIS" else "read_only"
|
||||
|
||||
record_trace_event(trace_id, "H4-security", "running", "Scanning input boundary")
|
||||
rc, safe_input, scan_msg = run_security(message, "input")
|
||||
if rc != 0:
|
||||
record_trace_event(trace_id, "H4-security", "blocked", "Input rejected by security boundary", {"scan": scan_msg})
|
||||
router["mode"] = "BLOCK"
|
||||
router["reason"] = "h4_input_denied"
|
||||
router["matched_rules"] = router.get("matched_rules", []) + [scan_msg]
|
||||
@@ -541,8 +586,14 @@ def ask(args):
|
||||
print(json.dumps(finish("DENIED", answer), ensure_ascii=False))
|
||||
return 2
|
||||
|
||||
record_trace_event(trace_id, "H4-security", "running", "Input passed; output scan pending", {"input_scan": "pass"})
|
||||
record_trace_event(trace_id, "H2-tool", "running", "Retrieving allowlisted evidence")
|
||||
sources = collect_sources(safe_input)
|
||||
history = load_history(chat_id, tenant_id)
|
||||
record_trace_event(trace_id, "H2-tool", "pass", "Allowlisted evidence prepared", {
|
||||
"source_count": len(sources),
|
||||
"history_available": bool(history),
|
||||
})
|
||||
|
||||
# Item 3: streaming — emit a SAFE deterministic draft (whitelist-only, no model
|
||||
# text, no side-effect) tagged UNCERTIFIED, then continue to the certified final.
|
||||
@@ -560,9 +611,17 @@ def ask(args):
|
||||
"synthesis": {"mode": "deterministic", "reason": "stream_draft"},
|
||||
}, ensure_ascii=False), flush=True)
|
||||
|
||||
record_trace_event(trace_id, "H3-eval", "running", "Synthesizing grounded answer")
|
||||
answer, synthesis = synthesize_answer(safe_input, sources, role, history)
|
||||
record_trace_event(trace_id, "H3-eval", "pass", "Grounded synthesis completed", {
|
||||
"mode": synthesis.get("mode", "deterministic"),
|
||||
"provider": synthesis.get("provider", "deterministic"),
|
||||
"model": synthesis.get("model", "none"),
|
||||
"source_count": len(sources),
|
||||
})
|
||||
rc, safe_answer, scan_msg = run_security(answer, "output")
|
||||
if rc != 0:
|
||||
record_trace_event(trace_id, "H4-security", "blocked", "Output rejected by security boundary", {"scan": scan_msg})
|
||||
router["mode"] = "BLOCK"
|
||||
router["reason"] = "h4_output_denied"
|
||||
router["matched_rules"] = router.get("matched_rules", []) + [scan_msg]
|
||||
@@ -572,6 +631,7 @@ def ask(args):
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 2
|
||||
|
||||
record_trace_event(trace_id, "H4-security", "pass", "Input and output security scans passed")
|
||||
result = finish("ANSWERED", safe_answer, sources, safe_input, synthesis)
|
||||
if getattr(args, "stream", False):
|
||||
result["phase"] = "final"
|
||||
|
||||
Reference in New Issue
Block a user