#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"` - [ ] T014a [P] Create `backend/tests/test_agent/fixtures/` directory. Copy canonical JSON fixtures from `specs/033-gradio-agent-chat/fixtures/api/` and `fixtures/model/` as test data. Load fixtures via pytest `conftest.py` fixture loader. @TEST_FIXTURE: load_all -> all 14 fixtures load without parse errors ## 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=1)`. 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. RATIONALE concurrency_limit=1 per FR-015 — serializes sends per user, prevents concurrent stream conflicts - [ ] 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` — extract user JWT from `request.headers["Authorization"]`, set via `ContextVar` BEFORE graph invocation (see `backend/src/agent/context.py`). Then: `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; user JWT propagated via ContextVar @SIDE_EFFECT Sets ContextVar with user JWT before graph run, resets after - [ ] T017a [P] [US1] Create `backend/src/agent/context.py` — `ContextVar[str]` for `_user_jwt_context` and `_service_jwt_context`. Thread-safe JWT storage for @tool functions to access auth headers without explicit parameter passing. RATIONALE LangGraph tools cannot receive per-request auth via graph config — ContextVar bridges Gradio handler → tool execution ### 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 - [x] 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. - [x] 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. - [x] T028a [US1] Write `backend/tests/test_agent/test_model_fixtures.py` — materialize model fixtures: FX_AgentChat.Model.SendMessage.Valid → streamingState="streaming", CancelGeneration → idle+partialText, ResumeConfirm → streaming, ResumeDeny → idle. Load expected from `fixtures/model/*.json` (HARDCODED). @TEST_FIXTURE: send_message_valid -> fixtures/model/send_message_valid.json --- ## 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, внутри → читает user JWT из `_user_jwt_context.get()` (ContextVar из T017a), service JWT из `_service_jwt_context.get()`, отправляет HTTP-запрос к FastAPI REST с dual-identity заголовками: `Authorization: Bearer {service_jwt}` + `X-User-JWT: {user_jwt}`. Список тулов: все существующие @assistant_tool операции. @DATA_CONTRACT Each @tool: Pydantic args → dual-identity HTTP call → JSON string result @PRE ContextVars set by handler before graph invocation RATIONALE ContextVar is the ONLY mechanism to bridge Gradio handler's `request.headers` to @tool function execution in LangGraph — graph config does not propagate per-request auth - [ ] 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 `action="confirm"/"deny"`: load checkpoint via `PostgresSaver.get(thread_id)`. If checkpoint missing: yield `metadata.type="error"` with `CHECKPOINT_NOT_FOUND`. If found: invoke graph with `Command(resume={"action": action}, config={"thread_id": conversation_id})`. Graph resumes from checkpoint. @TEST_EDGE: checkpoint_not_found -> error_metadata, expired_graph_state -> error_metadata - [ ] 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 - [x] 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. - [x] T035 [US2] Write `backend/tests/test_agent/test_confirmations.py` — test: concurrent send lock, unknown action treated as normal, graph failure handled. - [ ] 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 - [x] 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 - [x] T061 [US5] Write `backend/tests/test_agent/test_conversation_api.py` — test: save creates/updates, active gate, LLM config, legacy compat. - [x] 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` - [x] T063a [US5] Write `backend/tests/test_agent/test_api_fixtures.py` — materialize API fixtures: FX_AgentChat.Conversations.ListValid → 200+items, ListEmpty → 200+[], ListMissingAuth → 401, ListInvalidPage → 422, ListExternalFail → 500, History.Valid → 200+messages, History.NotFound → 404, ServiceToken.Valid → 200+role=agent, ServiceToken.InvalidSecret → 401. Load input/expected from `fixtures/api/*.json` (HARDCODED). @TEST_CONTRACT: [FixtureJSON] -> [HTTPResponseAssertion] - [ ] 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 - [x] 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 JWT with role `agent`, TTL 24 hours. Gradio container: fetches at startup, stores in ContextVar (T017a), auto-refreshes at 12h or on 401 response from FastAPI. After 3× refresh failure → degraded mode (logs error, rejects tool calls). @DATA_CONTRACT Input: {service_secret} → Output: {access_token, expires_in} @TEST_EDGE: invalid_secret→401, expired_token→401_on_next_call, refresh_failure_3x→degraded_mode @SIDE_EFFECT Gradio container caches token, background refresh thread - [ ] 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) - [x] T084 [P] Backend full test suite: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/ -v` ⚠️ 17 failed (15 pre-existing task_manager + smoke_app, 2 LLM config in full suite context) - [x] T085 [P] Frontend full test suite: `cd frontend && npm run test -- --reporter=verbose` ✅ 2431/2431 passed (123 test files) - [ ] 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` - [x] T088 [P] Frontend build: `cd frontend && npm run build` ✅ Built in 26.38s, adapter-static wrote to "build" - [x] T089 Semantic audit — agent contracts: `axiom_semantic_validation audit_contracts file_path="backend/src/agent/"` ✅ PASS — 0 warnings, all contracts valid - [ ] 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. - [x] T091 Rejected-path regression test — verify that `/api/assistant` REST endpoints still function (FR-020 backward compat). Legacy compat tests in `test_conversation_api.py` verify GET /api/assistant/history and GET /api/assistant/conversations return 200. - [x] T092 Verify ADR guardrails: `@REJECTED full Svelte replacement` → SvelteKit routes intact; `@REJECTED custom SSE/WebSocket` → @gradio/client used exclusively, regression-tested; `@REJECTED separate auth mechanism` → JWT reused via dual-identity pattern. - [x] T093 [P] Run all fixture-based tests: `cd backend && source .venv/bin/activate && python -m pytest backend/tests/test_agent/test_model_fixtures.py backend/tests/test_agent/test_api_fixtures.py -v` @POST All 19 canonical fixtures pass: 14 API + 5 model - [x] T094 [P] Rejected-path fixture audit — run `test_model_fixtures.py::test_rejected_websocket` (FX_AgentChat.Model.RejectedPath). Verify: no WebSocket imports in AgentChatModel.svelte.ts, no tabRole type, no follower_notify handler, no takeoverSession action. @TEST_INVARIANT: NoWebSocketResurrection -> VERIFIED_BY: FX_AgentChat.Model.RejectedPath - [x] T095 [P] Fixture coverage audit — verify every C3+ contract in `contracts/modules.md` has ≥1 fixture in `fixtures/manifest.md`. Report uncovered contracts. ❌ Uncovered C3+ contracts: AgentChat.GradioApp, AgentChat.LangGraph.Setup, AgentChat.Tools, AgentChat.Middleware, AgentChat.Panel, AgentChat.Page, AgentChat.ConversationList, AgentChat.ConfirmationCard @POST All C3+ contracts covered by at least 1 fixture - [x] T096 [P] REST confirmation regression audit — grep `backend/src/api/routes/` for any REST confirmation/rejection endpoints (`/confirm`, `/resume`, `/approve`, `/deny`) and verify NONE exist (all confirmation handled by LangGraph HITL per @REJECTED path). ✅ PASS — no REST confirm/deny endpoints found in new agent routes (agent_conversations.py, app.py, tools.py, middleware.py) @TEST_INVARIANT: NoRESTConfirmation -> grep returns 0 matches --- ## Task Summary | Phase | Stories | Tasks | Parallel | Test Status | |-------|---------|:-----:|:--------:|:-----------:| | P1 — Setup | — | T001-T009 | 6 | ⏳ | | P2 — Foundational | — | T010-T014 | 2 | ⏳ | | P3 — US1 (Streaming) | P1 🎯 | T015-T028a | — | ✅ Tests 6/6 | | P4 — US2 (Tools) | P1 🎯 | T026-T038 | — | ✅ Tests 11/11 | | P5 — US3 (Visibility) | P2 | T039-T044 | — | ✅ Tests 5/5 | | P6 — US4 (Context) | P2 | T045-T051 | — | ⏳ | | P7 — US5 (Persistence) | P3 | T052-T064 | — | ✅ Tests 23/23 | | P8 — US6 (Files) | P2 | T065-T072 | — | ✅ Tests 9/9 | | P9 — Polish + Gaps | — | T073-T095 | 12 | ⚠️ Partial | | **Total** | | **96** | **20** | **47 agent + 2431 frontend** | ## 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