deer-flow/frontend/AGENTS.md
Nan Gao 4fcb4bc366
feat(subagents): persist and display subagent step history (#3779) (#3845)
* feat(subagents): persist and display subagent step history (#3779)

Capture both assistant turns and tool outputs during subagent execution,
stream them in task_running events, and persist them as subagent.* run
events so the subtask card's step timeline survives a reload.

Backend:
- step_events.py: pure layer (capture_step_message, build_subagent_step,
  subagent_run_event) shared by streaming and persistence
- executor.py: capture ToolMessage outputs, not just AIMessage turns
- worker.py: persist task_* custom events to RunEventStore (category
  "subagent" keeps them out of the thread feed; list_events backfills)

Frontend:
- core/tasks/steps.ts + api.ts: SubtaskStep model, messageToStep,
  eventsToSteps, mergeSteps, fetchSubtaskSteps
- subtask card accumulates live steps and backfills on expand
- carry run_id onto history content messages for the events endpoint

* fix(subagents): show AI turns in subtask card + paginate step backfill (#3779)

Two follow-ups to the subagent step-history feature:

Problem 1 — reload backfill could silently truncate the step timeline because
list_events capped at 500 events (seq-ASC) across the whole run. Add task_id
filtering + an after_seq forward cursor to list_events (all three stores +
abstract base + the /events route), and make fetchSubtaskSteps page through one
task's subagent.step events until a short page. No schema migration: the DB
filter rides the existing run-scoped index via event_metadata["task_id"].

Problem 2 — the card only rendered tool steps, so persisted AI turns were never
shown. Replace toolStepsForDisplay with stepsForDisplay: interleave AI reasoning
turns (with text) and tool steps by message_index, drop blank-text AI turns, and
drop the trailing final-answer AI turn when completed (already shown as result).
Card renders AI steps as muted clamped markdown with a sparkles icon.

Tests: store task_id/after_seq filtering + pagination across memory/db/jsonl,
the /events route forwarding, stepsForDisplay rules, and fetchSubtaskSteps
pagination. Docs updated in both AGENTS.md.

* make format

* fix(subagents): capture full multi-tool step tail, batch step persistence, cap tool-call args (#3779)

Address PR review findings on the subagent step-history feature:

1. executor.py streamed on stream_mode="values" and captured only
   messages[-1] per chunk, so a multi-tool-call turn (ToolNode appends
   one ToolMessage per call in a single super-step) lost all but the last
   tool output in both the live task_running stream and the persisted
   history. Replace with capture_new_step_messages, which walks the
   newly-appended tail (and still re-checks the trailing message on
   no-growth chunks so id-less in-place replacements survive).

2. worker.py persisted each step with the store's low-frequency put()
   (a per-thread advisory lock per call); a deep subagent (max_turns=150)
   emits hundreds of steps on the hot stream loop. Replace with
   _SubagentEventBuffer, which batches via put_batch (flush on terminal
   subagent.end, at FLUSH_THRESHOLD, and in the worker finally).

3. build_subagent_step capped only text; tool_calls[].args were copied
   verbatim, so a large write_file/bash payload produced an unbounded
   subagent.step row. Cap each call's serialized args at
   SUBAGENT_STEP_MAX_CHARS, flagged args_truncated.

Tests updated/added for all three; AGENTS.md refreshed.

* fix(subagents): merge backfill into latest subtask state; reuse message_content_to_text (#3779)

Address the remaining two PR review findings:

4. subtask-card's fetchSubtaskSteps().then(updateSubtask) closed over a
   stale tasks snapshot: a late-resolving backfill wrote setTasks({...stale}),
   clobbering SSE steps/status and sibling subtasks that arrived during the
   fetch. useUpdateSubtask now reads/writes through a tasksRef mirroring the
   latest state (ref-to-latest), and the pure per-subtask transition is
   extracted to core/tasks/subtask-update.ts::computeNextSubtask (unit-tested).

5. step_events._content_to_text duplicated deerflow.utils.messages.
   message_content_to_text; call the shared helper instead (guarding None
   content with 'or ""' so a tool-call-only turn still renders as "").

Tests added for computeNextSubtask and the None-content case; AGENTS.md docs updated.
2026-07-02 07:43:09 +08:00

8.3 KiB

AGENTS.md

This file provides guidance to AI coding agents (Claude Code, Codex, and others) when working with the DeerFlow frontend. It is the source of truth; the sibling CLAUDE.md imports it via @AGENTS.md.

Project Overview

DeerFlow Frontend is a Next.js 16 web interface for an AI agent system. It communicates with a LangGraph-based backend to provide thread-based AI conversations with streaming responses, artifacts, and a skills/tools system.

Stack: Next.js 16, React 19, TypeScript 5.8, Tailwind CSS 4, pnpm 10.26.2. Requires Node.js 22+ and pnpm 10.26.2+.

Core dependencies

  • LangGraph SDK (@langchain/langgraph-sdk ^1.5.3) — Agent orchestration and streaming
  • LangChain Core (@langchain/core ^1.1.15) — Fundamental AI building blocks
  • TanStack Query (@tanstack/react-query ^5.90.17) — Server state management
  • UI: Shadcn UI, MagicUI, React Bits, and Vercel AI SDK elements (generated from registries — see Code Style)

Commands

Command Purpose
pnpm dev Dev server with Turbopack (http://localhost:3000)
pnpm build Production build
pnpm check Lint + type check (run before committing)
pnpm lint ESLint only
pnpm lint:fix ESLint with auto-fix
pnpm format Prettier check (pnpm format:write to apply)
pnpm test Run unit tests with Rstest
pnpm test:e2e Run E2E tests with Playwright (Chromium)
pnpm typecheck TypeScript type check (tsc --noEmit)
pnpm start Start production server

Unit tests live under tests/unit/ and mirror the src/ layout (e.g., tests/unit/core/api/stream-mode.test.ts tests src/core/api/stream-mode.ts). Powered by Rstest; import source modules via the @/ path alias.

E2E tests live under tests/e2e/ and use Playwright with Chromium. They mock all backend APIs via page.route() network interception and test real page interactions (navigation, chat input, streaming responses). Config: playwright.config.ts.

Architecture

Frontend (Next.js) ──▶ LangGraph SDK ──▶ LangGraph Backend (lead_agent)
                                              ├── Sub-Agents
                                              └── Tools & Skills

The frontend is a stateful chat application. Users create threads (conversations), send messages, and receive streamed AI responses. The backend orchestrates agents that can produce artifacts (files/code) and todos.

Source Layout (src/)

  • app/ — Next.js App Router. Routes include / (landing), /workspace/chats/[thread_id] (chat), /workspace/agents/[agent_name] and /workspace/agents/new (custom agents), /blog/…, the (auth)/{login,setup,auth/callback} flow, /[lang]/docs/…, and /api/… route handlers (e.g. /api/memory).
  • components/ — React components:
    • ui/ — Shadcn UI primitives (auto-generated, ESLint-ignored)
    • ai-elements/ — Vercel AI SDK elements (auto-generated, ESLint-ignored)
    • workspace/ — Chat page components (messages, artifacts, settings)
    • landing/ — Landing page sections
    • docs/ — Docs / MDX rendering components
  • core/ — Business logic, the heart of the app. Domains include threads/ (creation, streaming, state), api/ (LangGraph client singleton), agents/ (custom agents), auth/ (authentication), artifacts/, channels/ (IM connections), i18n/ (en-US, zh-CN), settings/, memory/, skills/, messages/, mcp/, models/, suggestions/, tasks/, todos/, tools/, config/, notification/, blog/, plus rendering helpers (rehype/, streamdown/) and utils/.
  • hooks/ — Shared React hooks
  • lib/ — Utilities (cn() from clsx + tailwind-merge)
  • content/ — MDX content (blog posts, docs) rendered by the app
  • styles/ — Global CSS with Tailwind v4 @import syntax and CSS variables for theming
  • typings/ — Ambient TypeScript declarations
  • Root files: env.js (env validation), mdx-components.ts (MDX component map)

Data Flow

  1. User input → thread hooks (core/threads/hooks.ts) → LangGraph SDK streaming
  2. Stream events update thread state (messages, artifacts, todos)
  3. Stop actions call the LangGraph SDK stream stop path; core/threads/hooks.ts invalidates current-thread, token-usage, and sidebar/search caches immediately and schedules one follow-up refetch because SDK stop may finish via abort + fire-and-forget cancel before backend title finalization commits
  4. TanStack Query manages server state; localStorage stores user settings
  5. Components subscribe to thread state and render updates

Key Patterns

  • Server Components by default, "use client" only for interactive components
  • Thread hooks (useThreadStream, useSubmitThread, useThreads) are the primary API interface
  • LangGraph client is a singleton obtained via getAPIClient() in core/api/
  • Environment validation uses @t3-oss/env-nextjs with Zod schemas (src/env.js). Skip with SKIP_ENV_VALIDATION=1
  • Subtask step history (core/tasks/) — the subtask card shows a subagent's full step timeline (#3779): its assistant reasoning turns interleaved with the tools it ran. Subtask.steps[] is accumulated live from task_running events (appended via mergeSteps, not overwritten) and backfilled on expand for historical runs by fetchSubtaskSteps, which pages the events endpoint scoped to one task (GET /runs/{runId}/events?event_types=subagent.step&task_id=…&after_seq=…) until a short page, so the run-wide limit can't truncate the timeline. core/tasks/steps.ts is the pure model: messageToStep (live), eventsToSteps (reload), mergeSteps (dedup by message_index), and stepsForDisplay (what the card renders — keeps tool steps + AI steps with text, drops the trailing final-answer AI step when completed since it's shown as result). core/tasks/subtask-update.ts::computeNextSubtask is the pure per-subtask state transition (merge step deltas, keep terminal status stable); core/tasks/context.tsx's useUpdateSubtask applies it against a tasksRef mirroring the latest state (not a closure snapshot), so a late-resolving fetchSubtaskSteps backfill merges into current state instead of clobbering SSE steps or sibling subtasks that arrived meanwhile. The owning run_id is carried onto history content messages in buildVisibleHistoryMessages so the card can resolve the events endpoint.

Interaction Ownership

  • src/app/workspace/chats/[thread_id]/page.tsx owns composer busy-state wiring.
  • src/core/threads/hooks.ts owns pre-submit upload state and thread submission.

Code Style

  • Imports: Enforced ordering (builtin → external → internal → parent → sibling), alphabetized, newlines between groups. Use inline type imports: import { type Foo }.
  • Unused variables: Prefix with _.
  • Class names: Use cn() from @/lib/utils for conditional Tailwind classes.
  • Path alias: @/* maps to src/*.
  • Components: ui/ and ai-elements/ are generated from registries (Shadcn, MagicUI, React Bits, Vercel AI SDK) — don't manually edit these.

Environment

Backend API URLs are optional; an nginx proxy is used by default:

NEXT_PUBLIC_BACKEND_BASE_URL=http://localhost:8001
NEXT_PUBLIC_LANGGRAPH_BASE_URL=http://localhost:8001/api

Leave these unset for the standard make dev / Docker flow, where nginx serves the public /api/langgraph/* prefix and rewrites it to Gateway's native /api/* routes.

Resources

Contributing

When adding features:

  1. Follow the established src/ structure
  2. Add TypeScript types and proper error handling
  3. Write unit tests under tests/unit/ (pnpm test) and E2E tests under tests/e2e/ (pnpm test:e2e)
  4. Run pnpm check before committing
  5. Update this AGENTS.md when architecture, commands, or conventions change