Skip to content

Building Agents

This guide covers building AI agents with @robota-sdk/agent-core — the foundation layer of the Robota SDK.

Robota Class

Robota is the main agent class. It wraps an AI provider with conversation history, tool execution, and plugin support.

typescript
import { Robota } from '@robota-sdk/agent-core';

const agent = new Robota({
  name: 'MyAgent',
  aiProviders: [provider],
  defaultModel: {
    provider: 'anthropic',
    model: 'claude-sonnet-4-6',
    systemMessage: 'You are a helpful assistant.',
  },
});

const response = await agent.run('Hello!');

IAgentConfig

FieldTypeRequiredDescription
namestringyesAgent name (for logging and identification)
aiProvidersIAIProvider[]yesOne or more provider instances
defaultModelobjectyesDefault provider, model, and system message
defaultModel.providerstringyesProvider name (must match an aiProviders entry)
defaultModel.modelstringyesModel identifier (e.g., claude-sonnet-4-6)
defaultModel.systemMessagestringnoSystem prompt for the agent
toolsIToolWithEventService[]noTools the agent can call
pluginsIPluginContract[]noPlugins for lifecycle hooks
systemMessagestringnoTop-level system message (alternative to defaultModel.systemMessage)

Key Methods

MethodDescription
run(input)Send a message and get a response. Executes tool calls automatically.
getHistory()Get the full conversation history as TUniversalMessage[].
clearHistory()Clear the conversation history.
setModel({ provider, model })Switch provider and model mid-conversation.

AI Providers

Providers implement the IAIProvider interface from agent-core. Each provider translates between the universal message format and the provider-specific API.

Anthropic (Claude)

typescript
import { AnthropicProvider } from '@robota-sdk/agent-provider-anthropic';

