Read-only demoLive view of gallirohik/research-canvas at 1ebb712, scanned 20 September 2026 — nothing here can be edited.
Join the waitlistToday report (agents/python/src/lib/state.py:37) is a plain string the model writes via the WriteReport tool and the user free-edits in a <Textarea> (ResearchCanvas.tsx:191-200). resources are fetched, cached, and rendered as cards fully decoupled from the report text (Resources.tsx) — there is no mechanism, today, connecting any sentence in the report to the resource(s) that support it. The ask is two things: (1) every claim in the report should be traceable to a resource, (2) a graph node should check the draft against the sources and flag statements nothing supports.
report into a structured claim tree — citations are a derived, decoupled layer, recomputed each fact-check run.WriteReport call — rejected (extra LLM call + latency per edit).Search/DeleteResources.rafa review's deterministic pre-pass (8 changed files, 31/32 notes engaged, 6 deep) + a prism-style judge pass found the diff coherent and grounded, and caught 4 brain notes this diff had made stale/incomplete — fixed as branch working-set edits: agent-state-shape-contract (now documents 6 fields, Python+frontend-only), (now documents the 6th node, ), (now documents the branch + fixed 5 drifted line-cites), and (fixed one drifted line-cite). passes 43/43 resolution, 1/1 policy, after the fixes. These land in the org brain at merge-to-main distillation.
Residual risk (surfaced, not silently accepted): no live, LLM-driven fact-check run has ever executed — this dev environment has no OpenAI/Anthropic/Tavily API keys. Everything about the runtime path is proven by static trace + parity with the already-working search_node, not by execution. Run one live smoke test with real API keys before this reaches real users.
Implement the fact-check tool + graph node (Python agent)
3112f4f. prism traced all 3 exit paths and confirmed ToolMessage resolution is airtight against the tool_calls-must-be-followed-by-result 400. Non-blocking notes: logs not cleared between runs (cosmetic), no defensive tool_calls guard (safe today). Done-check: prism PASS.citations: List[Citation] field to AgentState (Citation{claim, resource_urls, supported, note}), Python + frontend type only.FactCheckReport routes to a new fact_check_node, following the existing search_node structured-extraction pattern.Progress.tsx's step list) renders each claim with a supported/unsupported badge and a clickable link to its backing resource(s).frontend-state-any-typing, coagent-initialstate-divergence.citationslanggraph-agent-conventionfact_check_noderesearch-chat-flowFactCheckReportmodel-selection-flowrafa verify-citationsfact_check.py exists with FactCheckReport tool + fact_check_node mirroring search_node.chat.py binds + routes on FactCheckReport; agent.py wires the node + edge.ToolMessage appended on every exit path (verified by trace).Type resources/logs (and the new citations) with real types instead of any[]
9f6ba83. prism traced every reader/writer, confirmed no site relied on any-looseness. report_improvement_status(frontend-state-any-typing, fixed) reported + local ledger file updated. Done-check: prism PASS.AgentState.resources: Resource[], Log type declared, AgentState.logs: Log[].Render citations in the canvas — claim list linked to resources, unsupported claims flagged
6306ff3. prism's first pass caught a real latent crash (state.citations.length with no undefined-guard, inconsistent with every other field in the file) — fixed + a minor class-name nit, commit e327bf5, prism re-verified PASS. Correction to the record: prism's re-verification also claimed npx tsc --noEmit passed — independently re-checked, that's npm's placeholder joke package (exit 1) not a real check, and is excluded from the record; the rest of the re-verification was legitimate. Done-check: prism PASS.FactCheck.tsx renders each citation with a distinct supported/unsupported state + clickable resource links.ResearchCanvas.tsx renders it, properly undefined-guarded, when citations is non-empty.Unify the two useCoAgent initialState seeds and include the citations default
e0c9058. prism traced render guards old vs new, confirmed unchanged behavior. report_improvement_status(coagent-initialstate-divergence, fixed) reported + local ledger file updated. Done-check: prism PASS.Add a Citation type + citations field to AgentState (Python agent + frontend)
35dcc30. Build unverifiable in this standalone checkout (no node_modules, workspace:* deps); prism verified via real @copilotkit type defs instead. Done-check: prism PASS.state.py declares Citation TypedDict + AgentState.citations: List[Citation].types.ts declares matching Citation interface + citations: Citation[].useCoAgent<AgentState> call sites still compile.End-to-end manual verification across state, node, and UI
2026-07-24 — Copied repo to scratch dir, swapped workspace:* for real @copilotkit@^1.63.2, ran real tsc --noEmit (0 errors), real next build (compiled successfully, 6/6 pages), real next dev boot + curl (HTTP 200, correct render, citations panel correctly hidden when empty). npm run lint genuinely N/A (no ESLint config in repo, pre-existing). prism independently reproduced the entire recipe and got matching results, then reviewed the full 8-file diff for coherence.
Residual risk, surfaced loudly: no live LLM-driven fact-check run has ever executed (no OpenAI/Anthropic/Tavily API keys in this environment). Proven: build/type/graph-wiring correctness + parity with the working search_node. NOT proven by execution: the model actually returns well-formed ExtractClaimChecks args, citations populate/stream for real, supported:false renders correctly on real output, and a post-fact-check chat turn doesn't 400. Run one live smoke test with real API keys before shipping to real users.
Done-check: prism PASS on the plan as a whole, with this caveat surfaced rather than silently accepted.
dev · plan-level · 7/23/2026, 7:42:44 PM
Python-only for now.
Whether the new fact-check node/tool should be mirrored into the TypeScript agent port (agents/typescript/) in this same plan, or left Python-only.
considered: Python-only for now (defer TS parity as a fast-follow) · Include TS parity in this plan (mirror node/tool in agents/typescript/ immediately)
why: Matches the repo's existing accepted convention (agent-typescript-parity) that the TS port is a lagging alternate not run by default; ships faster and halves the surface to build/test.
dev · plan-level · 7/23/2026, 7:42:42 PM
The agent's DeleteResources tool routes to an interrupt, and the frontend renders the confirm dialog via useCopilotAction under the SAME string name — a mismatch means the delete never prompts and silently stalls
Authoritative trace (tight, cite-proven) of a CopilotChat turn through the /api/copilotkit runtime into the LangGraph agent graph and how streamed state signals render the canvas (download → chat → search/delete/fact-check → frontend render).
Both chat nodes re-serialise all downloaded resources into the system prompt each turn, so one long article can exceed an entire per-minute token budget in a single request; the TS agent caps this (8k/resource, 24k total) as of 2026-07-28, the Python agent does NOT. Rule: any new node that injects resource text into a prompt MUST budget it (per-resource and aggregate caps, like MAX_TOTAL_RESOURCE_CHARS in the TypeScript agent) — the missing Python cap is an open P1 improvement, not permission to skip the cap
The ModelSelector writes ?coAgentsModel to the URL; that string becomes state.model and drives get_model on the backend — the agent NAME only branches google_genai vs not, and both names resolve to the same graph. The wiring is correct for all four options, but the anthropic MODEL ID is dead (claude-3-5-sonnet-20240620, retired 2025-10-28): check the id before the key
Add the no-op @tool schema, bind it in the chat node, route on its name, and — if it drives UI or streams a state field — mirror the string on the frontend (useCopilotAction) or in an emit_intermediate_state mapping
One Next.js route handler proxies the browser to the LangGraph agents; it uses EmptyAdapter (agents own the LLM), gates on a public "api key", toggles HTTP vs LangGraph-Cloud mode by URL param, and DNS-resolves the lgcDeploymentUrl before trusting it
"research_agent" / "research_agent_google_genai" must match across the frontend agent prop, the runtime registry, and each agent's langgraph.json + endpoint path, or the coagent silently never connects
The Python agent is a 6-node StateGraph (download→chat→search/delete/fact-check loop); tools are empty @tool schemas the model CALLS but the node handles; the concrete LLM is chosen at runtime by get_model from state.model. The TS agent still mirrors the older 5-node shape (fact_check_node not yet ported, verifiable-report plan decision)
UI is shadcn 'new-york' Radix primitives in components/ui with CSS-variable tokens and the cn() merge helper; feature components layer on top but hardcode the #6766FC/#0E103D brand colors inline rather than as tokens
Six fields (model, research_question, report, resources, logs, citations) are defined across the TS frontend type and the Python agent; citations is Python+frontend ONLY (the TS agent's state.ts intentionally lags, verifiable-report plan decision) — a field the backend emits but the frontend doesn't expect (or vice versa) simply doesn't render