Skip to content

feat(phase8 #90 D8.4d): snapshot endpoint returns canonical UIMessage parts#1705

Merged
earayu merged 5 commits into
mainfrom
bryce/phase8-task90-d84d-snapshot-uimessage-parts
Apr 25, 2026
Merged

feat(phase8 #90 D8.4d): snapshot endpoint returns canonical UIMessage parts#1705
earayu merged 5 commits into
mainfrom
bryce/phase8-task90-d84d-snapshot-uimessage-parts

Conversation

@earayu
Copy link
Copy Markdown
Collaborator

@earayu earayu commented Apr 25, 2026

Summary

Phase 8 task #90 (D8.4d) — replaces the agent runtime turn snapshot endpoint's legacy {turn, timeline, artifacts} envelope with the canonical UIMessage-aligned AgentTurnSnapshot shape. The FE renderer (#76 / #77 / #78) now consumes the same UIMessagePart discriminated union from both the live SSE stream and the at-rest reload path, per D8 §2 wire / at-rest byte-equal canonical (architect msg=711f8c2f canonical lock + PM scope msg=383c2e2b / msg=247f4d8e).

What changed

Backend

  • NEW aperag/domains/agent_runtime/snapshot_assembler.py — projects legacy AgentArtifact rows into UIMessagePart[] (answer → TextPart, reference_bundle → SourceUrlPart + DataCitationPart, error_summary → error_text). Mirrors the FE-side snapshot-fallback.ts adapter that feat: support default collections #77 huangheng landed as a transitional bridge so deletion was mechanical from both sides.
  • AgentTurnSnapshot (now defined in uimessage.py alongside the rest of the UIMessage family; re-exported from schemas.py for back-compat) flips to {schema_version, turn_id, chat_id, role, status, parts, error_text?, timeline_cursor, ...timestamps}.
  • TurnService.get_turn_snapshot() rewritten:
    1. Forward-compat — try UIMessageStore.read(turn_id) first (D8.6 will populate agent_message.parts directly via the wire emitter; today the store is optional and reads return None).
    2. Fallbackassemble_parts_from_artifacts projects legacy artifacts.
    3. error_textextract_error_text pulls the error_summary payload (or summary), falling back to runtime_state.error_message.
  • The 3 ownership-only callers of get_turn_snapshot (cancel / consent / elicit endpoints) are unaffected — they only use the call to trigger ResourceNotFoundException, never read the body.

Frontend (deletes the #77 transitional adapter)

  • web/src/features/agent-runtime/api.tsAgentTurnSnapshotEnvelope flips to the new flat shape; legacy {turn, timeline, artifacts} types gone.
  • web/src/components/chat/chat-messages.tsxseedFromSnapshot synthesizes a minimal AgentTurnEnvelope from the new flat snapshot for the live-turn store; reload rendering reads baselineSnapshot.parts and baselineSnapshot.error_text directly with no client-side synthesis.
  • web/src/features/agent-runtime/snapshot-fallback.tssynthesizePartsFromSnapshot and extractErrorTextFromSnapshot are removed (their TODO(#90) trigger has fired). mapBackendTurnStatus / isTerminalBackendStatus are kept (status mapping stays useful).
  • web/src/features/agent-runtime/index.ts — dropped exports removed from the barrel.

Boundary discipline

Tests

  • NEW tests/unit_test/agent_runtime/test_snapshot_assembler.py — 8 contract tests pinning the artifact → UIMessagePart projection (answer text, reference-bundle fan-out with and without uri, ordering, unknown-artifact skip, error-text extraction with payload-preferred / summary fallback / none-without-error_summary).
  • tests/unit_test/agent_runtime/test_agent_runtime_v3.py snapshot tests rewritten for the new shape:
    • test_turn_snapshot_returns_canonical_uimessage_parts_for_completed_turn
    • test_turn_snapshot_surfaces_error_text_for_failed_turn
    • test_turn_snapshot_does_not_expose_legacy_keys (regression-guard: {turn, timeline, artifacts} must not reappear)
    • test_turn_snapshot_user_activity_inference_runs_via_event_service pins the empty-timeline guarantee.
  • tests/e2e_http/hurl/full/15_agent_runtime_v3.hurl snapshot assertions migrated to the new shape (turn_id, chat_id, role, status, parts at top level; legacy keys gone).
  • tests/unit_test/agent_runtime/test_agent_runtime_openapi_contract.py — continues to pass (schema name unchanged).

Test plan

  • pytest tests/unit_test/agent_runtime/ -q134 passed
  • pytest tests/unit_test --deselect concurrent_control flake -q831 passed / 29 skipped / 0 failed
  • ruff check → clean
  • ruff format --check → clean (446 files already formatted)
  • e2e-http-smoke + e2e-http-provider — pending CI (per merge-gate rule)
  • FE typed schema (web/src/api-v2/schema.d.ts) regen via yarn api:v2:types — yarn is not available in this worktree; CI / a follow-up FE-touching PR will resync if a drift is observed.
  • CR — pending Weston (per architect msg=711f8c2f middle-term lane lock)

Phase 8 Phase B / D8.4d status

Lane Owner Status
#76 D8.4a transport huangheng merged 63a9d522
#77 D8.4b renderer huangheng merged f0351662
#78 D8.4c consent/elicit body chenyexuan in_review (chained on #77)
#90 D8.4d snapshot endpoint Bryce this PR

Per architect canonical: this PR retires snapshot-fallback.ts (the FE transitional adapter) end-to-end, keeping the remaining mapBackendTurnStatus helper. After this lands, the only remaining D8.x backend cleanup is D8.6 / #80 (drop legacy agent_artifact / agent_timeline_event tables once the wire emitter starts populating agent_message.parts directly).

🤖 Generated with Claude Code

earayu and others added 5 commits April 26, 2026 00:10
… parts

Per architect msg=711f8c2f canonical lock + PM scope (msg=383c2e2b /
msg=247f4d8e): the agent runtime turn snapshot endpoint
(`GET /api/v2/agent/chats/{cid}/turns/{tid}`) is migrated from the
legacy `{turn, timeline, artifacts}` envelope to the canonical
`UIMessage`-aligned `AgentTurnSnapshot` shape, so the FE renderer
(#76 / #77 / #78) consumes the same `UIMessagePart` discriminated
union from both the live SSE stream and the at-rest reload path
(D8 §2 wire / at-rest byte-equal).

Backend changes
- New `aperag/domains/agent_runtime/snapshot_assembler.py` projects
  legacy `AgentArtifact` rows into `UIMessagePart[]`:
  * `answer` → single `TextPart`
  * `reference_bundle` → N × `SourceUrlPart` + N × `DataCitationPart`
  * `error_summary` → not a part; surfaced via `error_text`
  * `tool_result_summary` / `search_result_summary` → skipped
  Mirrors the FE-side `snapshot-fallback.ts` adapter that #77
  huangheng landed as a transitional bridge so deletion was
  mechanical from this side.
- `AgentTurnSnapshot` (now defined in `uimessage.py` next to the
  rest of the UIMessage family; re-exported from `schemas.py` for
  back-compat) flips to `{schema_version, turn_id, chat_id, role,
  status, parts, error_text?, timeline_cursor, ...timestamps}`.
- `TurnService.get_turn_snapshot()` rewritten:
  1. Forward-compat: try `UIMessageStore.read(turn_id)` (D8.6
     populates `agent_message.parts` directly; today the store is
     optional and reads return None).
  2. Fallback: `assemble_parts_from_artifacts` projects legacy
     artifacts.
  3. `extract_error_text` pulls the `error_summary` artifact's
     payload message (or summary) for FAILED / CANCELLED turns,
     falling back to `runtime_state.error_message`.
- The 3 ownership-only callers (cancel / consent / elicit) of
  `get_turn_snapshot` are unaffected — they only use the call to
  trigger `ResourceNotFoundException`, never read the body.

Frontend changes (deletes the #77 transitional adapter)
- `web/src/features/agent-runtime/api.ts`:
  `AgentTurnSnapshotEnvelope` flips to the new flat shape with
  `parts: AgentMessagePart[]`. Old `{turn, timeline, artifacts}`
  fields are gone.
- `web/src/components/chat/chat-messages.tsx`:
  `seedFromSnapshot` synthesizes a minimal `AgentTurnEnvelope` from
  the new flat snapshot for the live-turn store; reload-path
  rendering reads `baselineSnapshot.parts` and
  `baselineSnapshot.error_text` directly without any client-side
  synthesis.
- `web/src/features/agent-runtime/snapshot-fallback.ts`:
  `synthesizePartsFromSnapshot` and `extractErrorTextFromSnapshot`
  are removed (their TODO(#90) trigger has fired).
  `mapBackendTurnStatus` and `isTerminalBackendStatus` are kept --
  status mapping is still useful even after the schema flip.
- `web/src/features/agent-runtime/index.ts`: the dropped exports
  are removed from the barrel.

Tests
- `tests/unit_test/agent_runtime/test_snapshot_assembler.py` (NEW,
  8 tests) pins the artifact → UIMessagePart projection: answer
  text, reference-bundle fan-out (with and without uri), ordering,
  unknown-artifact skip, error-text extraction (payload preferred,
  summary fallback, none-without-error_summary).
- `tests/unit_test/agent_runtime/test_agent_runtime_v3.py` snapshot
  tests rewritten for the new shape:
  * `test_turn_snapshot_returns_canonical_uimessage_parts_for_completed_turn`
  * `test_turn_snapshot_surfaces_error_text_for_failed_turn`
  * `test_turn_snapshot_does_not_expose_legacy_keys` (regression
    guard: `{turn, timeline, artifacts}` must not reappear)
  * `test_turn_snapshot_user_activity_inference_runs_via_event_service`
    pins the empty-timeline guarantee on the new shape.
- `tests/e2e_http/hurl/full/15_agent_runtime_v3.hurl` snapshot
  assertions migrated to the new shape (`turn_id`, `chat_id`,
  `role`, `status`, `parts` at top level; legacy keys gone).
- The OpenAPI contract test at
  `tests/unit_test/agent_runtime/test_agent_runtime_openapi_contract.py`
  continues to pass — the schema name is unchanged
  (`AgentTurnSnapshot`); only the fields differ.

Gates
- `pytest tests/unit_test/agent_runtime/ -q` → 134 passed
- `pytest tests/unit_test --deselect concurrent_control flake -q`
  → 831 passed / 29 skipped / 0 failed
- `ruff check` + `ruff format --check` → clean

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nAI completion

The CI e2e-http-provider failure on PR #1705 head c16d131 surfaced
two production callers of ``TurnService.get_turn_snapshot()`` that
were still accessing the legacy ``snapshot.artifacts`` /
``snapshot.turn.answer_artifact_id`` shape. Both extract artifact
data for purposes independent of the FE-facing UIMessage protocol
(eval answer-text capture and OpenAI-compat completion content), so
they switch to ``db_ops.query_agent_artifacts_by_turn(turn_id)``
directly rather than reconstructing artifact data from the new
``UIMessagePart[]``.

- ``aperag/domains/evaluation/worker.py``:
  ``_extract_answer_text`` now takes a raw artifact list; the call
  site fetches artifacts via ``db_ops.query_agent_artifacts_by_turn``
  before invoking it.
- ``aperag/domains/conversation/service/chat_completion_service.py``:
  ``_build_completion_content`` rewritten to take the same raw
  artifact list; the OpenAI-compat completion path keeps its
  artifact-shaped logic (independent of FE protocol).
- ``tests/unit_test/chat/test_chat_completion_service.py``:
  ``_FakeTurnService`` swaps the old ``snapshot=`` parameter for
  ``artifacts=`` and exposes ``query_agent_artifacts_by_turn`` on
  its ``db_ops`` mock; the helper ``_snapshot()`` becomes
  ``_artifacts()`` returning the raw list.

Gates: full unit suite 831 passed / 29 skipped / 0 failed; ruff
check + format clean. CI e2e-http-provider should now pass since
the only ``AttributeError: 'AgentTurnSnapshot' object has no
attribute 'artifacts'`` raise was from these two paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…assertions

CI surfaced a second hurl file my inventory missed:
``tests/e2e_http/hurl/full/17_chat_collection_flow.hurl`` had the
same snapshot-endpoint shape assertions
(``$.turn.*`` / ``$.timeline`` / ``$.artifacts``) the previous commit
swept out of ``15_agent_runtime_v3.hurl``. Migrating to the new flat
shape in the same way: top-level ``turn_id`` / ``chat_id`` / ``role``
/ ``status`` / ``parts``, with legacy ``timeline`` / ``artifacts``
gone. The POST create-turn assertions (``$.turn.*`` on the
``CreateTurnResponse`` envelope, line 159-166) are unchanged — that
endpoint is unaffected by D8.4d.

Same root cause as the previous fix commit: my Explore agent
inventory only listed ``15_agent_runtime_v3.hurl`` for the snapshot
endpoint hit; broader grep would have caught this one too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nonical parts

CI rerun on 2f20d0d surfaced a bash script my inventory + grep both
missed: ``tests/e2e_http/scripts/run_chat_collection_flow.sh``. It
polls the snapshot endpoint to verify a completed turn:

* ``.turn.status`` → ``.status`` (top-level on new shape)
* ``.turn.answer_artifact_id`` / ``.turn.reference_bundle_artifact_id``
  → derive from ``.parts`` directly (TextPart text + DataCitationPart
  count) instead of fetching the legacy artifact endpoint twice.
* ``Timed out waiting for turn completion artifacts`` → ``parts``

The post-completion assertions now read off the snapshot parts
themselves: answer text non-empty (concatenation of all TextPart
``text`` fields) and at least one ``data-citation`` part. The
legacy ``/api/v2/agent/artifacts/{id}`` round-trip is removed —
post-#90 the FE-facing canonical does not expose artifact IDs from
the snapshot, and the script's intent (verify completion + non-empty
answer + references) is preserved with strictly fewer round-trips.

The POST create-turn response (line 338) keeps using ``.turn.turn_id``
because ``CreateTurnResponse`` is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…on-flow script

CI rerun on c42ae44 reached the snapshot polling step, the turn
COMPLETED with a non-empty answer (a clarification reply: "Which
collection would you like me to search in?"), and my too-strict
assertion ``reference_count > 0`` failed.

Pre-#90 script semantics: answer artifact required, reference bundle
artifact optional (the runtime only emits a reference_bundle when the
agent's reply actually cites sources). My first-cut migration kept
the answer-required side but tightened references from optional to
required, breaking the no-citation reply case.

This commit reverts the assertion budget to match the pre-#90 contract
exactly: answer non-empty is required; reference count is logged for
visibility but does not fail the script.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@earayu earayu merged commit 3f9303c into main Apr 25, 2026
4 checks passed
@earayu earayu deleted the bryce/phase8-task90-d84d-snapshot-uimessage-parts branch April 25, 2026 17:21
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