feat(phase8 #90 D8.4d): snapshot endpoint returns canonical UIMessage parts#1705
Merged
Merged
Conversation
… 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>
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
Phase 8 task #90 (D8.4d) — replaces the agent runtime turn snapshot endpoint's legacy
{turn, timeline, artifacts}envelope with the canonicalUIMessage-alignedAgentTurnSnapshotshape. The FE renderer (#76 / #77 / #78) now consumes the sameUIMessagePartdiscriminated 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
aperag/domains/agent_runtime/snapshot_assembler.py— projects legacyAgentArtifactrows intoUIMessagePart[](answer →TextPart, reference_bundle →SourceUrlPart+DataCitationPart, error_summary →error_text). Mirrors the FE-sidesnapshot-fallback.tsadapter that feat: support default collections #77 huangheng landed as a transitional bridge so deletion was mechanical from both sides.AgentTurnSnapshot(now defined inuimessage.pyalongside the rest of the UIMessage family; re-exported fromschemas.pyfor back-compat) flips to{schema_version, turn_id, chat_id, role, status, parts, error_text?, timeline_cursor, ...timestamps}.TurnService.get_turn_snapshot()rewritten:UIMessageStore.read(turn_id)first (D8.6 will populateagent_message.partsdirectly via the wire emitter; today the store is optional and reads returnNone).assemble_parts_from_artifactsprojects legacy artifacts.extract_error_textpulls theerror_summarypayload (or summary), falling back toruntime_state.error_message.get_turn_snapshot(cancel / consent / elicit endpoints) are unaffected — they only use the call to triggerResourceNotFoundException, never read the body.Frontend (deletes the #77 transitional adapter)
web/src/features/agent-runtime/api.ts—AgentTurnSnapshotEnvelopeflips to the new flat shape; legacy{turn, timeline, artifacts}types gone.web/src/components/chat/chat-messages.tsx—seedFromSnapshotsynthesizes a minimalAgentTurnEnvelopefrom the new flat snapshot for the live-turn store; reload rendering readsbaselineSnapshot.partsandbaselineSnapshot.error_textdirectly with no client-side synthesis.web/src/features/agent-runtime/snapshot-fallback.ts—synthesizePartsFromSnapshotandextractErrorTextFromSnapshotare removed (theirTODO(#90)trigger has fired).mapBackendTurnStatus/isTerminalBackendStatusare kept (status mapping stays useful).web/src/features/agent-runtime/index.ts— dropped exports removed from the barrel.Boundary discipline
{turn, timeline, artifacts}keys retiredsnapshot-fallback.tsadapters retired)legacy-snapshot-shim.tswas already deleted by feat: support default collections #77Tests
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.pysnapshot tests rewritten for the new shape:test_turn_snapshot_returns_canonical_uimessage_parts_for_completed_turntest_turn_snapshot_surfaces_error_text_for_failed_turntest_turn_snapshot_does_not_expose_legacy_keys(regression-guard:{turn, timeline, artifacts}must not reappear)test_turn_snapshot_user_activity_inference_runs_via_event_servicepins the empty-timeline guarantee.tests/e2e_http/hurl/full/15_agent_runtime_v3.hurlsnapshot assertions migrated to the new shape (turn_id,chat_id,role,status,partsat 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/ -q→ 134 passedpytest tests/unit_test --deselect concurrent_control flake -q→ 831 passed / 29 skipped / 0 failedruff check→ cleanruff format --check→ clean (446 files already formatted)web/src/api-v2/schema.d.ts) regen viayarn api:v2:types— yarn is not available in this worktree; CI / a follow-up FE-touching PR will resync if a drift is observed.Phase 8 Phase B / D8.4d status
63a9d522f0351662Per architect canonical: this PR retires
snapshot-fallback.ts(the FE transitional adapter) end-to-end, keeping the remainingmapBackendTurnStatushelper. After this lands, the only remaining D8.x backend cleanup is D8.6 / #80 (drop legacyagent_artifact/agent_timeline_eventtables once the wire emitter starts populatingagent_message.partsdirectly).🤖 Generated with Claude Code