Skip to main content
AI Generated Content
πŸ€– This documentation was generated with AI assistance. Please report any issues you find.

AI Agents

Pikku AI Agents let you define conversational AI assistants that use your Pikku functions as tools, maintain conversation memory, support multi-agent orchestration, and stream responses in real time β€” all with the same transport-agnostic architecture as the rest of Pikku.

What are AI Agents?​

An AI agent is a conversational interface backed by a large language model (LLM). When a user sends a message, the agent reasons about it, optionally calls Pikku functions as tools, and streams a response back through a channel.

Key features:

  • Functions as Tools: Any wired Pikku function can be used as an agent tool β€” complete with schema validation and middleware
  • Multi-Agent Orchestration: Agents can delegate to sub-agents, each with their own tools and memory
  • Conversation Memory: Persistent thread-based message history with configurable storage backends
  • Working Memory: Structured state that the agent maintains across turns (e.g., user preferences, task progress)
  • Real-Time Streaming: Token-by-token streaming via Pikku channels with full middleware support
  • Human-in-the-Loop: Tool approval flows that suspend execution until a user approves or denies a tool call
  • Model Configuration: Centralized model aliases, per-agent overrides, and multi-provider support
  • AI Middleware: Hooks to modify inputs, filter the output stream, and post-process results

Quick Example​

Define an agent that manages todos:

import { pikkuAIAgent } from './.pikku/agent/pikku-agent-types.gen'
import { listTodos, createTodo } from './todos.functions'

export const todoAssistant = pikkuAIAgent({
name: 'todo-assistant',
description: 'A helpful assistant that manages todos',
instructions: 'You help users manage their todo lists.',
model: 'openai/gpt-4o-mini',
tools: [listTodos, createTodo],
memory: { storage: 'aiStorage', lastMessages: 10 },
maxSteps: 5,
})

The pikkuAIAgent function is generated by the Pikku CLI and provides full type safety for your agent definition.

Core Concepts​

Agent Definition​

Every agent is defined with pikkuAIAgent() and requires at minimum:

PropertyTypeDescription
namestringUnique identifier for the agent
descriptionstringHuman-readable description (also shown to parent agents)
instructionsstring | string[]System prompt that guides the agent's behavior
modelstringModel identifier in provider/model format (e.g., openai/gpt-4o-mini)

Optional properties:

PropertyTypeDefaultDescription
toolsFunction[]β€”Pikku functions the agent can call
agentsAgent[]β€”Sub-agents this agent can delegate to
memoryAIAgentMemoryConfigβ€”Conversation and working memory config
maxStepsnumber10Maximum tool-call iterations per message
toolChoice'auto' | 'required' | 'none''auto'How the model selects tools
temperaturenumberβ€”Model temperature override
outputSchemaβ€”Structured output schema for the agent's response
tagsstring[]β€”Tags for filtering and organization
middlewareMiddleware[]β€”Pikku middleware applied to the agent
channelMiddlewareChannelMiddleware[]β€”Middleware for the streaming channel
aiMiddlewarePikkuAIMiddlewareHooks[]β€”AI-specific middleware hooks
permissionsPermissionGroupβ€”Permission requirements for accessing the agent

Tools​

Agents use your existing Pikku functions as tools. When you pass a function reference to the tools array, Pikku automatically:

  1. Extracts the function's input schema from its wired metadata
  2. Uses the function's description as the tool description for the LLM
  3. Executes the function through the standard Pikku function runner (with middleware, validation, etc.)
// These are regular Pikku functions
import { listTodos, createTodo, deleteTodo } from './todos.functions'

export const assistant = pikkuAIAgent({
name: 'assistant',
description: 'Task manager',
instructions: 'Help users manage tasks.',
model: 'openai/gpt-4o-mini',
tools: [listTodos, createTodo, deleteTodo],
})

Multi-Agent Orchestration​

Agents can delegate to sub-agents using the agents property. The parent agent sees each sub-agent as a tool it can call with a message and a session label:

export const todoAssistant = pikkuAIAgent({
name: 'todo-assistant',
description: 'Manages todo lists',
instructions: 'You help users manage their todo lists.',
model: 'openai/gpt-4o-mini',
tools: [listTodos, createTodo],
})

export const dailyPlanner = pikkuAIAgent({
name: 'daily-planner',
description: 'Plans your day and suggests tasks',
instructions: 'You help users plan their day.',
model: 'openai/gpt-4o-mini',
})

export const mainRouter = pikkuAIAgent({
name: 'main-router',
description: 'Routes requests to specialized agents',
instructions: 'You coordinate between agents.',
model: 'openai/gpt-4o-mini',
agents: [todoAssistant, dailyPlanner],
maxSteps: 5,
})

When the parent calls a sub-agent, it provides:

  • message: The task or question to send
  • session: A short label for thread continuity (reusing the same session continues the conversation)

Sub-agent calls are streamed through scoped channels, so the client receives events tagged with the sub-agent's name and session.

Conversation Memory​

Agents persist conversations in threads using an AIStorageService. Configure memory via the memory property:

export const agent = pikkuAIAgent({
name: 'my-agent',
// ...
memory: {
storage: 'aiStorage', // Name of the storage service in singletonServices
lastMessages: 10, // Number of recent messages to include in context
},
})
PropertyTypeDescription
storagestringKey of the AIStorageService in your singleton services (defaults to 'aiStorage')
lastMessagesnumberHow many recent messages to load from the thread (default: 20)
workingMemorySchemaEnable structured working memory with a JSON schema

Working Memory​

