bilig

LangGraph.js WorkPaper ToolNode Spreadsheet Tool

LangGraph.js workflows often route model tool calls through a ToolNode. That is a good place for WorkPaper tools when the graph needs a number it can trust: read a formula-backed summary, edit one input, recalculate, persist WorkPaper JSON, restore it, then keep the proof in graph state.

The checked example uses the real @langchain/langgraph ToolNode with AIMessage tool calls and returned ToolMessage state. It does not require an LLM key because the smoke test supplies deterministic tool calls directly.

There are two owned proofs:

Run the checked graph

git clone https://github.com/proompteng/bilig.git
cd bilig
cd examples/langgraph-workpaper-tool-state
pnpm install --ignore-workspace --lockfile=false
pnpm run typecheck
pnpm run smoke

The smoke builds this graph:

new StateGraph(MessagesAnnotation)
  .addNode('agent_requests_workpaper_tools', deterministicToolCalls)
  .addNode('tools', new ToolNode(workpaperTools))
  .addEdge(START, 'agent_requests_workpaper_tools')
  .addEdge('agent_requests_workpaper_tools', 'tools')
  .addEdge('tools', END)

It returns the graph nodes, tool-message names, the pre-edit summary, and the verified WorkPaper write proof:

{
  "framework": "langgraphjs-toolnode",
  "graphNodes": ["agent_requests_workpaper_tools", "tools"],
  "toolMessageNames": ["read_workpaper_quote", "set_workpaper_quantity"],
  "proof": {
    "editedCell": "Inputs!B2",
    "before": {
      "total": 1458
    },
    "after": {
      "total": 2187
    },
    "afterRestore": {
      "total": 2187
    },
    "verified": true
  }
}

ToolNode shape

import { AIMessage } from '@langchain/core/messages'
import { tool } from '@langchain/core/tools'
import { StateGraph, MessagesAnnotation, START, END } from '@langchain/langgraph'
import { ToolNode } from '@langchain/langgraph/prebuilt'

const tools = [
  tool(readQuoteSummary, {
    name: 'read_workpaper_quote',
    schema: z.object({}),
  }),
  tool(setQuantityAndProve, {
    name: 'set_workpaper_quantity',
    schema: z.object({ quantity: z.number().finite().positive() }),
  }),
]

const graph = new StateGraph(MessagesAnnotation)
  .addNode('agent_requests_workpaper_tools', () => ({
    messages: [
      new AIMessage({
        content: '',
        tool_calls: [
          { id: 'call_read_quote', name: 'read_workpaper_quote', args: {}, type: 'tool_call' },
          { id: 'call_set_quantity', name: 'set_workpaper_quantity', args: { quantity: 18 }, type: 'tool_call' },
        ],
      }),
    ],
  }))
  .addNode('tools', new ToolNode(tools))
  .addEdge(START, 'agent_requests_workpaper_tools')
  .addEdge('agent_requests_workpaper_tools', 'tools')
  .addEdge('tools', END)
  .compile()

What to copy

MCP adapter proof

Use this path when the agent stack already loads tools through MCP:

git clone https://github.com/proompteng/bilig.git
cd bilig
cd examples/langchain-mcp-workpaper-toolnode
pnpm install --ignore-workspace --lockfile=false
pnpm run typecheck
pnpm run smoke

The smoke starts the published WorkPaper MCP server over stdio:

npm exec --yes --package @bilig/workpaper@latest -- \
  bilig-workpaper-mcp \
  --workpaper .tmp/pricing.workpaper.json \
  --init-demo-workpaper \
  --writable

Then MultiServerMCPClient discovers the file-backed WorkPaper tools and ToolNode calls read_cell, set_cell_contents, read_cell again, get_cell_display_value, and export_workpaper_document. Finally it starts a second read-only MCP client against the same WorkPaper JSON to prove the persisted formula result survives a process boundary.

Expected proof shape:

{
  "framework": "langchainjs-mcp-adapters-toolnode",
  "mcpTransport": "stdio",
  "workpaperPackage": "@bilig/workpaper@latest",
  "editedCell": "Inputs!B3",
  "dependentCell": "Summary!B3",
  "before": 60000,
  "after": 96000,
  "afterRestore": 96000,
  "afterRestart": 96000,
  "displayValue": "96000",
  "persistedDocumentBytes": 1162,
  "checks": {
    "discoveredFileBackedTools": true,
    "dependentCellChanged": true,
    "persistedToDisk": true,
    "restartReadbackMatchesAfter": true,
    "displayValueRead": true,
    "exportedWorkPaperDocument": true
  },
  "verified": true
}

Official LangGraph.js references:

Runnable source: examples/langgraph-workpaper-tool-state and examples/langchain-mcp-workpaper-toolnode.