feat(phase8 #93 D8.5-FE): consume canonical AgentTurnSnapshot history + render user bubble from input_text#1707
Merged
Merged
Conversation
…kind discriminator Phase 8 task #92 (D8.5-BE) — first-cut backend migration of the non-agent chat path to the canonical ``UIMessage`` shape, scoped per architect msg=01918929 + Weston msg=df87fe24 + earayu2 msg=f20d5034 hard-cut acceptance: The inventory revealed the production "non-agent chat path" the original D8.5 design assumed has already converged on the agent runtime (``chat_completion_service.openai_chat_completions`` already delegates to ``runtime_manager.turn_service.create_or_get_turn`` and ``ChatService.create_chat`` rejects non-AGENT bots). So the actual #92 work is A+B+C only — adding the discriminator column for future non-agent paths and migrating the user-visible chat history shape to canonical UIMessage. The translator extension (``chat.text.delta`` / ``chat.completed``) and the ``StoredChatMessagePart`` / ``RedisChatMessageHistory`` deletion are deferred per architect / Weston canonical lock. Changes: A. ``runtime_kind`` discriminator on ``agent_message`` table - ``aperag/domains/agent_runtime/db/models.py``: new ``runtime_kind: str`` ORM column with values ``agent_runtime`` / ``direct_chat`` / ``rag_chat`` (mutually exclusive enum); existing rows backfill via ``server_default="agent_runtime"``. ``role`` keeps speaker semantics independent of the runtime that produced the message. - ``aperag/migration/versions/...c8f2d34a51e7_add_agent_message_runtime_kind.py``: additive migration; downgrade drops the column. B. ``ChatService._build_v3_chat_history`` rewrite - Returns ``list[AgentTurnSnapshot]`` (one snapshot per assistant turn) instead of the legacy ``list[list[ChatMessage]]`` shape. - Reuses ``snapshot_assembler.assemble_parts_from_artifacts`` (the #90 D8.4d projection) so historical turns expose the same ``UIMessagePart`` shape the FE consumes from the live SSE stream (D8 §2 wire/at-rest byte-equal). - ``error_text`` for FAILED / CANCELLED turns surfaces an ``error_summary`` artifact's message, falling back to ``turn.error_message`` — mirrors the snapshot endpoint contract. - The turn's user query lives at ``input_text`` on the snapshot envelope (rather than as a separate ``role=human`` ChatMessage) so the FE renders user/assistant from a single object per turn. - Legacy ``_extract_artifact_text`` / ``_extract_references`` / ``_map_reference_item`` / ``_artifact_type_value`` / ``_coerce_timestamp`` helpers are retired alongside the legacy shape. C. ``ChatDetails.history`` schema - ``aperag/domains/conversation/schemas.py``: ``history`` is now ``Optional[list[AgentTurnSnapshot]]`` with explicit description citing D8 §2 byte-equal canonical and the new shape. - The ``conversation.schemas`` ↔ ``agent_runtime.uimessage`` ↔ ``agent_runtime.schemas`` ↔ ``conversation.schemas`` cycle is broken via ``TYPE_CHECKING`` import + a module-level ``ChatDetails.model_rebuild()`` hook at the bottom of ``conversation/schemas.py``. Pydantic resolves the forward ref at load time so the OpenAPI schema is fully populated. - ``aperag/domains/agent_runtime/uimessage.py``: ``AgentTurnSnapshot`` gains ``runtime_kind: RuntimeKind`` (default ``"agent_runtime"``) and ``input_text: Optional[str]`` so historical turns can render the user query without a separate envelope round-trip. - ``TurnService.get_turn_snapshot`` writes both new fields on the live snapshot endpoint so live and historical reload paths match. D. (deferred) Translator extension for ``chat.text.delta`` / ``chat.completed`` and ``StoredChatMessagePart`` / ``RedisChatMessageHistory`` deletion stay out of #92 per Weston msg=df87fe24 / PM msg=01918929. The non-agent live path the extension would have served does not exist in the current codebase; reintroducing it is a feature task, not a refactor. Tests: - ``tests/unit_test/chat/test_chat_service.py`` rewritten: * ``test_get_chat_returns_canonical_uimessage_history`` pins the new shape (snapshot per turn with text + source-url + data-citation parts, runtime_kind, input_text) * ``test_get_chat_history_surfaces_error_text_for_failed_turn`` pins the error_text contract for FAILED turns * ``test_get_chat_history_does_not_expose_legacy_chatmessage_shape`` regression-guard against revert to ``list[list[ChatMessage]]`` - ``tests/unit_test/agent_runtime/test_agent_runtime_v3.py`` updated to import ``AgentTurnSnapshot`` from ``agent_runtime.uimessage`` (the back-compat re-export through ``agent_runtime.schemas`` was retired to break the new cycle). Per D10 §G hard gate 1 (comprehensive grep sweep) ran across ``aperag/`` + ``tests/unit_test/`` + ``tests/e2e_http/hurl/`` + ``tests/e2e_http/scripts/``: only the FE ``web/src/components/chat/chat-messages.tsx`` reads ``chat.history`` in the old shape — that is the explicit hand-off seam for #93 huangheng (per architect msg=6e53a7c4). Gates: full unit suite 833 / 29 skip / 0 fail; ruff check + format clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… + render user bubble from input_text D8.5-FE first-cut, chained on @bryce #92 D8.5-BE (`bryce/phase8-task92-d85-be-non-agent-uimessage`). Per architect msg=a92ca060 + PM lock msg=38e116e5 + Bryce handoff msg=27e8ec6d: * `ChatDetails.history` is now `AgentTurnSnapshot[]` (canonical UIMessage at-rest, byte-equal with the live SSE wire). * `AgentTurnSnapshot` carries `runtime_kind` (forward-compat discriminator, FE does not branch on it per PM lock) + `input_text` (user-side bubble content). * No new non-agent SSE / live path — production code already routes everything through `agent_runtime`. The deferred `chat.text.delta` / `chat.completed` envelope expansion stays out per Bryce msg=27e8ec6d D defer and PM acceptance. ## What changed (FE) * `web/src/components/chat/chat-messages.tsx` — full rewrite of the per-turn render orchestration: * State replaces `messages: ChatMessage[][]` with `liveTurns` map + `turnOrder: string[]` + `pendingUserMessages: { key, query, timestamp }[]`. * `chat.history` (canonical `AgentTurnSnapshot[]`) directly seeds `liveTurns` at mount via `seedFromHistory()` — no per-turn `getAgentTurnSnapshot()` round trip on first render. * `seedFromSnapshot()` and `ensureTurnGroups()` (tied to the legacy `ChatMessage[][]` shape) are gone; `recordTurn()` replaces them. * `handleSendMessage()` adds an optimistic `pendingUserMessages` entry until `createAgentTurn` returns the real turn id, then promotes the turn into `liveTurns` and drops the pending entry. * `recoverActiveTurn` effect still re-fetches the snapshot for the sessionStorage-active turn id so a mid-stream reload picks up cursor / status drift since page load. * `AgentTurnStreamCard` now renders `<MessagePartsUser>` from `envelope.input_text` inline above the AI card so historical and live turns share one render path. The legacy `MessagePartsAi` branch is gone (canonical parts handle historical render too). * `web/src/features/agent-runtime/api.ts` — `AgentTurnSnapshotEnvelope` extended with `runtime_kind: AgentRuntimeKind` (`agent_runtime` | `direct_chat` | `rag_chat`, default `agent_runtime`) and optional `input_text`. `AgentRuntimeKind` re-exported from the feature index. * `web/src/api-v2/schema.d.ts` — regenerated via `yarn api:v2:types` against the post-#92 OpenAPI public spec. ## Boundary held (per PM lock) * No new `chat-runtime/` feature module — the agent-runtime hook + renderer cover the historical render path 1:1 (per architect msg=38e116e5 lock). * `runtime_kind` stays a BE-internal discriminator; FE does not branch on it. * Legacy `MessagePartsAi` / `MessagePartAi` / `StoredChatMessagePart` files remain on disk — Python-side schema/storage delete is #80 territory, and the FE files have no callers after this PR but are not removed here (kept for #80 sweep). ## Verification * `yarn lint` clean (one pre-existing `no-explicit-any` warning in `features/providers/server-api.ts` unrelated to this PR). * `tsc --noEmit` clean for touched files (the four pre-existing errors in `chat-input.tsx` are main-baseline noise unchanged here). * `yarn dev` boots in 2.6s on port 3014; `GET /`, `/auth/signin`, `/workspace/collections`, `/workspace` all return 200. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Base automatically changed from
bryce/phase8-task92-d85-be-non-agent-uimessage
to
main
April 25, 2026 18:57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
D8.5-FE first-cut. Chained on @bryce's #92 PR #1706 (
bryce/phase8-task92-d85-be-non-agent-uimessage).ChatDetails.historyflips fromlist[list[ChatMessage]]→list[AgentTurnSnapshot]. The FE consumes the canonical UIMessage at-rest envelope directly: historical turns share the live-turn render path; user bubbles come fromsnapshot.input_text. LegacyMessagePartsAirendering branch is gone.What changed (FE)
web/src/components/chat/chat-messages.tsxliveTurnsmap +turnOrder+pendingUserMessages(optimistic). SeedsliveTurnsdirectly fromchat.history.AgentTurnStreamCardnow renders<MessagePartsUser>fromenvelope.input_textinline above the AI card.web/src/features/agent-runtime/api.tsAgentTurnSnapshotEnvelopeextended withruntime_kind: AgentRuntimeKind+input_text.AgentRuntimeKindexported.web/src/features/agent-runtime/index.tsAgentRuntimeKind.web/src/api-v2/schema.d.tsyarn api:v2:typesagainst post-#92 OpenAPI.Boundary held (per PM lock msg=38e116e5)
chat-runtime/feature module —agent-runtime/covers the historical render path 1:1.runtime_kindis BE-internal — FE does not branch on it.MessagePartsAi/StoredChatMessagePartdeletion — Python-side schema/storage delete is [Features] on premise deployment #80 territory; the FE legacy files have no callers after this PR but stay on disk for the [Features] on premise deployment #80 sweep.agent_runtime; per Bryce msg=27e8ec6d D defer + PM acceptance.D8.5-BE / D8.5-FE handoff contract
ChatDetails.history: list[AgentTurnSnapshot]— done in BE PR feat(phase8 #92 D8.5-BE): canonical UIMessage chat history + runtime_kind discriminator #1706.snapshot.input_textpaired withAgentTurnRenderer(parts, status, error_text).useAgentTurnStreamhook contract unchanged.Verification
yarn lintclean (one pre-existingno-explicit-anywarning infeatures/providers/server-api.tsunrelated).tsc --noEmitclean for touched files (pre-existing main-branch errors inchat-input.tsxunchanged).yarn devboots in 2.6s on port 3014;GET /,/auth/signin,/workspace/collections,/workspaceall return 200.Test plan
Note on chained base
This PR's base is
bryce/phase8-task92-d85-be-non-agent-uimessage(PR #1706). Once #1706 lands on main, GitHub will auto-rebase this branch.🤖 Generated with Claude Code