Lantide Data
This translation is available, but has not yet been checked against the latest Traditional Chinese source.

AI Agent Architecture: ReAct, State Routing, and UICommand

Companion ②: We implement the ReAct streaming loop ourselves in exchange for full control over event types, state filtering, and IDE integration. For usage, see User Guide §12.


Series Position

For the full introduction, see Series Introduction and Product Positioning.

Order Article Topic
0 Series Introduction and Product Positioning Series introduction and product positioning
1–3 Data Analysis Workflow in the Agent Era, Governable Agent Memory, Prompt and Context Engineering Workflow, Context
4 This article Agent runtime
5 Unified Query Layer Query layer

1. Why Build Our Own ReAct

ReAct (Reasoning + Acting, the reasoning–acting loop) lets the model alternate between "think → call a tool → read the observation → think again" within a single conversation turn. General-purpose Agent frameworks are good at orchestration, but Lantide Data needs finer control:

  • SSE event types broken down finely: thought, tool_call, tool_result, message, ask_user, error…
  • State-dependent tool allowlist and mid-turn refresh
  • UICommand synchronized with the frontend interceptor to open tabs, write SQL, and complete Source Run
  • IDE-integrated execution: tool output must land in real SQL tabs, the TabCacheRegistry, and UICommands—rather than merely returning charts from a remote sandbox
  • Product-level strategies such as exploration budget, provider fallback, and token compaction

Therefore we adopt a Python AsyncGenerator + our own stream_react_run, rather than applying an off-the-shelf Agent abstraction—we want an execution loop that is line-by-line aligned with IDE state, SSE events, and the tool allowlist, not the convenience of general-purpose orchestration.


2. Architecture Overview

flowchart TB
  api[FastAPI_stream]
  runner[LantideDataAgent.stream_react_run]
  router[StateRouter]
  prompt[_build_system_prompt]
  loop[ReAct_iteration]
  llm[LLM_streaming]
  tools[tool_ask_user_UICommand]
  sse[SSE_events]

  api --> runner
  runner --> router
  runner --> prompt
  runner --> loop
  loop --> llm
  llm --> tools
  tools --> loop
  loop --> sse

Each iteration: resolve state → filter tool schemas → assemble the system prompt → LLM stream → execute a tool / pause for ask_user / push a UICommand; after a state-transition tool runs, the prompt and tool set are refreshed within the same turn.


3. Tool Registration and TOOL_AVAILABILITY_MATRIX

Each tool defines its Safety (safe / write) and the AgentState in which it is available. The matrix filters schemas at the start of each iteration, for example:

State Typical difference
NoProject / Quick Exploration tools + focus_project
ProjectFocused open_plan, open_report, conditional HTML tools
PlanPlanning Document and exploration; limited run_query for data probing allowed; no SQL Tab tool group, no statistical run_* (the prompt forbids activate_analysis)
PlanExecuting run_sql_tab, source_run_sql_tab, add_report
ProjectFocused / PlanPlanning / PlanExecuting read_reference, patch_reference (read mappings on demand, make small edits to a Reference)

The state prompt does not list the tool catalog — names and parameters are governed by the current turn's function schemas (see Prompt and Context Engineering).


4. ReAct Loop Details

4.1 Iteration Limit

max_iterations defaults to 100, preventing an abnormal loop from exhausting resources; when the limit is reached, an iteration_limit event is emitted.

4.2 Exploration Budget

show_tables, read_schema, and validate_query count toward exploration. After exceeding the exploration_soft_cap (default 7), the tool call is skipped and EXPLORATION_BUDGET_REACHED is returned.

Continuation protocol: the next tool call must be ask_user, the question must contain the marker [[EXPLORATION_BUDGET]], and the options must include Continue exploring / Stop for now. If the user chooses to continue, the exploration count for this turn is reset; if the user chooses to stop, the Agent should first consolidate its findings before deciding. It should not bypass human confirmation by silently injecting a system string.

4.3 Auto-continuation

When finish_reason == "length" and there is still room, "Please continue..." may be appended (up to a limit), to avoid truncating long reports.

4.4 Mid-turn State Transition

