- test_agent_handler: additional edge cases for streaming, HITL, file upload - test_confirmations: HITL confirm/deny lifecycle coverage - test_conversation_api: conversation save/load persistence tests - test_langchain_tools: tool registration, dual-auth header propagation - ConversationList.test.ts (frontend): conversation list component tests - conftest: shared fixtures for agent tests - task_manager/manager: minor fixes from test coverage - tasks.md/test-documentation.md: spec and test documentation updates - speckit.test.md: speckit workflow documentation update
31 KiB
#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,pdfplumbertobackend/requirements.txt -
T002 [P] Add
@gradio/clienttofrontend/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. Noagent-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— addss-tools-agentservice (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. ForwardAuthorizationheader 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 modelsAgentConversation(id, user_id FK→users.id, title, is_archived, created_at, updated_at) andAgentMessage(id, conversation_id FK→agent_conversations.id, role, text, state, tool_calls JSON, attachments JSON, created_at). Useuuid4_strfor 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. Matchdata-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 matchbackend/src/schemas/agent.pyfield-for-field. @DATA_CONTRACT AgentTypes ↔ Schemas.Agent (cross-stack) -
T013 [P] Implement
frontend/src/types/agent.ts— TypeScript interfaces for conversation/message DTOs matchingbackend/src/schemas/agent.py.agent-ws.tsNOT created — no custom WebSocket protocol. -
T014 Generate Alembic migration for
agent_conversationsandagent_messagestables →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 fromspecs/033-gradio-agent-chat/fixtures/api/andfixtures/model/as test data. Load fixtures via pytestconftest.pyfixture 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— Gradiogr.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). Onaction="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 inDANGEROUS_TOOLSset. Compile graph withinterrupt_before=DANGEROUS_TOOLS. Wrap withRunnableWithMessageHistoryfor 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 fromrequest.headers["Authorization"], set viaContextVarBEFORE graph invocation (seebackend/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_contextand_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]). Iteratefor await (event):event.dataisChatMessagewith structuredmetadata. Render tokens, tool cards, confirmation cards based onmetadata.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 onmsg.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),connectionStateatom.
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. -
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 fromfixtures/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: PydanticBaseModelдля 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'srequest.headersto @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 withinterrupt_before=DANGEROUS_TOOLS. All tools fromtools.pyregistered 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 atinterrupt_beforenode: yieldChatMessage(metadata={"type":"confirm_required", ...}). On second submit withaction="confirm"/"deny": load checkpoint viaPostgresSaver.get(thread_id). If checkpoint missing: yieldmetadata.type="error"withCHECKPOINT_NOT_FOUND. If found: invoke graph withCommand(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— onmetadata.type="confirm_required": warning card + Confirm/Deny. Confirm →submit("/chat", {text:"confirm"}, [conversationId, "confirm"]). Deny →submit("/chat", {text:"deny"}, [conversationId, "deny"]). Onmetadata.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: 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 whenmetadata.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)— handletool_start(push card),tool_end(update status),tool_error(update status). UpdateactiveToolCalls[].
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
RunnableWithMessageHistoryauto-loads context from PostgreSQL perthread_id=conversation_id. No manualhistoryinjection needed.
Frontend
- T047 [US4] Implement
AgentChatModel.createConversation()infrontend/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.GetHistoryinbackend/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.DeleteConversationinbackend/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()andloadHistory()infrontend/src/lib/models/AgentChatModel.svelte.ts— call existinggetAssistantConversations()andgetAssistantHistory()fromfrontend/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()infrontend/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:<Input>for search (debounced 300ms), grouped by date on client ($derivedfromconversations), infinite scroll (IntersectionObserver), delete button (withconfirm()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/agentpage (left sidebar).
Verification
-
T061 [US5] Write
backend/tests/test_agent/test_conversation_api.py— test: save creates/updates, active gate, LLM config, legacy compat. -
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 -
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 fromfixtures/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 (Gradiogr.Filecomponent), 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<input type="file" accept=".pdf,.xlsx,.json,.csv,.txt,.png,.jpeg">, 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/agentroute. Two-column layout:<ConversationList>(240px left sidebar) +<AssistantChatPanel variant="embedded">(right area).<PageHeader>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 tomodel.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, callPOST /api/agent/conversations/{id}/activeto 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
/agentroute preserving current conversation context via store. @UX_FEEDBACK smooth navigation, conversation state preserved -
T077 Implement conversation auto-title — on first agent response, set
AgentConversation.titlefrom 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_audittable:{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 /healthreturns{"status":"ok","uptime":...}for Docker healthcheck. -
T080 [P] Implement
backend/src/api/routes/auth.py— internal endpointPOST /api/auth/service-tokenthat accepts a static service secret (envSERVICE_TOKEN_SECRET) and returns a JWT with roleagent, 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.jsonandfrontend/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.pyagent_handler — catchOutputParserExceptionfrom LangChain, retry once with"Respond with valid JSON only. Previous response was malformed."appended to prompt. If still malformed, yieldstream_errorwith 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()infrontend/src/lib/models/AgentChatModel.svelte.ts— ifstreamingState !== "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⚠️ 17 failed (15 pre-existing task_manager + smoke_app, 2 LLM config in full suite context) -
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 -
T088 [P] Frontend build:
cd frontend && npm run build✅ Built in 26.38s, adapter-static wrote to "build" -
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. -
T091 Rejected-path regression test — verify that
/api/assistantREST endpoints still function (FR-020 backward compat). Legacy compat tests intest_conversation_api.pyverify GET /api/assistant/history and GET /api/assistant/conversations return 200. -
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. -
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 -
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 -
T095 [P] Fixture coverage audit — verify every C3+ contract in
contracts/modules.mdhas ≥1 fixture infixtures/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 -
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