const provider = new AnthropicProvider({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

Supported models: claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5

OpenAI (not yet published)

typescript
import { OpenAIProvider } from '@robota-sdk/agent-provider-openai';

const provider = new OpenAIProvider({
  apiKey: process.env.OPENAI_API_KEY,
});

Google (not yet published)

typescript
import { GoogleProvider } from '@robota-sdk/agent-provider-google';

const provider = new GoogleProvider({
  apiKey: process.env.GOOGLE_API_KEY,
});

Switching Providers

typescript
const agent = new Robota({
  name: 'FlexAgent',
  aiProviders: [anthropicProvider, openaiProvider, googleProvider],
  defaultModel: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
});

// Switch at any time
agent.setModel({ provider: 'openai', model: 'gpt-4o' });

Tools

Tools let agents call functions during a conversation. The agent decides when to use a tool based on the conversation context.

Creating Tools with Zod

createZodFunctionTool takes positional arguments: name, description, Zod schema, and handler function. The handler receives validated parameters and returns a value (string or JSON-serializable).

typescript
import { createZodFunctionTool } from '@robota-sdk/agent-tools';
import { z } from 'zod';

const searchTool = createZodFunctionTool(
  'search_files',
  'Search for files by name pattern',
  z.object({
    pattern: z.string().describe('Glob pattern to match'),
    directory: z.string().optional().describe('Directory to search in'),
  }),
  async ({ pattern, directory }) => {
    const files = await glob(pattern, { cwd: directory ?? '.' });
    return JSON.stringify(files);
  },
);

Creating Tools with FunctionTool

FunctionTool takes a schema object and a handler function as separate arguments.

typescript
import { FunctionTool } from '@robota-sdk/agent-tools';

const timeTool = new FunctionTool(
  {
    name: 'current_time',
    description: 'Get the current date and time',
    parameters: {
      type: 'object',
      properties: {
        timezone: { type: 'string', description: 'IANA timezone' },
      },
    },
  },
  async (params) => {
    return new Date().toLocaleString('en-US', { timeZone: (params.timezone as string) ?? 'UTC' });
  },
);

Registering Tools with an Agent

typescript
const agent = new Robota({
  name: 'ToolAgent',
  aiProviders: [provider],
  defaultModel: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
  tools: [searchTool, timeTool],
});

// The agent will call tools automatically when appropriate
const response = await agent.run(
  'Find all .ts files in src/ and tell me the current time in Seoul',
);

Built-in CLI Tools

@robota-sdk/agent-tools ships 8 ready-to-use tools for file system operations and web access:

ToolDescription
bashToolExecute shell commands
readToolRead file contents with line numbers
writeToolWrite content to a file
editToolReplace a string in a file
globToolFind files by glob pattern
grepToolSearch file contents with regex
webFetchToolFetch URL content (HTML-to-text)
webSearchToolWeb search via Brave Search API

These are used by agent-sdk to assemble the CLI agent, but can also be used independently.

Plugins

Plugins hook into the agent lifecycle to add cross-cutting concerns.

Using Plugins

typescript
import { Robota, EventEmitterPlugin } from '@robota-sdk/agent-core';

const agent = new Robota({
  name: 'PluginAgent',
  aiProviders: [provider],
  defaultModel: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
  plugins: [new EventEmitterPlugin({ enabled: true })],
});

Plugin Lifecycle Hooks

HookTimingPurpose
beforeRunBefore LLM callInput transformation, validation
afterRunAfter LLM responseOutput processing, recording
onErrorOn execution errorError handling, recovery
onStreamChunkDuring streamingChunk processing
beforeToolExecutionBefore tool callTool input validation
afterToolExecutionAfter tool resultTool output processing

Available Plugins

EventEmitterPlugin is built into agent-core. 9 plugins are also available as separate @robota-sdk/agent-plugin-* packages (not yet published — available in the monorepo only):

Plugin PackagePurpose
@robota-sdk/agent-plugin-event-emitterEvent emission (standalone)
@robota-sdk/agent-plugin-loggingMulti-backend logging
@robota-sdk/agent-plugin-usageToken usage and cost tracking
@robota-sdk/agent-plugin-performanceMetrics collection
@robota-sdk/agent-plugin-execution-analyticsExecution analytics
@robota-sdk/agent-plugin-error-handlingError recovery strategies
@robota-sdk/agent-plugin-limitsRate limiting
@robota-sdk/agent-plugin-conversation-historyPersistent history
@robota-sdk/agent-plugin-webhookHTTP notifications

Creating Custom Plugins

typescript
import { AbstractPlugin } from '@robota-sdk/agent-core';

class MyPlugin extends AbstractPlugin {
  name = 'my-plugin';
  version = '1.0.0';

  async afterRun(context) {
    console.log(`Agent responded with ${context.response.length} characters`);
  }
}

Streaming

Text Delta Streaming

typescript
const agent = new Robota({
  name: 'StreamAgent',
  aiProviders: [provider],
  defaultModel: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
});

// The provider's onTextDelta callback streams text as it arrives
provider.onTextDelta = (delta) => process.stdout.write(delta);

const response = await agent.run('Write a poem about coding');
// Text appears in real-time via onTextDelta, response is the complete text

Conversation History

Robota maintains conversation history across run() calls. Every message has a unique id and a state ('complete' or 'interrupted').

typescript
const agent = new Robota({
  /* config */
});

await agent.run('My name is Alice.');
const response = await agent.run('What is my name?');
// response: "Your name is Alice."

// Access the full history
const history = agent.getHistory(); // TUniversalMessage[]
// Each message has: id, timestamp, state, role, content, metadata

// Clear and start fresh
agent.clearHistory();

Message State

Messages have a state field that tracks completion:

StateMeaning
'complete'Normal response — fully received
'interrupted'User pressed ESC during streaming — partial content

When the model receives history for the next turn, interrupted messages are annotated with [This response was interrupted by the user] so the model is aware of the interruption.

Streaming Buffer

During streaming, ConversationStore manages a streaming buffer internally:

  1. beginAssistant() — opens a new streaming buffer for the assistant turn
  2. appendStreaming(delta) — accumulates text from onTextDelta callbacks
  3. commitAssistant(state, metadata) — commits the buffer to history as a confirmed message

This is a single path — both normal completion ('complete') and abort ('interrupted') use the same commitAssistant call with different state values. History is append-only and read-only; text content is always preserved. The CLI's onTextDelta callback is preserved as a passthrough for real-time display.

Error Handling

All errors extend RobotaError with code, category, and recoverable properties:

typescript
import { ProviderError, RateLimitError } from '@robota-sdk/agent-core';

try {
  const response = await agent.run('Hello');
} catch (error) {
  if (error instanceof RateLimitError) {
    // Wait and retry
  } else if (error instanceof ProviderError) {
    // Provider-specific error
  }
}

Changes from v2.0.0

v2.0.0v3.0.0
Plugins built into agent-core9 plugins extracted to agent-plugin-* packages
FunctionTool in agent-coreMoved to @robota-sdk/agent-tools
ToolRegistry in agent-coreMoved to @robota-sdk/agent-tools
MCPTool / RelayMcpTool in agent-coreMoved to @robota-sdk/agent-tool-mcp
No permission/hook systemPermission evaluation + shell hook system in agent-core
No session managementSession class in agent-sessions with compaction
No CLIagent-cli with Ink TUI
No SDK layeragent-sdk with config/context loading and query()

Released under the MIT License.