bilig

OpenAI Agents SDK WorkPaper Tools

Use this path when an OpenAI Agents SDK app needs a workbook tool it can call from Node without opening Excel, LibreOffice, Google Sheets, or a screenshot UI. There are two maintained integration shapes:

The direct function-tool path gives the agent two ordinary function tools:

The maintained smoke script is provider-free by default. It imports Agent, tool(), RunContext, and invokeFunctionTool() from @openai/agents, creates a real SDK agent and function tools, then invokes the tools locally so the read/write contract can run in CI without an API key:

pnpm --dir examples/headless-workpaper run agent:openai-agents-sdk

The OpenAI Agents SDK documents function tools as local functions wrapped with a schema through tool(), and the same tools can be attached to an Agent: https://openai.github.io/openai-agents-js/guides/tools/.

The same guide documents MCP servers as attachable tool sources through MCPServerStdio; Bilig keeps a provider-free smoke for that path too:

pnpm --dir examples/headless-workpaper run agent:openai-agents-sdk-mcp

Minimal Tool Shape

import { Agent, RunContext, invokeFunctionTool, tool } from '@openai/agents'
import { z } from 'zod'
import { WorkPaper } from '@bilig/headless'

const workbook = WorkPaper.buildFromSheets({
  Inputs: [
    ['Metric', 'Value'],
    ['Qualified opportunities', 20],
    ['Win rate', 0.25],
    ['Average ARR', 12000],
  ],
  Summary: [
    ['Metric', 'Value'],
    ['Expected ARR', '=Inputs!B2*Inputs!B3*Inputs!B4'],
  ],
})

const setWorkPaperInputCell = tool({
  name: 'set_workpaper_input_cell',
  description: 'Set one validated WorkPaper input cell and return formula readback.',
  parameters: z.object({
    sheetName: z.literal('Inputs'),
    address: z.string().regex(/^[A-Z]+[1-9][0-9]*$/),
    value: z.union([z.string(), z.number(), z.boolean(), z.null()]),
  }),
  execute: async ({ sheetName, address, value }) => {
    const sheet = workbook.getSheetId(sheetName)
    const summarySheet = workbook.getSheetId('Summary')
    if (sheet === undefined) {
      throw new Error(`Unknown sheet: ${sheetName}`)
    }
    if (summarySheet === undefined) {
      throw new Error('Summary sheet is missing')
    }
    const cell = workbook.simpleCellAddressFromString(address, sheet)
    const summaryRange = workbook.simpleCellRangeFromString('Summary!A1:B2', summarySheet)
    if (cell === undefined) {
      throw new Error(`Invalid cell: ${sheetName}!${address}`)
    }
    if (summaryRange === undefined) {
      throw new Error('Summary range is invalid')
    }

    const before = workbook.getRangeValues(summaryRange)
    workbook.setCellContents(cell, value)

    return {
      editedCell: `${sheetName}!${address}`,
      before,
      after: workbook.getRangeValues(summaryRange),
    }
  },
})

const agent = new Agent({
  name: 'WorkPaper verification agent',
  instructions: 'Use WorkPaper tools and answer only from computed readback.',
  tools: [setWorkPaperInputCell],
})

const result = await invokeFunctionTool({
  tool: setWorkPaperInputCell,
  runContext: new RunContext(),
  input: JSON.stringify({
    sheetName: 'Inputs',
    address: 'B3',
    value: 0.4,
  }),
})

console.log(agent.name, result)

For a production adapter, use the full example instead of this short snippet: examples/headless-workpaper/openai-agents-sdk-tool-smoke.ts. It also verifies persisted formulas by exporting a WorkPaper document, restoring it, and comparing the computed readback after restore.

MCP Server Shape

Use this when your OpenAI Agents SDK app already manages MCP servers or when you want the same Bilig WorkPaper server available to other agent clients:

import { Agent, MCPServerStdio, RunContext, getAllMcpTools, invokeFunctionTool } from '@openai/agents'

const server = new MCPServerStdio({
  name: 'bilig-workpaper-stdio',
  fullCommand: 'npm run --silent agent:mcp-stdio',
  cwd: 'examples/headless-workpaper',
})

await server.connect()
try {
  const agent = new Agent({
    name: 'WorkPaper MCP verification agent',
    instructions: 'Answer only from computed WorkPaper MCP readback.',
    mcpServers: [server],
  })
  const runContext = new RunContext()
  const tools = await getAllMcpTools({
    mcpServers: [server],
    runContext,
    agent,
    convertSchemasToStrict: true,
  })
  const setInput = tools.find((tool) => tool.name === 'set_workpaper_input_cell')
  if (setInput === undefined) {
    throw new Error('Missing set_workpaper_input_cell')
  }

  const result = await invokeFunctionTool({
    tool: setInput,
    runContext,
    input: JSON.stringify({ sheetName: 'Inputs', address: 'B3', value: 0.4 }),
  })
  console.log(result)
} finally {
  await server.close()
}

The maintained proof file is examples/headless-workpaper/openai-agents-sdk-mcp-smoke.ts. It starts the Bilig stdio server, lists MCP tools, converts them into Agents SDK function tools with getAllMcpTools(), invokes set_workpaper_input_cell, and asserts formula readback plus JSON restore.

Expected Proof

The smoke output includes this shape:

{
  "apiShape": "OpenAI Agents SDK Agent -> tool() -> invokeFunctionTool()",
  "package": "@openai/agents",
  "agentName": "WorkPaper verification agent",
  "toolNames": ["read_workpaper_summary", "set_workpaper_input_cell"],
  "writeResult": {
    "editedCell": "Inputs!B3",
    "before": { "expectedArr": 60000, "targetGap": -34000 },
    "after": { "expectedArr": 96000, "targetGap": 5600 },
    "checks": {
      "formulasPersisted": true,
      "restoredMatchesAfter": true,
      "expectedArrChanged": true
    }
  }
}

The MCP smoke output includes this shape:

{
  "apiShape": "OpenAI Agents SDK Agent -> MCPServerStdio -> getAllMcpTools() -> invokeFunctionTool()",
  "package": "@openai/agents",
  "agentName": "WorkPaper MCP verification agent",
  "mcpServerName": "bilig-workpaper-stdio",
  "rawMcpToolNames": ["read_workpaper_summary", "set_workpaper_input_cell"],
  "functionToolNames": ["read_workpaper_summary", "set_workpaper_input_cell"],
  "writeResult": {
    "editedCell": "Inputs!B3",
    "before": { "expectedArr": 60000, "targetGap": -34000 },
    "after": { "expectedArr": 96000, "targetGap": 5600 },
    "restored": { "expectedArr": 96000, "targetGap": 5600 },
    "checks": {
      "formulasPersisted": true,
      "restoredMatchesAfter": true,
      "expectedArrChanged": true
    }
  }
}

Keep the workbook mutation closed-world: validate sheet names and A1 addresses, write one input at a time, recalculate through WorkPaper, return computed readback, and persist only after the verification passes.