26 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"
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=10). 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. -
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—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]). 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/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: PydanticBaseModelдля 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 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 withadditional_inputs[1]="confirm"/"deny": invoke graph withCommand(resume={"action": action}, config={"thread_id": conversation_id}). Graph resumes from checkpoint. -
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: 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 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: 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 (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 long-lived JWT with roleagent. 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.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 - 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/assistantREST endpoints still function (FR-020 backward compat). Existingbackend/tests/test_assistant_api.pypasses 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