Working memory lets agents maintain structured state across conversation turns β€” like user preferences, task lists, or accumulated context. When enabled, the agent receives its current working memory in the system prompt and can update it by outputting <working_memory> tags:

export const agent = pikkuAIAgent({
name: 'my-agent',
// ...
memory: {
storage: 'aiStorage',
lastMessages: 10,
workingMemory: WorkingMemorySchema, // A Zod/JSON schema
},
})

The agent outputs partial JSON updates. Setting a field to null deletes it. Updates are deep-merged with the existing state and validated against the schema.

Streaming​

Agents stream responses through Pikku channels. The stream emits AIStreamEvent objects:

Event TypeDescription
text-deltaIncremental text token from the model
reasoning-deltaReasoning/thinking token (for models that support it)
tool-callAgent is calling a tool (includes name and args)
tool-resultTool execution result
agent-callParent agent is calling a sub-agent
agent-resultSub-agent has completed
approval-requestA tool requires user approval before execution
usageToken usage statistics for the current step
errorAn error occurred during execution
suspendedExecution suspended (e.g., missing RPC functions)
doneStream is complete

Events from sub-agents include agent and session fields so clients can track which agent produced which output.

Tool Approval (Human-in-the-Loop)​

You can require user approval before certain tools execute. This is configured via the requiresToolApproval option when streaming:

await streamAIAgent(
'my-agent',
{ message, threadId, resourceId },
channel,
params,
undefined,
{ requiresToolApproval: 'explicit' }
)
ModeBehavior
'explicit'Only tools marked with requiresApproval: true in their function metadata need approval (default)
'all'Every tool call requires approval
falseNo approval required

When a tool requires approval:

  1. The agent stream emits an approval-request event with the tool name, args, and a reason
  2. Execution suspends β€” the run state is saved with status: 'suspended'
  3. The client presents the approval request to the user
  4. The user approves or denies via resumeAIAgent()
  5. If approved, the tool executes and the agent continues; if denied, the agent receives a denial message and continues
// Resume after approval
await resumeAIAgent(
{ toolCallId: 'tc_123', approved: true },
channel,
params
)

Required Services​

AI Agents require three services in your singleton services:

ServiceInterfacePurpose
aiAgentRunnerAIAgentRunnerServiceExecutes the LLM (e.g., VercelAIAgentRunner from @pikku/ai-vercel)
aiStorageAIStorageServicePersists threads, messages, and working memory (e.g., PgAIStorageService from @pikku/pg)
aiRunStateAIRunStateServiceTracks agent run status and pending approvals (e.g., PgAIStorageService also implements this)

Setup Example​

import { PgAIStorageService } from '@pikku/pg'
import { VercelAIAgentRunner } from '@pikku/ai-vercel'
import { createOpenAI } from '@ai-sdk/openai'
import { createAnthropic } from '@ai-sdk/anthropic'
import postgres from 'postgres'

const sql = postgres(process.env.DATABASE_URL!)
const pgAiStorage = new PgAIStorageService(sql)
await pgAiStorage.init()

const providers = {
openai: createOpenAI({
apiKey: process.env.OPENAI_API_KEY!,
}),
anthropic: createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY!,
}),
}

const singletonServices = await createSingletonServices(config, {
aiStorage: pgAiStorage,
aiRunState: pgAiStorage,
aiAgentRunner: new VercelAIAgentRunner(providers),
})

The VercelAIAgentRunner from @pikku/ai-vercel uses the Vercel AI SDK under the hood, supporting any provider compatible with the AI SDK (OpenAI, Anthropic, Ollama, Google, etc.).

Model identifiers use provider/model format (e.g., openai/gpt-4o-mini, anthropic/claude-sonnet-4-20250514, ollama/llama3).

Model Configuration​

You can configure model aliases and per-agent overrides centrally:

// In your config
const modelConfig = {
models: {
fast: 'openai/gpt-4o-mini',
smart: { model: 'anthropic/claude-sonnet-4-20250514', temperature: 0.7 },
},
agentDefaults: {
temperature: 0.5,
maxSteps: 8,
},
agentOverrides: {
'my-agent': {
model: 'anthropic/claude-sonnet-4-20250514',
temperature: 0.3,
},
},
}

With model aliases, your agent definitions stay clean:

export const agent = pikkuAIAgent({
name: 'my-agent',
instructions: '...',
model: 'fast', // Resolves to openai/gpt-4o-mini
// ...
})

Resolution order for model settings: agent override > model alias > agent defaults > agent definition.

AI Middleware​

AI middleware hooks let you intercept and transform at three points in the agent execution:

const myMiddleware: PikkuAIMiddlewareHooks = {
// Before the LLM call β€” modify messages or instructions
modifyInput: async (services, { messages, instructions }) => {
return { messages, instructions: instructions + '\nBe concise.' }
},

// During streaming β€” filter or transform individual events
modifyOutputStream: async (services, { event, allEvents, state }) => {
if (event.type === 'text-delta') {
return event // Pass through
}
return null // Filter out
},

// After completion β€” modify the final text and messages
modifyOutput: async (services, { text, messages, usage }) => {
return { text: text.trim(), messages }
},
}

export const agent = pikkuAIAgent({
name: 'my-agent',
// ...
aiMiddleware: [myMiddleware],
})

Visualizing Agents in the Console​

The Pikku Console provides a built-in chat interface for your AI agents, displays agent metadata, and lets you test agents interactively. See Console Features for details.

Next Steps​

  • Channels: Learn about the streaming transport used by agents
  • Functions: Understand how Pikku functions work as agent tools
  • Middleware: Apply middleware to agent tool calls