After focus_project, add_report, etc. run, the state is re-resolved from the session, and messages[0] system prompt and the tool set are updated, so that subsequent iterations in the same turn land in the correct state (e.g., NoProject → ProjectFocused).


5. Provider Fallback

When an LLM call fails, it can fall back to a backup profile per configuration and be marked as such in the SSE, reducing the chance that a single unavailable model fails an entire turn.


6. System Prompt Assembly and Mid-turn Refresh

LantideDataAgent._build_system_prompt() merges core, state, knowledge, catalog hints, and the conditional HTML section. After a state transition or an html_report_active flip, it is rebuilt within the same turn, aligned with the tool schemas.

6.1 Query Step Ledger Injection (Layer 3.5)

During multi-step analysis, the model needs to remember "which table the previous step materialized, and what the SQL looked like." Each conversation maintains a Query Step Ledger within the workspace: after a run_query / run_sql_tab succeeds or fails, a step index is written; the system prompt appends a condensed summary (cache name + SQL preview, ≤1500 chars), so that the model still knows recent steps after many turns. The full SQL is looked up via query_result, get_cache_source, or the registry.

Division of labor with Plan Progress: the Steps UI during Plan execution and the execution_record.steps of read_plan come from the steps[] in *.progress.json (which becomes steps_frozen after the report); the Ledger still serves conversation continuity. fork_conversation copies the source conversation's ledger in full to the new conversation, so that list_query_steps and the summary are not broken after a fork.

Tool / API When to use Purpose
get_cache_source Need to review the original SQL of a cache table Equivalent to the UI "View SQL," same source as GET /cache/{tab_id}/source
list_query_steps A multi-step chain is out of order or step_ids need verification Lists the steps of this conversation; optionally with truncated SQL
query_result Read the result and SQL of a given run Meta sidecar in memory or after spill
[[QUERY_STEP]] protocol After each successful run (assistant body) A one-line summary; after the runner parses it, purpose / pitfalls are backfilled

conversation_id is injected by the runner; the LLM may not pass it itself; when there is no conversation, the ledger is skipped and the materialization strategy still runs on the workspace heuristics.

Annotations do not go through the Query Step Ledger—their state lives in the *.annotations.json sidecar and the body mark, handled by read_plan / read_report / resolve_annotations (see Prompt and Context Engineering §11).

6.3 Project Document and Annotation Tools

Tool Safety Typical state Purpose
read_plan / read_report safe Plan/Report related Disk body + sidecar + annotation_summary
resolve_annotations write Planning (body lock is not Executing/Executed) Atomically patch the body and sidecar per open annotations
read_reference safe ProjectFocused, PlanPlanning, PlanExecuting Load the full text of a Reference per the [Ref: …] When-to-read
patch_reference write Same as above Small-scope find-replace; does not create a new Reference file

7. HTML Tools and Contract (Body Summary)

For the product motivation, see Data Analysis Workflow in the Agent Era §5. For usage, see §10.

7.1 Conditional Registration html_report_active

When true (task_mode=html_report or a Report tab with contract HTML already on disk):

  • The always-present create_html_report (Generate / Re-generate)
  • Additions such as read_html_text, get/patch_html_chunk_by_id, get/patch_html_styles
  • Injection of the Editing HTML Reports state section

After a successful create_html_report in the same turn, the next iteration recomputes the flag and schemas, with no need to switch tabs.

7.2 activate_html_editing

When the user is not in a Report tab but wants to edit HTML, the Agent first calls this tool to unlock the chunk/style tools, then read → get → patch. When multiple reports all have HTML and no filename is specified, it should ask a follow-up or return AMBIGUOUS_REPORT. Old files without data-report-contract="1" should be Re-generated. Each conversation must re-activate or return to the Report tab.

7.3 Contract Highlights (Internalized Narrative)

  • Root element: <html data-report-contract="1" data-report-profile="standard|standard-chartjs|revealjs|offline">
  • Inside <head>, exactly one <style id="report-styles"> carries the global CSS
  • Directly under <body>, patchable blocks: data-text-id (regex ^[a-z][a-z0-9_-]{0,63}$) + data-chunk-desc; nested patchable containers are forbidden
  • create_html_report performs full validation; chunk tools validate only the body
  • standard-chartjs: the head includes the Chart.js CDN + #report-chart-init; charts use canvas[data-chart-spec] JSON; after patching, you must Open Report again to redraw

