diff --git a/config/agent_contracts/agent_output_contracts.json b/config/agent_contracts/agent_output_contracts.json index e3d9f90c..1bd3d3ec 100644 --- a/config/agent_contracts/agent_output_contracts.json +++ b/config/agent_contracts/agent_output_contracts.json @@ -1,7 +1,7 @@ { "registry_id": "contextlattice_agent_output_contracts", - "registry_version": 2, - "updated_at": "2026-05-27T00:00:00Z", + "registry_version": 3, + "updated_at": "2026-05-28T00:00:00Z", "default_validator": "contextlattice.boundary.v1", "purpose": "Agent-facing ContextLattice payloads must carry explicit contract identifiers and pass boundary validation before being returned or handed to another agent.", "shared_fragments": { @@ -61,6 +61,9 @@ "payload_kind": "agent_policy_context_package", "required_output_mode": "json_object", "safe_failure_behavior": "return_json_contract_violation_envelope_without_raw_payload", + "max_total_json_bytes": 60000, + "max_string_bytes": 6000, + "max_list_items": 32, "required_fields": [ "version", "agent", @@ -160,6 +163,13 @@ "anti_scheming_protocol.red_flags": 4, "anti_scheming_protocol.delivery": 3 }, + "max_bytes_by_path": { + "mission": 2000, + "objective": 2000, + "goal": 2000, + "handoff.handoff_prompt": 4000, + "evidence.mission_pack_error": 1000 + }, "forbidden_fields": [ "hookSpecificOutput", "messages", @@ -175,6 +185,9 @@ "contract_version": 1, "payload_kind": "agent_preflight_response", "required_output_mode": "json_object", + "max_total_json_bytes": 180000, + "max_string_bytes": 8000, + "max_list_items": 64, "required_fields": [ "ok", "service", @@ -225,6 +238,9 @@ "contract_version": 1, "payload_kind": "context_pack_response", "required_output_mode": "json_object", + "max_total_json_bytes": 120000, + "max_string_bytes": 6000, + "max_list_items": 64, "required_fields": [ "context_pack", "source_coverage", @@ -274,6 +290,10 @@ "secrets" ], "forbidden_scope": "root", + "max_bytes_by_path": { + "query": 2000, + "task_summary": 1000 + }, "required_string_contains": { "format_contract.schema_id": "context_pack_response.v1" } @@ -321,6 +341,9 @@ "contract_version": 1, "payload_kind": "codex_compact_hook_stdout", "required_output_mode": "json_object", + "max_total_json_bytes": 4096, + "max_string_bytes": 4000, + "max_list_items": 4, "allowed_fields": [ "continue", "suppressOutput", @@ -597,6 +620,9 @@ "payload_kind": "context_overflow_recovery", "contract_version": 1, "required_output_mode": "json_object", + "max_total_json_bytes": 120000, + "max_string_bytes": 8000, + "max_list_items": 16000, "required_fields": [ "ok", "status", diff --git a/contextlattice-dashboard/app/api/telemetry/memory/graph/route.ts b/contextlattice-dashboard/app/api/telemetry/memory/graph/route.ts new file mode 100644 index 00000000..0031a097 --- /dev/null +++ b/contextlattice-dashboard/app/api/telemetry/memory/graph/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from "next/server"; +import { callOrchestrator } from "@/lib/orchestrator"; + +export async function GET(request: Request) { + const url = new URL(request.url); + const params = new URLSearchParams(); + for (const key of ["project", "limit", "include_ephemeral"]) { + const value = url.searchParams.get(key); + if (value) { + params.set(key, value); + } + } + const suffix = params.toString() ? `?${params.toString()}` : ""; + const data = await callOrchestrator(`/telemetry/memory/graph${suffix}`); + return NextResponse.json(data); +} diff --git a/contextlattice-dashboard/app/status/page.tsx b/contextlattice-dashboard/app/status/page.tsx index fa357d4d..f438f83f 100644 --- a/contextlattice-dashboard/app/status/page.tsx +++ b/contextlattice-dashboard/app/status/page.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { RetrievalPanel } from "@/components/RetrievalPanel"; +import { MemoryGraphPanel, type MemoryGraphPayload } from "@/components/MemoryGraphPanel"; type Service = { name: string; @@ -61,17 +62,19 @@ export default function StatusPage() { const [preferences, setPreferences] = useState(null); const [topics, setTopics] = useState(null); const [memoryTelemetry, setMemoryTelemetry] = useState(null); + const [memoryGraph, setMemoryGraph] = useState(null); const [error, setError] = useState(null); const [updatedAt, setUpdatedAt] = useState(null); async function loadStatus() { try { setError(null); - const [statusRes, prefRes, topicRes, memRes] = await Promise.all([ + const [statusRes, prefRes, topicRes, memRes, graphRes] = await Promise.all([ fetch("/api/memory/status", { cache: "no-store" }), fetch("/api/memory/preferences", { cache: "no-store" }), fetch("/api/memory/topics", { cache: "no-store" }), fetch("/api/telemetry/memory", { cache: "no-store" }), + fetch("/api/telemetry/memory/graph", { cache: "no-store" }), ]); const statusData = await statusRes.json(); if (!statusRes.ok) { @@ -87,6 +90,9 @@ export default function StatusPage() { if (memRes.ok) { setMemoryTelemetry(await memRes.json()); } + if (graphRes.ok) { + setMemoryGraph(await graphRes.json()); + } setUpdatedAt(new Date().toLocaleTimeString()); } catch (err: any) { setError(err?.message || "Status unavailable"); @@ -206,6 +212,8 @@ export default function StatusPage() { + + ); diff --git a/contextlattice-dashboard/components/MemoryGraphPanel.tsx b/contextlattice-dashboard/components/MemoryGraphPanel.tsx new file mode 100644 index 00000000..6119330a --- /dev/null +++ b/contextlattice-dashboard/components/MemoryGraphPanel.tsx @@ -0,0 +1,211 @@ +"use client"; + +type CountRow = { + name?: string; + count?: number; +}; + +type ProjectRow = { + project?: string; + docs?: number; + connected_docs?: number; + isolated_docs?: number; + edges?: number; + inferred_edges?: number; + explicit_edges?: number; + density_edges_per_doc?: number; + top_relations?: CountRow[]; +}; + +type NodeRow = { + memory_id?: string; + degree?: number; + inbound?: number; + outbound?: number; +}; + +export type MemoryGraphPayload = { + ok?: boolean; + status?: string; + generated_at?: string; + doc_count?: number; + edge_count?: number; + connected_doc_count?: number; + isolated_doc_count?: number; + inferred_edge_count?: number; + explicit_edge_count?: number; + density_edges_per_doc?: number; + projects?: ProjectRow[]; + relations?: CountRow[]; + top_nodes?: NodeRow[]; + recommendations?: string[]; + edge_store?: { + bytes?: number; + path?: string; + }; +}; + +function numberValue(value: unknown): number { + return typeof value === "number" && Number.isFinite(value) ? value : 0; +} + +function MiniBar({ value, max }: { value: number; max: number }) { + const pct = max > 0 ? Math.max(0, Math.min(100, (value / max) * 100)) : 0; + return ( +