#region AgentChat.Tasks [C:3] [TYPE ADR] [SEMANTICS tasks,implementation,agent-chat]
@BRIEF Implementation tasks for Gradio Agent Chat — phased by user story, dependency-ordered, with contract inlining for C3+ functions.
## Phase 1: Setup (shared infrastructure)
- [ ] T001 [P] Add `gradio>=5.0`, `langgraph>=0.2`, `langchain-core>=0.3`, `langchain-openai>=0.3`, `langgraph-checkpoint-postgres`, `pdfplumber` to `backend/requirements.txt`
- [ ] T002 [P] Add `@gradio/client` to `frontend/package.json` → `npm install`
- [ ] T003 [P] Create `backend/src/agent/` directory with `__init__.py`
- [ ] T004 [P] Create `backend/tests/test_agent/` directory with `__init__.py`
- [ ] T005 [P] Create `frontend/src/routes/agent/` directory
- [ ] T006 [P] Create `frontend/src/types/agent.ts` — TypeScript DTOs for conversation/message/stream metadata. No `agent-ws.ts`.
- [ ] T007 Create `docker/Dockerfile.agent` — Python 3.11-slim, installs gradio/langchain/langchain-openai/pdfplumber from requirements. No shared volume mount needed — Gradio calls FastAPI tools via HTTP.
RATIONALE Tool execution via REST (FR-004 revised) — Gradio container has no DB connection
- [ ] T008 Update `docker/docker-compose.yml` — add `ss-tools-agent` service (port 7860 internal, NOT exposed to host). Env vars: LLM_API_KEY, LLM_BASE_URL, LLM_MODEL, FASTAPI_URL=http://ss-tools-api:8000, SERVICE_JWT, JWT_SECRET (shared with FastAPI for stateless JWT validation).
- [ ] T009 Update `docker/nginx.conf` — add reverse proxy location `/api/agent/gradio` → `http://ss-tools-agent:7860`. Forward `Authorization` header from browser request.
RATIONALE Browser cannot resolve Docker service names; Gradio must be proxied through nginx
## Phase 2: Foundational (data layer — blocking for all stories)
- [ ] T010 Implement `backend/src/models/agent.py` — SQLAlchemy models `AgentConversation` (id, user_id FK→users.id, title, is_archived, created_at, updated_at) and `AgentMessage` (id, conversation_id FK→agent_conversations.id, role, text, state, tool_calls JSON, attachments JSON, created_at). Use `uuid4_str` for primary keys.
@DATA_CONTRACT Models.Agent — see data-model.md §2
- [ ] T011 Implement `backend/src/schemas/agent.py` — Pydantic schemas: `ConversationItem`, `ConversationListResponse`, `MessageItem`, `HistoryResponse`, `DeleteResponse`. Match `data-model.md` §3 exactly.
@DATA_CONTRACT Schemas.Agent
- [ ] T012 [P] Implement `frontend/src/types/agent.ts` — TypeScript interfaces: `ConversationItem`, `ConversationListResponse`, `MessageItem`, `ToolCall`, `AttachmentMeta`, `HistoryResponse`, `DeleteResponse`. MUST match `backend/src/schemas/agent.py` field-for-field.
@DATA_CONTRACT AgentTypes ↔ Schemas.Agent (cross-stack)
- [ ] T013 [P] Implement `frontend/src/types/agent.ts` — TypeScript interfaces for conversation/message DTOs matching `backend/src/schemas/agent.py`. `agent-ws.ts` NOT created — no custom WebSocket protocol.
- [ ] T014 Generate Alembic migration for `agent_conversations` and `agent_messages` tables → `cd backend && source .venv/bin/activate && alembic revision --autogenerate -m "add agent conversations"`
## Phase 3: Story 1 — Gradio Agent Engine with Streaming Chat (P1) 🎯 MVP
### Backend
- [ ] T015 [US1] Implement `backend/src/agent/app.py` — Gradio `gr.ChatInterface(fn=agent_handler, type="messages", multimodal=True, additional_inputs=[gr.Textbox("conversation_id"), gr.Textbox("action")], examples=[["Покажи дашборды", null, null], ["Статус системы", null, null]], concurrency_limit=10)`. Handler: `agent_handler(message, history, request: gr.Request, conversation_id=None, action=None)`. On `action="confirm"/"deny"`: resume graph from checkpoint. Per-user lock via in-memory dict keyed by user_id.
- [ ] T016 [US1] Implement `backend/src/agent/langgraph_setup.py` — `create_react_agent(model, tools, checkpointer=PostgresSaver(os.getenv("DATABASE_URL")))`. Define dangerous tools in `DANGEROUS_TOOLS` set. Compile graph with `interrupt_before=DANGEROUS_TOOLS`. Wrap with `RunnableWithMessageHistory` for auto history load/save (thread_id = conversation_id).
@POST Compiled StateGraph with checkpointer, interrupt_before, message history
RATIONALE langgraph provides native interrupt/resume; no custom HITL middleware needed
- [ ] T017 [US1] Implement agent_handler yield loop in `app.py` — `async for event in graph.astream_events({"messages": [HumanMessage(content=message["text"])]}, config={"configurable": {"thread_id": conversation_id}}): yield to_chatmessage(event)`. Tool events → `ChatMessage(metadata={"type":"tool_start", "tool":..., "input":...})`. Interrupt events → `ChatMessage(metadata={"type":"confirm_required", "prompt":..., "thread_id":conversation_id})`.
@POST Each LangGraph event converted to ChatMessage with structured metadata; frontend receives typed objects
### Frontend
- [ ] T018 [US1] Adapt `AssistantChatPanel.svelte` — `Client.connect("/api/agent/gradio")`, `submit("/chat", message, [conversationId, null])`. Iterate `for await (event)`: `event.data` is `ChatMessage` with structured `metadata`. Render tokens, tool cards, confirmation cards based on `metadata.type`.
@UX_STATE idle, streaming, awaiting_confirmation, error, disconnected
- [ ] T019 [US1] Implement `AgentChatModel.sendMessage()` — `client.submit("/chat", {text: message, files}, [conversationId, null])`, iterate events, append tokens. `streamingState`: idle→streaming.
- [ ] T020 [US1] Implement `AgentChatModel.cancelGeneration()` — `submission.cancel()`.
- [ ] T021 [US1] Implement metadata handler in `AgentChatModel._onStreamData(msg)` — switch on `msg.metadata.type`: stream_token→append, tool_start→card+spinner, tool_end→checkmark, tool_error→cross, confirm_required→confirmation card, confirm_resolved→collapse card, error→error card.
- [ ] T022 [US1] Implement Gradio connection lifecycle — `Client.connect()`, retry on disconnect (5×5s), `connectionState` atom.
### Verification
- [ ] T024 [US1] Write `backend/tests/test_agent/test_agent_handler.py` — test Gradio handler: empty message returns immediately, streaming yields tokens, cancel stops generator, LLM unavailable yields error event.
- [ ] T025 [US1] Write `frontend/src/lib/models/__tests__/AgentChatModel.test.ts` — L1 model tests (no DOM render): sendMessage transitions state, cancelGeneration resets, disconnect/reconnect state machine, invalid state transitions rejected.
- [ ] T026 [US1] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/ -v`
- [ ] T027 [US1] Verify: `cd frontend && npm run test -- --reporter=verbose`
- [ ] T028 [US1] Verify UX against `ux_reference.md`: streaming tokens appear in real time, Stop button halts generation, connection dot green/red/yellow.
---
## Phase 4: Story 2 — Autonomous Tool Selection via Agent (P1) 🎯 MVP
### Backend
- [ ] T026 [US2] Implement `backend/src/agent/tools.py` — native LangChain `@tool`-декорированные функции. Каждый `@tool`: Pydantic `BaseModel` для args_schema, docstring для description, внутри → HTTP-вызов к FastAPI REST с service JWT. Пример: `@tool async def search_dashboards(query: str, environment: str = None) → str`. Список тулов: все существующие @assistant_tool операции.
@DATA_CONTRACT Each @tool: Pydantic args → FastAPI REST call → JSON string result
RATIONALE Native @tool replaces dual @assistant_tool + StructuredTool wrapping
- [ ] T027 [US2] Implement `backend/src/agent/middleware.py` — `LoggingMiddleware` (логирует tool-call события в audit trail), `ConfirmationRiskMiddleware` (определяет interrupt_on список из конфига TOOL_RISK_LEVELS).
- [ ] T028 [US2] Register tools in `langgraph_setup.py` — `DANGEROUS_TOOLS = {"deploy_dashboard", "execute_migration", "commit_changes"}`. Compile graph with `interrupt_before=DANGEROUS_TOOLS`. All tools from `tools.py` registered as graph nodes.
@POST Agent with tools, interrupt, logging, checkpoints, history — all LangChain-native
- [ ] T029 [US2] Implement interrupt handler in `app.py` — when graph pauses at `interrupt_before` node: yield `ChatMessage(metadata={"type":"confirm_required", ...})`. On second submit with `additional_inputs[1]="confirm"/"deny"`: invoke graph with `Command(resume={"action": action}, config={"thread_id": conversation_id})`. Graph resumes from checkpoint.
- [ ] T031 [US2] Implement confirmation rendering in `AssistantChatPanel.svelte` — on `metadata.type="confirm_required"`: warning card + Confirm/Deny. Confirm → `submit("/chat", {text:"confirm"}, [conversationId, "confirm"])`. Deny → `submit("/chat", {text:"deny"}, [conversationId, "deny"])`. On `metadata.type="confirm_resolved"`: collapse card.
### Verification
- [ ] T034 [US2] Write `backend/tests/test_agent/test_langchain_tools.py` — test: REST-calling tools wrap correctly, agent selects tool, agent handles tool failure gracefully.
- [ ] T035 [US2] Write `backend/tests/test_agent/test_confirmations.py` — test: confirmation created, confirmed, denied, expired flows.
- [ ] T036 [US2] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/ -v -k "tool or confirm"`
- [ ] T037 [US2] Verify UX: confirmation card appears for dangerous ops, confirm via second `submit()` with `__resume__`, deny yields `⏹️`.
---
## Phase 5: Story 3 — Tool-Call Visibility in Chat (P2)
### Frontend
- [ ] T039 [US3] Implement `frontend/src/lib/components/assistant/ToolCallCard.svelte` — inline card within assistant message. Props: `{tool, input, output?, error?, status}`. Visual: tool name in mono font, spinner (executing)/checkmark (done)/cross (error). Expandable to show input params and output result.
@UX_STATE executing (spinner), completed (checkmark, expandable result), failed (cross, error detail)
@RELATION DEPENDS_ON -> $lib/ui/Icon (existing)
@RELATION DEPENDS_ON -> $lib/ui/Button variant="ghost" (existing, for expand/collapse)
Design tokens: card=`bg-surface-muted border border-border rounded-lg`, tool name=`text-text-muted text-xs font-mono`, spinner=`animate-spin text-primary`, checkmark=`text-success`, cross=`text-destructive`
- [ ] T040 [US3] Integrate ToolCallCard into `AssistantChatPanel.svelte` — render ToolCallCard when `metadata.type="tool_start"/"tool_end"/"tool_error"`. On tool_start: card+spinner. On tool_end: checkmark+result. On tool_error: cross+error.
- [ ] T041 [US3] Implement metadata dispatch in `AgentChatModel._onStreamData(msg)` — handle `tool_start` (push card), `tool_end` (update status), `tool_error` (update status). Update `activeToolCalls[]`.
### Verification
- [ ] T042 [US3] Write `frontend/src/lib/components/assistant/__tests__/ToolCallCard.test.ts` — test: renders tool name, shows spinner in executing state, shows checkmark on completed, shows cross on error, expand/collapse toggles detail visibility.
- [ ] T043 [US3] Verify: `cd frontend && npm run test -- --reporter=verbose`
- [ ] T044 [US3] Verify UX: multi-tool query → each tool card appears inline, can be expanded, shows real-time status.
---
## Phase 6: Story 4 — Multi-Turn Conversation Context (P2)
### Backend
- [ ] T045 [US4] Verify `RunnableWithMessageHistory` auto-loads context from PostgreSQL per `thread_id=conversation_id`. No manual `history` injection needed.
### Frontend
- [ ] T047 [US4] Implement `AgentChatModel.createConversation()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — clear messages, reset streamingState→idle, set currentConversationId=null. Existing conversation is NOT lost (persisted in DB).
@ACTION createConversation(): starts clean conversation
@POST messages=[], streamingState="idle", partialText=""
@TEST_EDGE: create_during_streaming→cancel_first_then_create
### Verification
- [ ] T048 [US4] Write integration test: send message 1 referencing entity, send message 2 with pronoun → agent resolves pronoun from context.
- [ ] T049 [US4] Write test: conversation exceeds 4000 tokens → older messages summarized, recent detail preserved.
- [ ] T050 [US4] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/ -v -k "memory or context"`
- [ ] T051 [US4] Verify UX: follow-up question resolves prior context, new conversation starts clean.
---
## Phase 7: Story 5 — Conversation Persistence and Management (P3)
### Backend
- [ ] T052 [US5] Implement `backend/src/api/routes/agent_conversations.py` — FastAPI router: `POST /api/assistant/conversations` (create), `GET /api/assistant/conversations` (paginated list + search), `GET /api/assistant/history` (paginated messages), `POST /api/assistant/conversations/{id}/messages` (append batch after stream_end), `DELETE /api/assistant/conversations/{id}` (soft-delete: is_archived=true).
- [ ] T053 [US5] Implement `POST /api/assistant/conversations`:
@PRE User authenticated, title from first message (truncated 60 chars)
@POST ConversationItem(id, title, created_at) — HTTP 201
@TEST_EDGE: unauthenticated→401
@PRE User authenticated (via existing FastAPI auth dependency)
@POST ConversationListResponse(items[], has_next, active_total, archived_total)
@DATA_CONTRACT Input: (page, page_size, include_archived?, search?) → Output: ConversationListResponse
@TEST_EDGE: empty_result→items=[], totals=0, no_error — filter_invalid→ignored
- [ ] T054 [US5] Implement `AgentChat.Api.GetHistory` in `backend/src/api/routes/agent_conversations.py`:
@PRE conversation_id valid
@POST HistoryResponse(items[], has_next, conversation_id)
@TEST_EDGE: invalid_conversation_id→404 — empty_conversation→items=[], no_error
- [ ] T055 [US5] Implement `AgentChat.Api.DeleteConversation` in `backend/src/api/routes/agent_conversations.py`:
@PRE conversation_id valid, user owns conversation
@POST DeleteResponse(deleted=true), conversation is_archived=true
@SIDE_EFFECT Sets is_archived=true, cascades to messages (soft delete)
@TEST_EDGE: already_deleted→404 — not_owner→403
- [ ] T056 [US5] Register agent_conversations router in `backend/src/app.py` — prefix `/api/assistant` (shares namespace with existing assistant routes for backward compat FR-020).
### Frontend
- [ ] T057 [US5] Implement `AgentChatModel.loadConversations()` and `loadHistory()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — call existing `getAssistantConversations()` and `getAssistantHistory()` from `frontend/src/lib/api/assistant.ts`. Infinite scroll for both: append on next page, reset on new filter.
@ACTION loadConversations(reset?): fetches from REST, manages pagination
@POST conversations array updated, conversationsHasNext flag set
@ACTION loadHistory(conversationId?): fetches messages from REST
@POST messages array populated, historyHasNext flag set
@TEST_EDGE: api_error→error state with retry, empty_response→empty state
- [ ] T058 [US5] Implement `AgentChatModel.deleteConversation()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — optimistic removal from conversations array, DELETE via REST, rollback on failure with toast.
@ACTION deleteConversation(id): optimistic delete
@POST removed from list, toast on success/error
@TEST_EDGE: api_failure→rollback_reinsert, active_conversation_deleted→switch_to_next
- [ ] T059 [US5] Implement `frontend/src/lib/components/assistant/ConversationList.svelte` — sidebar list (240px) with: `` for search (debounced 300ms), grouped by date on client (`$derived` from `conversations`), infinite scroll (IntersectionObserver), delete button (with `confirm()` before calling model.deleteConversation), active state highlight (`bg-surface-muted`). Skeleton on load.
@UX_STATE loading (skeleton), loaded (grouped list), empty ("Нет диалогов"), error (retry)
@RELATION DEPENDS_ON -> $lib/ui/Input (existing), $lib/ui/Button variant="ghost" (existing), $lib/ui/Icon (existing)
@RELATION BINDS_TO -> AgentChat.Model
Design tokens: sidebar=`bg-surface-card border-r border-border`, skeleton=`animate-pulse bg-surface-muted`
- [ ] T060 [US5] Integrate ConversationList into `AssistantChatPanel.svelte` (drawer header as dropdown) and into `/agent` page (left sidebar).
### Verification
- [ ] T061 [US5] Write `backend/tests/test_agent/test_conversation_api.py` — test: list returns paginated, search filters by title, history returns messages, delete archives conversation, 404 on invalid id, 403 on other user's conversation.
- [ ] T062 [US5] Write `frontend/src/lib/components/assistant/__tests__/ConversationList.test.ts` — test: renders conversations, groups by date, search filters, delete shows confirm and removes, error shows retry.
- [ ] T063 [US5] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/test_conversation_api.py -v`
- [ ] T064 [US5] Verify: `cd frontend && npm run test -- --reporter=verbose`
---
## Phase 8: Story 6 — File Upload and Document Analysis (P2)
### Backend
- [ ] T065 [US6] Implement `backend/src/agent/document_parser.py` — `parse_pdf(file_path)` using pdfplumber (primary, extracts text + tables), PyPDF2 (fallback for encrypted). `parse_xlsx(file_path)` using openpyxl (sheet names → 2D cell arrays).
@PRE File exists on disk, valid extension
@POST Returns extracted text (PDF) or structured dict (XLSX)
@TEST_EDGE: encrypted_pdf→ParseError with message, password_xlsx→ParseError, empty_file→empty_string (no crash), unsupported_extension→ParseError
- [ ] T066 [US6] Integrate document parser into agent_handler in `backend/src/agent/app.py` — before running agent, detect file attachments (Gradio `gr.File` component), parse content via T065, inject as system message: `"Вот содержимое файла {name}:\n{parsed_text}"`.
@PRE File uploaded via Gradio multimodal input
@POST Parsed content injected as system message in conversation
- [ ] T067 [US6] Add file size validation (10MB limit) in agent_handler — reject oversized files before parsing, return error message to user.
@PRE File attached to message
@POST File accepted if ≤10MB, rejected with error message if exceeded
### Frontend
- [ ] T068 [US6] Add file upload UI to `AssistantChatPanel.svelte` — paperclip icon button (using `$lib/ui/Icon name="paperclip"`), hidden ``, preview chip below input showing filename + size. Validate format and size before upload (10MB client-side check).
@UX_FEEDBACK file chip appears below input, invalid format→toast error, oversized→toast error
@RELATION DEPENDS_ON -> $lib/ui/Icon (existing)
- [ ] T069 [US6] Pass file attachments through `AgentChatModel.sendMessage()` — append file data to WebSocket message payload, show parsing indicator on file chip while agent processes.
@TEST_EDGE: upload_during_streaming→rejected, upload_empty_file→validation_error
### Verification
- [ ] T070 [US6] Write `backend/tests/test_agent/test_document_parser.py` — test: PDF extracts text, PDF with tables preserves structure, XLSX parses all sheets, encrypted/empty/invalid files handled gracefully.
- [ ] T071 [US6] Verify: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/test_document_parser.py -v`
- [ ] T072 [US6] Verify UX: upload PDF → agent analyzes content, upload XLSX → agent reads tabular data, oversized file → rejected.
---
## Phase 9: Polish & Cross-Cutting Verification
### Frontend
- [ ] T073 Implement `frontend/src/routes/agent/+page.svelte` — full-page agent chat at `/agent` route. Two-column layout: `` (240px left sidebar) + `` (right area). `` title="Агент-чат". Protected route (auth check in +layout.svelte).
@RELATION BINDS_TO -> AgentChat.Model (same instance)
@RELATION DEPENDS_ON -> $lib/ui/PageHeader (existing)
@RELATION DEPENDS_ON -> AgentChat.ConversationList
@RELATION DEPENDS_ON -> AgentChat.Panel
Design tokens: page=`bg-surface-page max-w-7xl mx-auto`, two-column=`flex`, sidebar=`w-60 shrink-0 bg-surface-card border-r border-border`, chat=`flex-1`
- [ ] T074 Implement `frontend/src/lib/components/assistant/ConnectionIndicator.svelte` — green/red dot binding to `model.connectionDotColor` `$derived`. Tooltip: "Подключено"/"Недоступно".
@RELATION BINDS_TO -> AgentChat.Model
Design tokens: green=`bg-success w-2 h-2 rounded-full`, red=`bg-destructive w-2 h-2 rounded-full`
- [ ] T075 Implement multi-tab gate in `AgentChatModel.sendMessage()` — before sending, call `POST /api/agent/conversations/{id}/active` to check for existing session. If active → reject with toast "Диалог активен в другой вкладке".
@TEST_EDGE: second_tab_send→rejected_with_toast, first_tab_send→proceeds
- [ ] T076 Add "Expand to /agent" button in drawer header — navigates to `/agent` route preserving current conversation context via store.
@UX_FEEDBACK smooth navigation, conversation state preserved
- [ ] T077 Implement conversation auto-title — on first agent response, set `AgentConversation.title` from truncated first user message (60 chars) during conversation creation in T052. No separate PATCH endpoint needed — title is set at insert time.
@POST Conversation.title populated at creation
### Backend
- [ ] T078 Add audit logging to agent_handler — every user message and agent response writes to existing `assistant_audit` table: `{user_id, conversation_id, decision: "message_sent"/"tool_called"/"confirmed"/"denied", message, payload}`.
@SIDE_EFFECT Writes to assistant_audit table
RATIONALE FR-010: all agent interactions logged for auditability
- [ ] T079 Implement Gradio healthcheck endpoint — `GET /health` returns `{"status":"ok","uptime":...}` for Docker healthcheck.
- [ ] T080 [P] Implement `backend/src/api/routes/auth.py` — internal endpoint `POST /api/auth/service-token` that accepts a static service secret (env `SERVICE_TOKEN_SECRET`) and returns a long-lived JWT with role `agent`. Used by Gradio container at startup for Gradio→FastAPI HTTP calls.
@DATA_CONTRACT Input: {service_secret} → Output: {access_token, expires_in}
@TEST_EDGE: invalid_secret→401, expired_token→401_on_next_call
RATIONALE FR-023: Gradio→FastAPI calls authenticated via JWT, not separate API key mechanism
- [ ] T081 [P] Add i18n keys to `frontend/src/lib/i18n/locales/ru/assistant.json` and `frontend/src/lib/i18n/locales/en/assistant.json` — new keys: `stop`, `tool_executing`, `tool_completed`, `tool_failed`, `confirm_required`, `confirm_expired`, `confirm_retry`, `file_upload`, `file_parse_error`, `file_unsupported`, `file_too_large`, `connection_lost`, `reconnecting`, `manual_reconnect`, `archive_dialog`.
RATIONALE FR-013: Russian + English i18n for all new UX strings (8+ in UX reference §3-4)
- [ ] T082 Implement LLM malformed JSON retry in `backend/src/agent/app.py` agent_handler — catch `OutputParserException` from LangChain, retry once with `"Respond with valid JSON only. Previous response was malformed."` appended to prompt. If still malformed, yield `stream_error` with clarification request.
@TEST_EDGE: first_retry_succeeds→normal_flow, second_failure→stream_error_with_clarification
RATIONALE Spec Edge Cases: "LLM returns malformed tool-call JSON → retry once with stricter prompt"
- [ ] T083 Implement rapid-fire message queue in `AgentChatModel.sendMessage()` in `frontend/src/lib/models/AgentChatModel.svelte.ts` — if `streamingState !== "idle"`, enqueue message; process queue sequentially when state returns to idle. Show queue position badge if >1 pending.
@TEST_EDGE: send_during_streaming→queued, send_during_sending→queued, queue_drains_on_idle
RATIONALE Spec Edge Cases: "Simultaneous rapid-fire messages → queue and process sequentially"
### Verification (cross-cutting)
- [ ] T084 [P] Backend full test suite: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/ -v`
- [ ] T085 [P] Frontend full test suite: `cd frontend && npm run test -- --reporter=verbose`
- [ ] T086 [P] Backend lint: `cd backend && python -m ruff check src/agent/ src/api/routes/agent_conversations.py src/models/agent.py src/schemas/agent.py`
- [ ] T087 [P] Frontend lint: `cd frontend && npm run lint`
- [ ] T088 [P] Frontend build: `cd frontend && npm run build`
- [ ] T089 Semantic audit — agent contracts: `axiom_semantic_validation audit_contracts file_path="backend/src/agent/"`
- [ ] T090 UX reference validation — verify all states from `contracts/ux/agent-chat-ux.md` (15 FSM states) are reachable and rendered correctly via browser or vitest @UX_TEST scenarios.
- [ ] T091 Rejected-path regression test — verify that `/api/assistant` REST endpoints still function (FR-020 backward compat). Existing `backend/tests/test_assistant_api.py` passes unmodified.
- [ ] T092 Verify ADR guardrails: `@REJECTED full Svelte replacement` → SvelteKit routes intact; `@REJECTED custom SSE/WebSocket` → @gradio/client used exclusively; `@REJECTED separate auth mechanism` → JWT reused.
---
## Task Summary
| Phase | Stories | Tasks | Parallel |
|-------|---------|:-----:|:--------:|
| P1 — Setup | — | T001-T009 | 6 |
| P2 — Foundational | — | T010-T014 | 2 |
| P3 — US1 (Streaming) | P1 🎯 | T015-T028 | — |
| P4 — US2 (Tools) | P1 🎯 | T029-T038 | — |
| P5 — US3 (Visibility) | P2 | T039-T044 | — |
| P6 — US4 (Context) | P2 | T045-T051 | — |
| P7 — US5 (Persistence) | P3 | T052-T064 | — |
| P8 — US6 (Files) | P2 | T065-T072 | — |
| P9 — Polish + Gaps | — | T073-T092 | 8 |
| **Total** | | **92** | **16** |
## Story Independence
| Story | Independent Test |
|-------|------------------|
| **US1** | Launch Gradio, connect from Svelte, send message → streaming tokens appear |
| **US2** | Send multi-tool query → agent selects correct tools autonomously |
| **US3** | Multi-tool query → tool-call cards visible inline, expandable |
| **US4** | Follow-up with pronoun → agent resolves from prior context |
| **US5** | 3 conversations, restart app → all 3 appear, resume works |
| **US6** | Upload PDF → agent extracts and analyzes content |
#endregion AgentChat.Tasks