Files
ducoterra dbf2af26c6 refactor(agents): migrate .agent/ planning tree to .agents/
Standardize on the .agents/ directory (shared with project skills):
phases/, user_stories/, reports/, screenshots/, validate.sh, and
phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves
history; runtime artifacts move alongside).

Updates every reference in AGENTS.md, README.md, .gitignore, app
docstrings, and test story headers. Historical KB content in data/
and the runtime pipeline.log transcript are left untouched.
2026-09-05 10:57:07 -04:00

3.6 KiB

Story: Agent document tools (list + read)

Phase: 37_agent_document_tools · E2E: tests/e2e/test_agent_document_tools.py

Narrative

As a user, when my question depends on content the retriever did not put in context — e.g. a document that references another file ("the exact JSON shape is in example-record-file.json") — today I get a guess or an apology ("I don't want to invent it!"). I want the answer agent to have two tools: list the available sources, and read the one extra document it thinks is relevant — with the number of opportunities configured by environment variables — and the UI to show a "calling tool" state in addition to "thinking".

  • Given a grounded (HIGH-gate) chat turn
  • When the model calls list_documents (≤ BOR_AGENT_LIST_CALLS, default 1) or read_document(source, path) (≤ BOR_AGENT_READ_CALLS, default 1)
  • Then the app executes the tool server-side (DB-only), streams a tool SSE event the UI renders as a "calling tool" line + button state, and — once both budgets are spent — the model is required to answer with the augmented context; done sources include the read document.

Acceptance criteria

  1. Live probe (task 01): scripts/llm_probe.py --tools verifies turbo's tool-calling support (non-streaming + streaming) against the aipi endpoint; the verdict is recorded in the app/rag/agent.py docstring + the commit message (phase-17 "verified live" convention).
  2. app/rag/llm.py: chat_stream(messages, tools=None) — tools passed through to the API; streaming tool_calls deltas (partial name / fragmented arguments, indexed) accumulated into ToolCallPiece(id, name, arguments); tools=None → byte-identical request to today.
  3. app/rag/agent.py: the loop — budgets from BOR_AGENT_LIST_CALLS / BOR_AGENT_READ_CALLS (default 1/1); the list_documents tool returns the DB catalog (source: X | path: Y | title: Z lines, /api/docs order); read_document appends the full document text (A7-revised: never truncated); after both budgets are spent the tools are dropped and the model must answer; safety cap on model rounds; the deflection (LOW) path is byte-identical.
  4. app/api/chat.py + app/schemas.py: new SSE {"type":"tool","name":…,"argument":…} event (PLAN §4 revision note, owner permission 2026-08-26); done.sources + query_log.sources include the read document (deduped); the per-turn log line gains tool_calls=N (PLAN §9 note).
  5. Frontend: "calling tool" button/label state + visible .tool-call lines in the bubble (distinct from the Thinking block), aria-live, tool lines persisted with the chat record (phase 14/17 convention); UI Structure Check + no CDN.
  6. mock_llm.py deterministic: marker-driven list → read → answer sequence (the mock parses the catalog out of the tool result it was given); the story E2E asserts the tool UI, the answer quoting the read document, the read document as a source chip, and reload re-render; existing suites stay green (no tool events without the marker).
  7. Unit + integration green, app/ coverage >90%, story E2E green in isolation, ruff + pyright clean, one --no-gpg-sign commit.

Playwright Mapping Rule

tests/e2e/test_agent_document_tools.py — one story, one file, run in isolation: the marker question triggers two tool events; the UI shows the "calling tool" state while tools run; the final answer quotes the read document; the source chips include it; after a reload the tool lines re-render; a plain question and a deflected question produce no tool events.