Features

A graph orchestrator, not a chatbot with tools

Six specialized agents coordinated by a graph. Each one has a single job, a small toolset, and a focused system prompt. Below, every agent is broken down with its job, its tools, and the code that runs it.

AGENT 01

Router

Classifies the user's message into needed capabilities

The Router is the entry point of every turn. It reads the user's message and decides which downstream agents need to run — Schema, SQL, EDA, Viz, ML, or some combination. It's a lightweight JSON-mode call optimized for latency, not prose. By classifying intent up front, the Router prevents the unbounded reasoning loops that plague ReAct-style agents.

Tools
completeJson
LLM calls
1 fast call
  • Single fast JSON-mode LLM call — typically <400ms
  • Outputs a structured intent: schema lookup, SQL query, EDA, viz, ML, or synthesis
  • Determines which downstream agents run, eliminating agent wander
  • Re-runs on every turn — context is always fresh
src/lib/agents/router.ts
// src/lib/agents/router.ts
const decision = await llm.completeJson({
  system: ROUTER_SYSTEM_PROMPT,
  user: message,
  schema: {
    intent: ["schema", "sql", "eda", "viz", "ml", "synthesis"],
    agents: { type: "array", items: ["sql", "eda", "viz", "ml"] },
    reason: "string",
  },
});

// → { intent: "sql", agents: ["sql", "eda", "viz"], reason: "..." }
Why this agent exists

Without a Router, the LLM has to decide what tools to call inside its reasoning loop — that's where ReAct agents wander, hallucinate, and burn tokens.

The Router makes that decision up front in a single fast JSON call. Downstream agents only run if they're needed.

AGENT 02

Schema

Answers structural questions from the cached snapshot

The Schema agent answers questions about the database structure — what tables exist, what columns they have, primary and foreign keys, approximate row counts. It works from a cached schema snapshot taken at connection time, so it's instant. It only calls the LLM to phrase the answer in natural language, never to reason about the schema.

Tools
Schema text lookup
LLM calls
0 for pure lookups, 1 to phrase the answer
  • Reads from a cached snapshot — no live queries for structural questions
  • Re-runs introspection on demand via /api/refresh-schema
  • Surfaces primary keys, foreign keys, and approximate row counts
  • Generates suggested starter questions from the schema
src/lib/agents/schema.ts
// After /api/connect, the schema snapshot is cached:
{
  tables: [
    { name: "orders", columns: [...], pk: "id", rowHint: 3214 },
    { name: "order_items", columns: [...], pk: "id", fk: "orders.id" },
    // ...
  ]
}

// The Schema agent just looks this up — no LLM reasoning needed.
Why this agent exists

Most "what tables exist" questions don't need an LLM at all — they need a cache lookup.

Schema answers from the cached snapshot are instant. The LLM only writes the prose, never reasons about structure.

AGENT 03

SQL

Generates & executes a single query; gates writes in Zen mode

The SQL agent is the only agent that touches the live database. It generates a single query from the user's question, executes it, and stores the result in the frame-cache. If Zen mode is on and the statement is a write, it never auto-executes — it registers a pending write with a 10-minute TTL and surfaces it to the user for confirmation. On a syntax error, it gets exactly one regex-fallback retry before surfacing a structured error.

Tools
DbConnection.query / executeWrite, frame-cache
LLM calls
1 call (+ 1 regex-fallback retry)
  • Only agent permitted to construct or execute SQL — safety by design
  • Zen mode gates writes: pending writes never auto-execute
  • One retry on failure, then a clean structured error — no infinite loops
  • Results cached by id, only the id + preview passed downstream
src/lib/agents/sql.ts
// src/lib/agents/sql.ts
const sql = await llm.complete({
  system: SQL_SYSTEM_PROMPT,
  user: `${schemaSnapshot}

Question: ${question}`,
});

if (isWrite(sql) && zenMode) {
  // Register as pending — NEVER auto-execute
  const pending = await pendingWrites.register({ sql, sessionId, ttl: 600 });
  return { kind: "pending-write", pending };
}

const result = await db.query(sql);
const frameId = await frameCache.put(result);
return { kind: "sql", sql, frameId, rowCount: result.rows.length };
Why this agent exists

Only the SQL agent can construct or execute SQL. No other agent has database access — safety by design.

In Zen mode, writes are intercepted at the graph edge, never auto-executed. One retry on failure, then a clean error.

AGENT 04

EDA

Profiles a dataframe: stats, nulls, distributions, correlations

The EDA (Exploratory Data Analysis) agent takes a cached dataframe and produces a statistical profile: column types, null percentages, min/max/mean/median, value distributions, and correlations between numeric columns. The math is done in pure TypeScript — no Python, no pandas. The LLM only writes the narrative summary that accompanies the stats.

