Skip to content

Agent citations + multi-turn conversations - #9

Merged
codedsultan merged 6 commits into
mainfrom
feat/agent-citations-multiturn-history
Jul 27, 2026
Merged

Agent citations + multi-turn conversations#9
codedsultan merged 6 commits into
mainfrom
feat/agent-citations-multiturn-history

Conversation

@codedsultan

Copy link
Copy Markdown
Owner

Summary

Closes the two chat-surface gaps from the DevX review: citations were
silently dropped on /v1/agent/chat (the endpoint the frontend actually
uses), and neither chat endpoint retained conversation history.

What changed

Citations (short-circuit)
query_documents already returns {answer, citations} — the gap was
purely that AgentService never forwarded it. Rather than let the outer
model re-synthesize (risking marker/citation misalignment), a cited
tool result now short-circuits straight to a final answer and emits
the citations event the frontend was already built to render.

Multi-turn conversations
New Conversation/Message tables, trimmed history hydration into the
agent's message state, and a conversationId round-tripped via SSE
(conversation_started on first turn). History trimming is hybrid:
hard cap of 20 messages, then a ~3000-token budget trim within that,
always preserving the latest turn intact. Scoped to /v1/agent/chat only.

Frontend
/chat is now a real thread (message list + conversationId) instead of
a single-answer view; a "New conversation" control resets it.

Testing

  • agent.service.spec.ts: citation short-circuit (1 generate() call
    only), history seeding, persistence across all exit paths (cited,
    normal, max-iterations, confirmation-pause).
  • history.util.spec.ts: hard ceiling, token-budget trim, min-keep
    safeguard.
  • Not yet run in CI — needs npx prisma migrate dev --name add_conversations
    against a local Postgres before pnpm test.

Manual verification checklist

  • Migration applies cleanly
  • Ask a question, then a follow-up referencing "it" — agent uses
    prior context
  • Citation badges still render on a query_documents answer
  • Confirmation flow (external_write tool) still pauses correctly
    and doesn't persist a phantom assistant message

query_documents already returns {answer, citations}. Previously the
agent looped the result back through modelTurn, letting the outer
model rewrite the answer and risk dropping or renumbering the [N]
citation markers relative to the emitted citations array.

- Add finalAnswerOverride to AgentState; toolDispatchNode detects a
  cited-answer-shaped tool result and short-circuits straight to END
  instead of another modelTurn iteration.
- Stream consumer emits the (previously dead)  SSE event
  alongside the tool's own answer text.
- Dedupe the 3x-duplicated TurnCompleted emission into one helper.
- Add spec coverage asserting exactly one generate() call on this path.

No schema, DTO, or frontend changes — useChatStream/chat page already
handled the  event.
Neither chat endpoint remembered prior turns. This adds persistent,
trimmed history to /v1/agent/chat (the only surface the frontend uses).

- Prisma: Conversation + Message models, MessageRole enum.
- ConversationsModule: create/list conversations, load+persist
  messages, ownership checks. GET /v1/conversations and
  GET /v1/conversations/:id/messages for future history reload.
- trimHistory(): hybrid cap — last 20 messages, then ~3000 token
  budget within that window, always keeping at least the latest turn.
- AgentChatDto.conversationId (optional); AgentController creates a
  conversation and emits `conversation_started` when omitted.
- AgentService.run() seeds trimmed history into the graph's initial
  messages state, persists the user query immediately and the final
  assistant answer once produced. Nothing persisted on a turn that
  pauses for external_write confirmation.
- Frontend: useChatStream now holds a messages[] thread + conversationId
  instead of single content/citations state; /chat renders a scrollable
  thread with a "New conversation" control.

Scoped to /v1/agent/chat only; /v1/chat/stream stays single-turn.
- Escape apostrophe in chat/page.tsx JSX text
- Add explicit parameter type to generate() mock in agent.service.spec.ts
- Use custom generated client import path for Prisma namespace in conversations.service.ts
- Escape apostrophe in chat/page.tsx JSX text
- Add explicit parameter type to generate() mock in agent.service.spec.ts
- Use custom generated client import path for Prisma namespace in conversations.service.ts
- Escape apostrophe in chat/page.tsx JSX text
- Add explicit parameter type to generate() mock in agent.service.spec.ts
- Use custom generated client import path for Prisma namespace in conversations.service.ts
@codedsultan
codedsultan merged commit a8e1e4d into main Jul 27, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant