Skip to content

feat(phase8 #80 D8.6 chunk-2): wire UIMessageStore.write live + drop snapshot_assembler / agent_artifact#1723

Merged
earayu merged 1 commit into
mainfrom
ming-shu/d80-chunk2-uimessage-write
Apr 26, 2026
Merged

feat(phase8 #80 D8.6 chunk-2): wire UIMessageStore.write live + drop snapshot_assembler / agent_artifact#1723
earayu merged 1 commit into
mainfrom
ming-shu/d80-chunk2-uimessage-write

Conversation

@earayu

@earayu earayu commented Apr 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Task #80 D8.6 hard-cut chunk-2 per PM Option 2 lock (msg=d916b44a) — A+B in one PR:

  • A Read-side artifact-fallback removal: delete snapshot_assembler.py + tests, rip out assemble_parts_from_artifacts / extract_error_text from services.py:get_turn_snapshot and chat_service:_build_v3_chat_history.
  • B Write-side live wire-in: build UIMessageStore.write into the agent runtime end-of-turn (compose UIMessage(parts=[TextPart, *(SourceUrlPart+DataCitationPart per ref)])), delete ArtifactService / agent_artifact table / /agent/artifacts/{id} route / AgentArtifactEnvelope / AgentArtifactType and migrate 4 readers (HistoryWriter, chat_completion_service, evaluation/worker, snapshot endpoint).

agent_timeline_event deletion is chunk-3 (out of scope per PM lock).

Diff

 16 files changed, 472 insertions(+), 968 deletions(-)
 -site-net delete: 496 LOC

PM 3 review-face gates (msg=d916b44a + msg=c84e9fa4)

1. UIMessageStore.write is wired into live runtime (not test-only)

  • aperag/domains/agent_runtime/runtime.py:436+: end-of-turn now calls await self.uimessage_store.write(turn_id=..., chat_id=..., message=UIMessage(...)) with parts composed by _compose_assistant_parts(turn_id, answer_text, references).
  • AgentRuntimeTaskManager.__init__ constructs the singleton via UIMessageDbOps(session_factory=get_async_session) + AgentRuntimeRedisStore(); same instance is passed to TurnService, HistoryWriter, and PydanticAIRuntime.
  • runtime_manager.uimessage_store is exposed as a public attribute so chat_completion_service and evaluation/worker can read the canonical store directly.

2. Read path still complete after artifact-fallback delete

  • Snapshot endpoint (services.py:get_turn_snapshot) reads await self.uimessage_store.read(turn_id) — answer text + citations come back as TextPart + SourceUrlPart + DataCitationPart (D8 §2 byte-equal). FAILED error_text falls back to the AgentTurn row's error_message.
  • Chat history endpoint (chat_service.py:_build_v3_chat_history) reads canonical UIMessage per-turn, lazy-builds the store with the same factory pattern as chat_title_service (chunk-1).
  • OpenAI-compat completion (chat_completion_service._build_completion_content) projects TextPart + DataCitationPart into the existing answer + DOC_QA_REFERENCES + json envelope shape.
  • Evaluation worker reads the same canonical store.

3. Write set strictly disjoint from chunk-3

  • Zero touch to agent_timeline_event / EventService.append_event / redis_store.append_event / get_events_after. SSE streaming + reload mechanism unchanged.
  • All 18 emit() call-sites in runtime.py retained (turn lifecycle + tool lifecycle + state.changed + text.delta).

Caller sweep

Full grep across aperag/ + tests/ for AgentArtifact* / artifact_service / create_artifact / query_agent_artifact* / snapshot_assembler / extract_error_text / assemble_parts_from_artifacts / answer_artifact_id / reference_bundle_artifact_id:

  • Zero remaining Python call-sites in production code or tests.
  • Remaining hits are docstring narratives explaining the removal + 1 bash comment in tests/e2e_http/scripts/run_chat_collection_flow.sh:164 (historical context).

Migration

New alembic head d8e6c2b4f1a9 (revises c8f2d34a51e7):

def upgrade():
    drop_index("ix_agent_turn_reference_bundle_artifact_id", "agent_turn")
    drop_index("ix_agent_turn_answer_artifact_id", "agent_turn")
    drop_column("agent_turn", "reference_bundle_artifact_id")
    drop_column("agent_turn", "answer_artifact_id")
    drop_index("ix_agent_artifact_turn_id", "agent_artifact")
    drop_index("ix_agent_artifact_artifact_type", "agent_artifact")
    drop_index("idx_agent_artifact_turn_type", "agent_artifact")
    drop_table("agent_artifact")

Downgrade reconstructs the table + columns + indices for rollback symmetry.

Tests

  • Rewrote test_agent_runtime_v3.py (3 snapshot tests + history writer test + view test) using _FakeUIMessageStore.
  • Rewrote test_chat_service.py (3 history tests) using _FakeUIMessageStore.
  • Rewrote test_chat_completion_service.py (3 OpenAI-compat tests) using _FakeUIMessageStore.
  • Rewrote test_agent_runtime_openapi_contract.py to assert /agent/artifacts/{id} route + AgentArtifactEnvelope schema are absent from FastAPI OpenAPI spec.
  • Added test_agent_runtime_views_no_artifact_route regression guard.
  • Full unit suite: 989 passed, 29 skipped, ruff + format clean.

Out of scope (chunk-3, separate PR)

  • agent_timeline_event table removal
  • Replay/reload semantic change (Redis-only vs UIMessage replay)
  • EventService.append_event DB write removal

Test plan

  • uv run pytest tests/unit_test/ -q — 989 passed
  • uvx ruff check + format — clean
  • Caller sweep for legacy artifact symbols → zero Python call-sites
  • Migration upgrade/downgrade symmetry verified

…snapshot_assembler / agent_artifact

Phase 8 D8.6 chunk-2 hard-cut Option 2 (PM lock msg=d916b44a): A+B in
one PR — read-side artifact-fallback removal + write-side
``UIMessageStore.write`` live wire-in. Pre-launch system has no users
/ no data, so destructive deletion replaces migration.

A — Read-side artifact-fallback removal
---------------------------------------
- Delete ``aperag/domains/agent_runtime/snapshot_assembler.py`` and
  its test.
- ``services.py:get_turn_snapshot`` reads ``UIMessageStore.read``
  only — no more ``query_agent_artifacts_by_turn`` fallback. FAILED
  / CANCELLED ``error_text`` falls back to the ``AgentTurn`` row's
  ``error_message``.
- ``chat_service.py:_build_v3_chat_history`` switches to per-turn
  ``UIMessageStore.read`` (lazy singleton, mirrors chunk-1's
  ``ChatTitleService`` pattern).

B — Write-side ``UIMessageStore.write`` live wire-in
----------------------------------------------------
- ``runtime.py`` end-of-turn now composes a single canonical
  ``UIMessage`` (``TextPart`` + ``SourceUrlPart`` /
  ``DataCitationPart`` per reference, mirroring the FE-bound
  shape) and persists via ``uimessage_store.write``. Replaces both
  ``artifact_service.create_artifact(ANSWER, ...)`` and
  ``create_artifact(REFERENCE_BUNDLE, ...)``.
- FAILED path drops the ``error_summary`` artifact write — the
  error surface stays on the ``AgentTurn`` row, which the snapshot
  endpoint already reads.
- ``mark_completed`` loses the ``answer_artifact_id`` /
  ``reference_bundle_artifact_id`` parameters; the runtime-state
  Redis merge no longer carries those fields.
- ``HistoryWriter.build_history_context`` reads canonical
  ``UIMessage`` text parts per-turn; legacy artifact lookup is
  gone.
- Reader migration:
  * ``ChatCompletionService._build_completion_content`` accepts a
    parts list and projects the OpenAI-compat
    ``answer + DOC_QA_REFERENCES + json`` envelope from
    ``TextPart`` + ``DataCitationPart``.
  * ``evaluation/worker._extract_answer_text`` joins ``TextPart``
    text from the persisted message.

Deletions
---------
- ``ArtifactService`` class (no production callers after wire-in).
- ``/api/v2/agent/artifacts/{artifact_id}`` route +
  ``get_artifact_view``.
- ``AgentArtifact`` SQLAlchemy model + ``AgentArtifactType`` enum +
  ``AgentArtifactEnvelope`` schema (incl. its ``__all__`` export).
- ``AgentTurn.answer_artifact_id`` / ``reference_bundle_artifact_id``
  columns + their indices.
- ``AgentTurnEnvelope`` artifact_id fields (no remaining consumer).
- ``db_ops`` methods: ``create_agent_artifact`` /
  ``query_agent_artifact`` / ``query_agent_artifacts_by_turn``.
- Helper ``_extract_answer_text_from_artifact`` in services.py.

Migration
---------
- New alembic head ``d8e6c2b4f1a9`` (revises ``c8f2d34a51e7``):
  drops the ``agent_artifact`` table + its three indices and the
  two ``agent_turn`` artifact_id columns + their indices. Downgrade
  reconstructs both for rollback symmetry.

Caller sweep (``AgentArtifact`` / ``artifact_service`` /
``snapshot_assembler`` / ``extract_error_text`` /
``assemble_parts_from_artifacts`` / ``answer_artifact_id`` /
``reference_bundle_artifact_id``): zero remaining Python call-sites
in ``aperag/`` or ``tests/`` (only docstring narratives describing
the removal + a single bash comment in
``tests/e2e_http/scripts/run_chat_collection_flow.sh``).

Tests
-----
- Rewrote ``test_agent_runtime_v3.py`` + ``test_chat_service.py`` +
  ``test_chat_completion_service.py`` to drive the new code paths
  via a minimal in-memory ``_FakeUIMessageStore``; dropped legacy
  ``_FakeArtifactService`` and ``query_agent_artifacts_by_turn``
  mocks.
- Added ``test_agent_runtime_views_no_artifact_route`` regression
  guard pinning the deletion of the ``/agent/artifacts/{id}`` route
  + ``get_artifact_view`` symbol.
- ``test_agent_runtime_openapi_contract.py`` now asserts the
  artifact route + ``AgentArtifactEnvelope`` schema are absent from
  the FastAPI OpenAPI spec.
- Full unit suite: 989 passed, 29 skipped, ruff + format clean.

Out of scope (chunk-3 per PM Option 2 lock)
-------------------------------------------
- ``agent_timeline_event`` table removal + replay/reload semantic
  change. Wire-emitter event sourcing stays as-is in this PR.
@earayu earayu merged commit 884aa93 into main Apr 26, 2026
4 checks passed
@earayu earayu deleted the ming-shu/d80-chunk2-uimessage-write branch April 26, 2026 02:32
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