Tools
frame-cache get
LLM calls
1 call to phrase the narrative
  • Pure-TS statistics: mean, median, std-dev, quartiles, correlations
  • Null-percentage bars rendered inline in the canvas
  • Distribution histograms for numeric columns
  • Runs in parallel with Viz and ML after a SELECT
src/lib/agents/eda.ts
// src/lib/agents/eda.ts
const frame = await frameCache.get(frameId);

const profile = {
  columns: frame.columns.map(col => ({
    name: col.name,
    type: col.type,
    nullPct: nullPercentage(frame, col),
    stats: col.type === "number"
      ? numericStats(frame, col)  // mean, median, std, quartiles
      : categoricalStats(frame, col),  // top values, cardinality
  })),
  correlations: correlationMatrix(frame),
};

return { kind: "eda", profile };
Why this agent exists

Statistical profiling in pure TypeScript — no pandas, no Python runtime.

The math runs in the Node process. The LLM only writes the narrative that accompanies the stats.

AGENT 05

Viz

Decides chart type & emits a Vega-Lite JSON spec

The Viz agent looks at the dataframe and decides the right chart type — bar, line, scatter, pie, heatmap — then emits a declarative Vega-Lite JSON spec. The frontend renders the spec as SVG via react-vega. No executable plotting code ever runs in the browser, which is a deliberate safety choice. If the LLM picks a chart type that doesn't fit the data, a smart fallback rewrites the spec.

Tools
frame-cache get
LLM calls
1 call + smart fallback
  • Emits declarative Vega-Lite JSON — never executable plotting code
  • Smart fallback rewrites bad specs (e.g. scatter → bar for categorical data)
  • Charts export as SVG or PNG directly from the canvas
  • Runs in parallel with EDA and ML after a SELECT
src/lib/agents/viz.ts
// src/lib/agents/viz.ts
const spec = await llm.completeJson({
  system: VIZ_SYSTEM_PROMPT,
  user: `${frameSummary}

Question: ${question}`,
  schema: vegaLiteSchema,
});

// Validate against Vega-Lite schema; fall back if invalid
const validated = validateVegaLite(spec) ?? fallbackSpec(frame);

return { kind: "viz", spec: validated };
Why this agent exists

Charts are declarative Vega-Lite JSON, never executable plotting code. A deliberate safety choice.

If the LLM picks the wrong chart type, a smart fallback rewrites the spec automatically.

AGENT 06

ML

Runs OLS regression / k-means / linear-trend forecast

The ML agent picks the right model for the question — ordinary least-squares regression for relationships, k-means (with k-means++ initialization) for clustering, or linear-trend forecasting for time series. All the math is implemented in pure TypeScript: normal equations + Gaussian elimination for OLS, k-means++ for initialization. No Python runtime, no scikit-learn, no extra dependencies.

Tools
frame-cache get, pure-TS math
LLM calls
1 call to pick the model
  • OLS regression via normal equations + Gaussian elimination
  • k-means clustering with k-means++ initialization
  • Linear-trend forecasting with R² and slope metrics
  • Pure TypeScript — no Python runtime, no extra dependencies
src/lib/agents/ml.ts
// src/lib/agents/ml.ts (pure TypeScript, no Python)
const model = await llm.completeJson({
  system: ML_SYSTEM_PROMPT,
  user: `${frameSummary}

Question: ${question}`,
  schema: { model: ["ols", "kmeans", "forecast"], config: "object" },
});

switch (model.model) {
  case "ols":
    return { kind: "ml", result: olsRegression(frame, model.config) };
  case "kmeans":
    return { kind: "ml", result: kmeans(frame, model.config.k, { init: "kpp" }) };
  case "forecast":
    return { kind: "ml", result: linearForecast(frame, model.config.horizon) };
}
Why this agent exists

OLS regression via normal equations. k-means with k-means++ initialization. Linear-trend forecasting with R².

All in pure TypeScript. No scikit-learn, no Python runtime, no extra dependencies.

AGENT 07

Synthesis

Writes the final user-facing reply (streamed)

Synthesis is the only agent that writes user-facing prose. It receives the summaries from the other agents that ran this turn plus the list of canvas objects produced, and streams a natural-language reply token-by-token via SSE. Because it's the only agent that writes prose, the rest of the pipeline stays context-isolated — the Viz agent never sees credentials, the SQL agent never sees the full chat history.

Tools
agent summaries + canvas list
LLM calls
1 streaming call
  • Only agent that writes user-facing prose — strict context isolation
  • Streams token-by-token via Server-Sent Events
  • Receives only summaries, never raw rows or credentials
  • References canvas objects by id, keeping the reply grounded
src/lib/agents/synthesis.ts
// src/lib/agents/synthesis.ts
const stream = await llm.completeStream({
  system: SYNTHESIS_SYSTEM_PROMPT,
  user: `${agentSummaries}

Canvas: ${canvasObjectIds}`,
});