8. Agent Memory Tools

Tool Direction Description
expand_knowledge_catalog Read In catalog mode, batch-loads full Rules/Info entries by parent tag
propose_knowledge Write to queue Proactively proposes reusable knowledge into Queued Knowledge; injected only after the user Applies it

propose_knowledge shares knowledge_queue.json with passive extraction; the runner injects evidence_corpus_texts for validation. Rate limits: at most 2 proposals per user message (per full Agent stream); at most 5 pending per conversation (released after Apply/Dismiss). evidence_quote must be a verifiable substring of a recent user/tool message; SQL, cache table names, and other execution-state content are forbidden. For details, see Governable Agent Memory §4.3.


9. UICommand and Frontend Interceptor

Some tools do not end by directly returning a string, but instead send a UICommand SSE: the frontend handleUICommand executes open_tab, update_tab_content, set_tab_results, source_run_complete, focus_project, etc., and dedupes with processedEventIds. The user sees the tabs and results change, forming a "copilot" experience. See §12.6.


10. ask_user: Pause/Resume

  1. The Agent calls ask_user
  2. SSE type: ask_user → the frontend interaction area
  3. The user replies → POST /api_v1/ask_user_response
  4. The result is fed back into ReAct, and iteration continues

The timeout is about 5 minutes. Exploration quota, execution mode, Execute confirmation, and so on all follow this protocol (Data Analysis Workflow in the Agent Era).


11. Token Compaction Strategies

Three strategies complement each other on different time scales, and can be triggered in sequence within the same ReAct turn:

  • History truncation (cross-turn): long conversations preserve the analytical context and discard earlier irrelevant messages (Prompt and Context Engineering §12).
  • Mid-turn compaction (within a single turn): when tokens approach the limit after many tools in one turn, this turn's tool results are compacted, preserving key fields such as step_id and cache names (same article, §13).
  • Auto-continuation: when the model output is truncated due to length, a continuation request is appended while there is still headroom (§4.3); works in tandem with context window discovery.

12. Frontend Streaming (Optional)

streamStateMachine dispatches the SSE into Segments such as thought / tool_call / tool_result / message, and flushes with a mutable draft + requestAnimationFrame throttling, avoiding a React re-render on every token; the finish/error/abort paths forceFlush.


13. SQL Execution Tools (Aligned with the Unified Query Layer)

Tool Function
run_sql_tab Executes a persistent tab and materializes a cache
source_run_sql_tab Source Run DAG
sql_tab_result Reads a tab's cached result (supports resolution by tab title)
validate_query Exploration: validate only
run_query Preview rows (≤200); conditionally materializes into an agent cache (see Unified Query Layer §8.1)

The quality signals returned by tools also feed into the runner's judgment: complexity_hint means the step needs to be split smaller; optimization_hint is only a suggestion to consider cache and does not block analysis. The Release smoke's hard gate focuses on plan → todo → report, the absence of obvious error loops, and report quality signals; DAG / Source Run are soft signals, used to observe whether the model naturally uses the traceability capability.


14. Engineering Decision Overview

Decision Reason
Build our own ReAct SSE + state + UICommand
Exploration ask_user continuation Human confirmation rather than a hard stop
Conditional HTML tools Reduce default schema noise
max_iterations 100 Long-chain analysis can complete
UICommand Real IDE side effects
Release gate weights analysis quality Do not let cache shape replace a credible report

15. Conclusion

The Agent architecture is the execution engine for the workflow and Context: the ReAct loop turns the layered prompt into SSE events, tool calls, and IDE side effects (UICommand). The difference between the Harness and a chat sandbox is that execution is bound to a real workspace.

If the model "keeps validating but never runs run_query," check both the QEM explanation in Prompt and Context Engineering and the exploration budget in this article; for query materialization and lineage semantics, see Unified Query Layer.