TypeScript framework for building AI agents with structured outputs, tools, and type-safe execution.
It is part of Rubric's architecture for Generative UI when used with:
bun add @rubriclab/actions
@rubriclab scope packages are not built, they are all raw typescript. If using in a next.js app, make sure to transpile.
// next.config.ts
import type { NextConfig } from 'next'
export default {
transpilePackages: ['@rubriclab/agents'],
reactStrictMode: true
} satisfies NextConfigIf using inside the monorepo (@rubric), simply add
{"@rubriclab/agents": "*"}to dependencies and then runbun i
import { generateObject } from '@rubriclab/agents'
import { z } from 'zod'
const result = await generateObject({
apiKey: env.OPENAI_API_KEY,
model: 'gpt-5.2',
systemPrompt: 'Extract user info',
schema: z.object({
name: z.string(),
age: z.number()
}),
messages: [{ role: 'user', content: 'I am John, 25 years old' }]
})
// { name: "John", age: 25 }A tool agent will recurse until it has what it needs to export a final answer.
By default, the format of the final answer is { answer: string }.
import { createAgent, createTool } from '@rubriclab/agents'
import { z } from 'zod'
const systemPrompt = 'You are a weather agent. Use tools to get the weather for the user.'
const weatherTool = createTool({
schema: {
input: z.object({ city: z.string() }),
output: z.object({ temp: z.number(), condition: z.string() })
},
execute: async ({ city }) => ({ temp: 72, condition: 'sunny' })
})
const { executeAgent, eventTypes, __ToolEvent, __ResponseEvent } = createAgent({
systemPrompt,
tools: { getWeather: weatherTool }
})// Execute the agent
'use server'
import env from '~/env'
import { executeWeatherAgent } from './agent'
export async function sendMessage({ userId, message }: { userId: string; message: string }) {
const { answer } = await executeWeatherAgent({
messages: [{ role: 'user', content: message }],
onEvent: async events => {
switch (events.type) {
case 'function_call':
switch (events.name) {
case 'getWeather':
console.log(`the temperature in ${events.arguments.city} is ${events.result.temp} degrees`)
}
case 'assistant_message':
console.log(events.message.answer)
break
}
},
openAIKey: env.OPENAI_API_KEY
})
return answer
}You can override the default response format and agents that return complex types. Supplying an agent BOTH tools and response format will cause the agent to recurse using tools, and when ready, the response in the right format.
const responseFormat = createResponseFormat({
name: 'research_results',
schema: z.object({
answer: z.string(),
sources: z.array(z.object({ title: z.string(), url: z.string() }))
})
})const { executeAgent } = createAgent({
systemPrompt,
tools: { ... },
responseFormat
})Usage with @rubriclab/events
The createAgent(...) function returns eventTypes which is a record of Zod types.
const { executeAgent, eventTypes } = createAgent({
systemPrompt,
tools: { research: researchTool },
})
export { eventTypes as researchAgentEventTypes }
export { executeAgent as executeResearchAgent }You can easily pass these events to the events package
import { createEventTypes } from '@rubriclab/events'
import { researchAgentEventTypes } from './agent'
export const eventTypes = createEventTypes({
...researchAgentEventTypes
})You can then publish them safely:
import env from '~/env'
import { executeResearchAgent } from './agent'
import { publish } from './events/server'
await executeResearchAgent({
messages: [{ role: 'user', content: 'do something' }],
onEvent: async events => {
switch (events.type) {
case 'function_call':
await publish({
channel: userId,
eventType: events.name,
payload: events
})
break
case 'assistant_message':
await publish({
channel: userId,
eventType: events.type,
payload: events
})
break
}
},
openAIKey: env.OPENAI_API_KEY
})And consume them on the client with end to end safety:
useEvents({
id: userId,
on: {
research: ({arguments, result}) => console.log(arguments.query, result.map(r => r.title)),
assistant_message: ({message}) => console.log(message.answer)
}
})Usage with @rubriclab/actions
coming soon...
Usage with @rubriclab/blocks
coming soon...
Usage with @rubriclab/chains
coming soon...