for await (const token of stream) {
  yield { type: "token", value: token };
}

yield { type: "reply_done" };
Why this agent exists

The only agent that writes user-facing prose. Strict context isolation.

Receives only summaries from other agents — never raw rows, never credentials. Streams token-by-token via SSE.

Four pillars

Organized capabilities, not a feature dump

The agent pipeline, safety & control, canvas artifacts, and connectivity — the four pillars of SelectStar's architecture.

Agent Pipeline

Six specialized agents — Router, Schema, SQL, EDA, Viz, ML — coordinated by a graph orchestrator. Each agent has one job, a small toolset, and a focused system prompt. The orchestrator decides which agents run, not the user, not the agents.

  • Router classifies intent & picks agents
  • SQL agent writes & executes queries
  • EDA agent profiles & computes stats
  • Viz agent emits Vega-Lite specs
  • ML agent: regression, k-means, forecast
  • Synthesis streams the final reply

Safety & Control

Zen mode is non-negotiable. Read-only by default. Writes are intercepted at the graph edge, never auto-executed. Every write resolution — executed, rolled-back, cancelled, or failed — is audit-logged with the SQL text, timestamp, and row count.

  • Read-only by default
  • Zen-mode toggle for writes
  • Per-write confirmation UI
  • Dry-run with rollback
  • Full audit log

Canvas Artifacts

Every turn produces structured artifacts in the canvas pane alongside the chat reply. Tables, charts, SQL, statistical profiles, and model results — each rendered as a typed component with export buttons. No copy-pasting from a terminal.

  • Query result tables
  • Vega-Lite charts (SVG)
  • Syntax-highlighted SQL
  • Statistical summaries
  • Model results with metrics

Connectivity & Classic Mode

Two ways to get data into SelectStar: connect a live database (SQLite, PostgreSQL, MySQL-ready) or upload spreadsheets (CSV, XLSX, ODS). Classic mode parses each file into in-memory SQLite tables — the agents treat them like any database, and you can edit cells directly.

  • SQLite via better-sqlite3
  • PostgreSQL via pg
  • CSV / XLSX / ODS upload
  • Any OpenAI-compatible LLM
  • Demo DB included
Canvas artifacts

Structured results, not just chat

Every turn produces typed artifacts in the canvas pane alongside the chat reply — tables, charts, SQL, statistical profiles, and model results. Each one exports to a real file format.

localhost:3000/selectstar
● live
Query Result Table — SelectStar canvas artifact
Query Result Table

Query Result Table

When the SQL agent executes a SELECT, the result set renders as a sortable, scrollable table in the canvas. Export to CSV with one click. The full frame is cached server-side by id — only the id and a small preview travel through the LLM context, so large result sets never bloat the prompt.

  • Sortable, scrollable result table with column types
  • One-click CSV export
  • Server-side frame-cache — large results don't bloat prompts
  • Row count + execution time shown inline
localhost:3000/selectstar
● live
Vega-Lite Chart — SelectStar canvas artifact
Vega-Lite Chart

Vega-Lite Chart

The Viz agent emits a declarative Vega-Lite JSON spec, and the canvas renders it as SVG via react-vega. Bar, line, scatter, pie, heatmap — the agent picks the right type for the data. Export as SVG or PNG. No executable plotting code ever runs in the browser.

  • Declarative Vega-Lite JSON rendered as SVG
  • Agent picks chart type based on data shape
  • Smart fallback rewrites bad specs automatically
  • Export as SVG or PNG from the canvas
localhost:3000/selectstar
● live
ML Model Result — SelectStar canvas artifact
ML Model Result

ML Model Result

When the ML agent runs — regression, k-means, or forecast — the canvas shows the model metrics (R², slope, cluster centroids) and a predictions table. All math is pure TypeScript: OLS via normal equations, k-means with k-means++ init. No Python runtime required.

  • Model metrics: R², slope, cluster sizes, inertia
  • Predictions table with cluster assignments
  • Pure-TS math — no Python, no scikit-learn
  • Re-runs instantly from cached frames
localhost:3000/selectstar
● live
Spreadsheet Grid (Classic Mode) — SelectStar canvas artifact
Spreadsheet Grid (Classic Mode)

Spreadsheet Grid (Classic Mode)

In Classic Mode, uploaded CSV/XLSX files render as an editable spreadsheet grid. Set cells, add/rename/delete rows and columns — every edit writes through to the in-memory SQLite table underneath. The SQL agent sees your edits live; you can ask a question, edit a cell, and ask again without re-uploading.

  • Edit cells, add/rename/delete rows and columns
  • Edits write through to in-memory SQLite instantly
  • SQL agent sees edits live — no re-upload needed
  • Export the edited table back as CSV or XLSX

Want to see the agents run on your database?

Open the live app, type 'demo', and ask your first question in 60 seconds. No install required.