@sheetgrid/agent
v0.1.0-alpha.1
Published
SheetGrid — framework-agnostic controller and LLM tool descriptors for agentic apps
Downloads
28
Maintainers
Readme
@sheetgrid/agent
Framework-agnostic controller and LLM tool descriptors for driving SheetGrid from an agent.
Pair with @sheetgrid/react or @sheetgrid/vue to make a grid that your app's AI agent can read, mutate, and undo — with typed operations, structured error returns, and paste/multi-cell atomicity out of the box.
| BYOK panel (6 providers) | Vue equivalent | |---|---| | | |
Try it live in 60 seconds —
pnpm dev:demo(React) orpnpm dev:demo-vue(Vue) from the monorepo. Both demos ship a BYOK panel above the chat with 6 providers (mock / Anthropic / OpenAI / OpenAI-compatible / Gemini / Vercel). Paste a key, pick a model, talk to your grid.
Table of contents
- Install
- Quickstart — React / Vue
- How SheetGrid compares
- Controller API
- Transactions, history, safety, events
- LLM tool descriptors
<AgentChat>anduseAgent- Recipe:
docs/recipes/12-agent-chat.md— full adapters, production proxy, security, troubleshooting
How SheetGrid compares
Agentic capabilities are new territory for data grids. Here's the honest state as of alpha:
| | SheetGrid | AG Grid | TanStack Table | Handsontable |
|---|---|---|---|---|
| Typed controller API for external programmatic driving | ✅ built-in (GridController) | ❌ (imperative Grid API, not designed for agents) | ❌ (headless — you own the state) | ❌ |
| Structured OpResult returns (no exceptions across boundary) | ✅ | ❌ | ❌ | ❌ |
| Reversible Command / History model with undo / redo / snapshot | ✅ | Partial (undo via Enterprise) | ❌ | Partial (via plugin) |
| Ready-made LLM tool descriptors (26 tools, JSON Schema, SDK-agnostic) | ✅ (describeGridTools) | ❌ | ❌ | ❌ |
| Drop-in chat UI with tool loop (<AgentChat> / useAgent) | ✅ | ❌ | ❌ | ❌ |
| Per-column + per-op authorization for agent writes | ✅ (agentWritable, authorize) | ❌ | N/A (you own state) | ❌ |
| Bundle impact if unused | 0 (opt-in package) | — | 0 | — |
| Framework support | React + Vue + Nuxt | React / Angular / Vue / vanilla | React / Vue / Solid / Svelte / Qwik | React / Vue / Angular |
| License | MIT | MIT + Enterprise ($) | MIT | MIT + Commercial ($) |
Where SheetGrid focuses: making the grid a first-class agent target. Where the others focus: enterprise-grade grid features (server-side row model, aggregation, pivot tables) that SheetGrid doesn't ship yet. Pick based on what dominates your project.
Install
pnpm add @sheetgrid/agent
# Plus your framework binding:
pnpm add @sheetgrid/react # or @sheetgrid/vue@nextPeer: @sheetgrid/core >= 0.3.0 (installed transitively).
Quickstart — React
import { Grid, useGridController } from "@sheetgrid/react";
import { describeGridTools } from "@sheetgrid/agent";
function App() {
const controller = useGridController({
// Optional: lock the whole grid.
readOnly: false,
// Optional: dynamic authorization.
authorize: (op) => {
if (op.type === "grid.delete_row" && !userIsAdmin) return "admins only";
return true;
},
});
// Get the tool descriptors to hand to your LLM SDK.
const tools = describeGridTools(controller);
// Anthropic example:
// await anthropic.messages.create({ model, tools, messages });
return (
<Grid
controller={controller}
rows={[
{ id: "r1", name: "Ada", age: 36 },
{ id: "r2", name: "Grace", age: 40 },
]}
columns={[
{ id: "name", header: "Name" },
{ id: "age", header: "Age", type: "number" },
]}
/>
);
}Quickstart — Vue
<script setup lang="ts">
import { SheetGrid, useGridController } from "@sheetgrid/vue";
import { describeGridTools } from "@sheetgrid/agent";
const controller = useGridController();
const tools = describeGridTools(controller);
</script>
<template>
<SheetGrid :controller="controller" :rows="rows" :columns="columns" />
</template>Controller API — reads
controller.getSchema();
controller.getData({ rowIds?, columnIds?, range?, includeFormulaSources? });
controller.getCell(rowId, columnId);
controller.queryRows(whereClause);
controller.getSelection();
controller.describe(); // human-readable summary for prompt contextController API — writes
Every write returns { ok: true } | { ok: false, code, message, details? }.
controller.setCell(rowId, columnId, value);
controller.setCells(patches); // partial-success report
controller.addRow(values, { at?, id? });
controller.updateRow(rowId, patch);
controller.deleteRow(rowId);
controller.moveRow(rowId, toIndex);
controller.addColumn(def, { at? });
controller.updateColumn(columnId, patch);
controller.deleteColumn(columnId);
controller.moveColumn(columnId, toIndex);
controller.setSort([{ columnId, direction }]);
controller.clearSort();
controller.setFilter({ column, op, value });
controller.select({ rowId, columnId });
controller.setFormula(rowId, columnId, source);
controller.clearFormula(rowId, columnId);Transactions
await controller.batch(async (tx) => {
tx.setCell("r1", "amount", 100);
tx.setCell("r1", "tax", 15);
// Throw to roll back. One undo reverses the whole batch.
});History and snapshots
controller.undo();
controller.redo();
controller.canUndo();
const snap = controller.snapshot();
controller.restore(snap); // itself undoableSafety layers
Three composable levers (all optional):
useGridController({
readOnly: true, // grid-wide lock
authorize: (op) => op.type !== "grid.delete_row" // dynamic policy
? true
: "deletes require admin",
});Per-column: mark { agentWritable: false } on any column def to lock it.
Events
controller.on("cell.changed", (e) => console.log(e));
controller.on("*", (e) => log(e)); // all eventsEvery event carries a source tag: { kind: 'agent' | 'user' | 'system', ... } so multi-actor sessions stay coherent.
LLM tool descriptors
describeGridTools(controller) returns SDK-agnostic descriptors:
{
name: 'grid_set_cell',
description: 'Write a single cell...',
input_schema: { /* JSON Schema */ },
execute(input) => Promise<OpResult>;
}Convert for your SDK
Anthropic (matches directly):
const tools = describeGridTools(controller).map((t) => ({
name: t.name,
description: t.description,
input_schema: t.input_schema,
}));OpenAI (rename input_schema → parameters):
const tools = describeGridTools(controller).map((t) => ({
type: "function",
function: {
name: t.name,
description: t.description,
parameters: t.input_schema,
},
}));Vercel AI SDK:
import { tool } from "ai";
const tools = Object.fromEntries(
describeGridTools(controller).map((t) => [
t.name,
tool({
description: t.description,
parameters: /* your schema converter */,
execute: async (args) => (await t.execute(args)),
}),
]),
);<AgentChat> and useAgent — drop-in chat UI
For a full chat experience, use the higher-level useAgent hook (React + Vue) or <AgentChat> component. Both are built on createAgentLoop, are framework-native, and ship zero LLM SDK dependencies.
React — <AgentChat>
import { Grid, useGridController, AgentChat } from "@sheetgrid/react";
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ apiKey: import.meta.env.VITE_ANTHROPIC_KEY });
function App() {
const controller = useGridController();
return (
<>
<Grid controller={controller} rows={rows} columns={columns} />
<AgentChat
controller={controller}
send={async ({ messages, tools, systemPrompt, signal }) => {
const res = await client.messages.create(
{
model: "claude-opus-4-7",
max_tokens: 1024,
system: systemPrompt,
tools: tools.map((t) => ({
name: t.name,
description: t.description,
input_schema: t.input_schema,
})),
messages: messages.map((m) =>
m.role === "user"
? { role: "user", content: m.content }
: m.role === "assistant"
? { role: "assistant", content: m.content }
: { role: "user", content: m.content.map((c) => ({ type: "tool_result", tool_use_id: c.tool_use_id, content: JSON.stringify(c.output) })) }
),
},
{ signal },
);
return res;
}}
/>
</>
);
}That's the full integration. Anthropic's response shape matches SendOutput directly.
Vue — <AgentChat>
<script setup lang="ts">
import { SheetGrid, useGridController, AgentChat } from "@sheetgrid/vue";
const controller = useGridController();
async function send(input) { /* same as React */ }
</script>
<template>
<AgentChat :controller="controller" :send="send" />
</template>Headless — useAgent
If <AgentChat> doesn't fit your UI, use the hook directly:
const { messages, thinking, send, error } = useAgent(controller, { send: myLLMCall });
// render however you wantAdapters for other SDKs
- OpenAI: rename
tool.input_schema→parameters; map responsetool_calls→contentblocks (~10 lines). - Vercel AI SDK: pass
toolstogenerateText; walk the returnedtoolCalls(~15 lines).
The loop engine only cares that send returns { content: [{ type: 'text' | 'tool_use' }], stop_reason }. Adapt on your side.
Interception hooks
<AgentChat
controller={controller}
send={myLLMCall}
onBeforeTool={(call) => call.name !== "grid_delete_row" || confirm("Really delete?")}
onAfterTool={(call, result) => console.log(call.name, result)}
onError={(err) => showToast(err.message)}
/>Return false from onBeforeTool to deny — the loop feeds the deny back to the model as a tool_result so it can adapt.
Customization
<AgentChat> accepts className, style, renderMessage, renderInput, renderToolTrace, renderError, renderThinking (Vue uses named slots with the same names). Beyond that, drop the component and use useAgent directly.
Status
Alpha. API surface is not stable until 0.1.0. Feedback via GitHub issues welcome.
