feat: add live H1-H7 trace explorer

This commit is contained in:
thanhnv
2026-07-10 23:17:23 +09:00
parent 7cb592a32f
commit 1345d4c930
11 changed files with 496 additions and 28 deletions
@@ -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"