Second `louisTabular` agent deployed; tabular review chat wired to AgentCore

dropthejase adds a purpose-built `louisTabular` AgentCore agent for tabular review chat and works through the resulting bug cluster: jsonb string parsing across multiple routes, Bedrock client singleton fix, UUID cast on the document IN-query, citation schema fixes, and a revert of a retry mechanism that created stale UI. There's also a useful negative result: naive cross-endpoint retry on 500s causes stale state, not safety.

contract-reviewchat-ui

The two-agent architecture (louisMain for general chat, louisTabular for tabular review) keeps tool surfaces tight. The tabular agent has a single tool: read_table_cells, with the same ownership guard as the main agent. System prompt is built per-request with review title, column manifest, and document list.

The bug cluster after wiring is instructive. columns_config comes back from Aurora as a jsonb string and needs JSON.parse before use in the generation endpoint (66a5cec). The Bedrock client was being re-instantiated on every call; 66a5cec adds a module-level singleton (let _client: BedrockRuntimeClient | null = null). UUID casts on the document IN-query (::uuid on numbered params) appear in multiple places across the tabular route - same class of bug as the main Aurora migration, just showing up later.

The shared_with and columns_config parse-on-read pattern lands in GET and PATCH tabular review responses (6e5b266, 8a09da7), in workflow responses (714bcbd), and in mapTRMessages for the frontend (8c7e740). Each requires the same three-way check: typeof x === 'string' ? JSON.parse(x) : (x ?? []).

The retry attempt and revert are worth noting. 54fd7a3 adds a doRequest wrapper that retries once after 1.5 seconds on HTTP 500. 720650f reverts it immediately with the explanation that retrying idempotent and non-idempotent endpoints uniformly produces stale UI in unrelated parts of the app. The right fix was a resolve-batch endpoint (1fede12c) that reads S3 once, applies all bulk accept/reject operations in memory, and writes once - removing the race condition at the source.

Tabular chat storage migrates from Aurora to S3 in 29b80f4, matching the main agent's session persistence approach. Short-lived tabular chat turns stored in a relational DB were a mismatch to start with.

So what Worth a look if you're building a split-agent architecture and want to see how tabular review chat separates from general chat. The `resolve-batch` pattern for bulk edit operations is the most portable single piece. Skip the retry code entirely - it was reverted for good reason. The jsonb parse-on-read fixes are necessary infrastructure for anyone on Aurora Data API, not specific to this feature.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

13 commits from dropthejase/louis, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
1a79b1c0 Add louisTabular agent and wire tabular chat to AgentCore Jason Lee 2026-05-09 ↗ GitHub
commit body
- agents/app/tabular/: new Strands agent mirroring main structure; single
  read_table_cells tool; system prompt built per-request from review context
- backend: replace POST /:reviewId/chat (runLLMStream) with two thin endpoints:
  POST /:reviewId/chats (create) and POST /:reviewId/chats/:chatId/messages (persist)
- frontend: streamTabularChat now calls AgentCore louisTabular URL; TRChatPanel
  pre-creates chat record before streaming, persists turn after [DONE]
- config: NEXT_PUBLIC_AGENTCORE_TABULAR_URL added to config + env example
- docs: README and ARCHITECTURE updated
1fede12c feat(api): add resolve-batch endpoint to fix bulk accept/reject race condition Jason Lee 2026-05-12 ↗ GitHub
commit body
Single Lambda reads S3 once, applies all resolutions in memory, writes once.
Frontend handleAll sends one batch request per document instead of N parallel.
Also constrains agent to single-paragraph edits to avoid cross-para deletion failures.
6e5b266e fix(api): parse columns_config and shared_with JSON strings in tabular review responses Jason Lee 2026-05-13 ↗ GitHub
8a09da7b fix(api): parse columns_config and shared_with in PATCH tabular review response Jason Lee 2026-05-13 ↗ GitHub
a0d653c1 feat(tabular): optimistic update when adding documents to review Jason Lee 2026-05-13 ↗ GitHub
54fd7a39 feat(frontend): retry apiRequest once on 500 after 1.5s delay Jason Lee 2026-05-13 ↗ GitHub
720650fc revert(frontend): remove apiRequest retry - caused stale UI across endpoints Jason Lee 2026-05-13 ↗ GitHub
66a5cecb fix(tabular): fix generation failures - columns_config parse, Bedrock client, UUID cast Jason Lee 2026-05-13 ↗ GitHub
commit body
- Parse columns_config from RDS Data API string before use in generate endpoint
- Remove stale 'const client = client' self-references in bedrock.ts (ReferenceError)
- Rename queryGeminiAllColumns/queryGemini to queryBedrockAllColumns/queryBedrock
- Add ::uuid cast to :pid${i} params in shared-by-project query
e2ed854f fix(tabular-agent): fix credential bootstrap, columns_config parse, and UUID cast on document IN query Jason Lee 2026-05-14 ↗ GitHub
714bcbd1 fix(api): parse columns_config JSON string in workflow responses Jason Lee 2026-05-14 ↗ GitHub
commit body
Aurora Data API returns jsonb columns as raw strings. withWorkflowAccess
was spreading the raw DB row without parsing columns_config, causing
TypeError: .sort is not a function on the WorkflowPage and corrupt
double-serialized data when creating a tabular review from a custom workflow.

Parse in withWorkflowAccess so all workflow endpoints (GET /, GET /:id,
PUT, PATCH) are covered by a single fix.
8c7e7405 fix(tabular): parse jsonb content/annotations in mapTRMessages Jason Lee 2026-05-14 ↗ GitHub
42c8e187 fix(tabular): suppress CITATIONS from stream, fix citation schema and extraction Jason Lee 2026-05-14 ↗ GitHub
29b80f48 feat(tabular): migrate tabular chat storage from Aurora to S3 Jason Lee 2026-05-14 ↗ GitHub
commit body
Tabular review chat messages now stored in S3 (conversations/{chatId}/messages.json)
using the same Strands snapshot format as the main chat agent. This gives the tabular
agent full conversation history on every turn (agent memory).

- agent.ts: loadMessages (S3 GET) + AfterInvocationEvent hook (S3 PUT)
- index.ts: load previous messages, pass to createAgent, tail-buffer suppresses <CITATIONS>
- sessions.ts: new shared lib - S3 read/write + snapshotMessagesToSessionMessages
- chat.ts: remove ~250 lines of private session logic, import from sessions.ts
- tabular.ts: replace Aurora message reads/writes with S3 via sessions.ts
- mikeApi.ts + TRChatPanel.tsx: remove Aurora-specific message mapping

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-345.md from inside the repo you want the changes in.

⬇ Download capture-thread-345.md