Repository navigation
feat(narratives): add HMAC-chained transcripts, state slots, and multi-turn context - #501
Conversation
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements secure transcript signing, verification, and compaction to support multi-turn context in the Narratives agent. It introduces a sliding window of signed turns (up to 6) using HMAC-SHA256 to prevent client-side tampering, and folds older turns into a compacted summary when the window overflows. The backend endpoint /chat/stream is updated to enforce request size limits and verify transcripts before starting a turn, while the frontend is updated to store and round-trip these signed transcripts. Feedback on the newly added state_slots.py suggests simplifying the _clean_name whitespace collapsing, making the _strings helper more robust for dictionary values, and avoiding redundant _display_name evaluations by checking dictionary membership first.
juliawu
left a comment
There was a problem hiding this comment.
This looks great! Left some comments found with the Gemini skill.
Overview
This PR is the fourth stage of the Narratives model loop migration. It replaces the unsigned, client-assembled
historyarray on/agent/chat/streamwith an HMAC-SHA256-chainedTranscriptwindow, extracts groundedConversationStateSlots(QueryScopeentries) from each turn's MCP tool results and validated chart configurations, compacts turns beyond the six-turn sliding window into a bounded summary, and passes verified conversation context into both the MCP tool loop and the synthesis phase.Isolating conversation history behind
Transcript(load_transcript,transcript_contents, andfinalize_turn) keeps the model loop independent of how history is transported, so moving conversation history to server-side session storage later only requires updatingload_transcriptandfinalize_turn. On the client,useSseChatpersists the signed fields returned on eachcompleteterminal frame (turn_index,hmac,state_slots,compacted_summary, and the re-signedwindowarray) inlocalStorageand sends only verified turns on subsequent requests.Related Issues
This PR follows #494, #495, and #496 in the Narratives model loop migration.
Changes Made
TRANSCRIPT_HMAC_SECRET(Settings.transcript_hmac_secretas aSecretStr, validated to at least 32 UTF-8 bytes when set) insettings.py, with a per-process random fallback key for local development and a startup warning on Cloud Run (K_SERVICE) when unset.workflows/state_slots.py): Addedextract_state_slotsto buildConversationStateSlots(QueryScoperecords mapping place DCIDs, parent-place cohorts, and statistical variable DCIDs to single-line display names, plus min-to-max observation date ranges) strictly from MCP observation calls that returned rows and rendered chart configurations whose DCIDs were grounded in tool payloads. Addedhas_observation_rowsinmcp/data_utils.py.workflows/transcript.py): AddedTurn,Transcript,SignedWindow,TranscriptError,load_transcript,transcript_contents, andfinalize_turn. Each turn'shmacchains over the domain tag (narratives.transcript.turn.v1), the predecessor'shmac, the turn's fields, and the window'scompacted_summary. When a completed turn pushes the window pastMAX_VERBATIM_TURNS(6),finalize_turnsummarizes evicted turns via Gemini (with a 5s timeout and a deterministic fallback summary) and re-signs the retained window.server/routes/chat.py,workflows/chat_pipeline.py,workflows/mcp_loop.py): Bounded request bodies toMAX_REQUEST_BYTES(4 MiB, returning HTTP 413request_too_large), replacedChatRequest.historywithturnsandcompacted_summaryvalidated byload_transcriptbefore opening the SSE stream (returning HTTP 400transcript_invalidon failure), passedTranscriptintoexecute_mcp_tool_loopandrun_synthesis_phase, and populatedturn_index,hmac,state_slots,compacted_summary, andwindowon thecompleteterminal frame.ui/src/hooks/use_sse_chat.ts): AddedtranscriptRequestFieldsandapplySignedWindowto store signed turns inChatSessionProvider(localStorage), sendturnsandcompacted_summaryinstead ofhistory, update stored signatures when compaction re-signs the window, and clear stored signatures when the agent returns HTTP 400transcript_invalid.transcript_test.py,state_slots_test.py,settings_test.py,chat_test.py,chat_pipeline_test.py,mcp_loop_test.py,use_sse_chat.test.ts, andchat_session_context.test.ts, and updatedREADME.mdanddocs/smoke.sh.Behavior changes
MCP_HISTORY_RESPONSE_CHARS = 2_000per prior answer in the MCP loop; full answers in synthesis), along with the compacted summary of older turns and the structuredQueryScopedescriptions wrapped in<conversation_context>tags.request_too_large). Requests carrying an altered turn, a mismatched signature chain, non-contiguous turn indexes, duplicate idempotency keys, or fields outside schema caps fail before streaming with HTTP 400 (transcript_invalid). When the UI receives HTTP 400transcript_invalid(for example, after a key rotation or an unconfigured server restart), it clears stored signatures so the next message starts a fresh context window rather than failing every subsequent turn.compacted_summary(capped at 8,000 characters) and the retained 6 turns are re-signed from the front of the window. If the compaction model call times out (5s) or errors, a deterministic fallback summary is built from the evicted turns so compaction never fails a turn whose answer has already streamed.MAX_RESPONSE_CHARS(32,000 characters) or signing fails unexpectedly, the turn still completes normally without signing fields on theterminalframe, and the browser omits that turn from future context requests while retaining earlier signed turns.Testing Done
Verified the Python agent locally from
narratives/agent/:Ran the UI type-check, the full workspace test suite, and the production build from
narratives/with the Node version from.nvmrc:nvm use pnpm -C ui run lint pnpm test pnpm buildAll 146 UI tests and 611 agent tests pass.
Manual verification against a live MCP server:
narratives/agent/, exportMCP_SERVER_URL=https://api.datacommons.org/mcp,DATA_PLANE_URL=https://api.datacommons.org,DATA_PLANE_WEB_URL=https://datacommons.org,DC_API_KEY, andTRANSCRIPT_HMAC_SECRET=$(openssl rand -hex 32), then runuv run narratives-agent-dev.narratives/, runpnpm -C ui run devand openhttp://localhost:3000.terminalSSE frame includesturn_index: 0,hmac,state_slots, andwindow.turns(with turn 0's signature and state slots) and that the agent resolves the comparison variable and entities from the first turn.Risk & Rollback
Low risk. Narratives has no active deployments. For multi-instance Cloud Run deployments,
TRANSCRIPT_HMAC_SECRETshould be configured so all instances share the same signing key; if unset, each instance generates an ephemeral key and logs a warning at startup. To roll back, revert the merge commit onnarratives-dev.Follow-ups
Checklist
AGENTS.mdand followedCODING_GUIDELINES.md, plusFRONTEND.mdfor UI changes.Note: Only Maintainers can approve and merge PRs. Expected initial review time: 3 business days.