# Agentica Documentation
## Overview
Agentica is a library for integrating agentic features into TypeScript applications.
## Basics
The primary method of interaction is through the `agentic()` function, which works like this:
```typescript
import { agentic } from '@symbolica/agentica';
// Defines the agent-backed function
async function add(a: number, b: number): Promise<number> {
return await agentic("Returns the sum of a and b", { a, b });
}
// Calls the agent-backed function
const result = await add(1, 2); // This addition is done by an LLM via the agentica framework
console.assert(result === 3);
```
This allows you to use agents to implement functions which are not possible to implement in pure TypeScript.
The alternative syntax is to `spawn` an agent.
```typescript
import { spawn } from '@symbolica/agentica';
// Spawns the agent
const agent = await spawn({ premise: "You are a truth-teller." });
// Calls the agent
const result: boolean = await agent.call<boolean>("The Earth is flat");
console.assert(result === false);
```
The creation and call of agents created with `spawn` are always awaitable.
### Return Types
Specify return types using TypeScript generics:
```typescript
// Simple call type inferred from explicit annotation
const result: number = await agent.call("What is 2+2?");
// Structured output
interface UserData {
name: string;
email: string;
role: string;
}
// Explicitly passed into generic argument
const user = await agent.call<UserData>("Get user information");
// user is typed as UserData
```
## Model Selection
Agentica supports any text-to-text model available on OpenRouter. Specify with the `model` parameter:
```typescript
// For agents (default is 'openai/gpt-4.1')
const agent = await spawn({ premise: "You are a helpful assistant.", model: "openai/gpt-5" });
// For agentic functions
async function analyze(text: string): Promise<AnalysisResult> {
return await agentic("Analyze the text", { text }, { model: "anthropic/claude-sonnet-4.5" });
}
```
**Supported models**:
Any OpenRouter model slug (e.g. `google/gemini-2.5-flash`).
## Token Limits
Control the maximum number of tokens generated with `maxTokens`:
```typescript
import { agentic, MaxTokens, spawn } from '@symbolica/agentica';
// For agents
const agent = await spawn({
premise: "Brief responses only",
maxTokens: 500 // Limit total output tokens per invocation
});
// For agentic functions
async function summarize(text: string): Promise<string> {
return await agentic("Create a concise summary", { text }, { maxTokens: 1000 });
}
// For finer control, use MaxTokens:
// - perInvocation: total tokens across all rounds
// - perRound: tokens per inference round
// - rounds: maximum number of inference rounds
const agent2 = await spawn({
premise: "Brief responses only",
maxTokens: MaxTokens.from({ perInvocation: 5000, perRound: 1000, rounds: 5 })
});
```
**Use cases**:
- Ensure brief responses for cost control
- Prevent overly long outputs
- Match specific output length requirements
If the response would exceed `maxTokens`, a `MaxTokensError` will be raised. See [Error Handling](#error-handling) for how to handle this.
## Tracking Token Usage
Track token consumption with `lastUsage()` and `totalUsage()` methods on agents:
```typescript
import { spawn } from '@symbolica/agentica';
const agent = await spawn({ premise: "You are helpful." });
await agent.call<string>("Hello!");
await agent.call<string>("How are you?");
// Get usage from the last invocation
const usage = agent.lastUsage();
console.log(`Input tokens: ${usage.inputTokens}`);
console.log(`Output tokens: ${usage.outputTokens}`);
console.log(`Cached tokens: ${usage.cachedTokens}`);
console.log(`Reasoning tokens: ${usage.reasoningTokens}`);
// Get cumulative usage across all invocations
const total = agent.totalUsage();
console.log(`Total: ${total.inputTokens} in, ${total.outputTokens} out, ${total.totalTokens} processed`);
// For agentic functions, use the onUsage callback
async function summarize(text: string): Promise<string> {
return await agentic("Summarize the text", { text }, {
onUsage: (usage) => console.log(`Used ${usage.totalTokens} tokens`)
});
}
```
The `Usage` object contains:
- `inputTokens`: tokens consumed as input
- `outputTokens`: tokens generated as output
- `totalTokens`: total tokens processed (not double-counting re-consumed tokens)
- `cachedTokens`: input tokens served from cache
- `reasoningTokens`: tokens used for internal reasoning
## Initial Prompt vs System Prompt
You can control the agent's instructions in two ways:
```typescript
// Use `premise` (added to default system prompt with environment explainer)
const agent = await spawn({ premise: "You are a math expert." });
// Use `system` for full control of the system prompt (no environment explainer)
const agent = await spawn({ system: "You are a helpful assistant. Always respond with JSON." });
```
**Note**: You cannot use both `premise` and `system` together.
## Passing in objects
If you want the agentic function or agent to use functions, classes, objects etc., simply pass them in the `scope` argument to `call` or `agentic`.
```typescript
import { agentic, spawn } from '@symbolica/agentica';
// User-defined function
import { webSearch } from './tools';
// Defines the agent with scope
const agent = await spawn({ premise: "You are a truth-teller." }, { webSearch });
// Defines the agent-backed function
async function truthTeller(statement: string): Promise<boolean> {
return await agentic(
"Returns whether or not this statement is True or False.",
{ statement, webSearch }
);
}
```
### SDK Integration Pattern
Extract specific methods from SDK clients for focused scope:
```typescript
import { agentic } from '@symbolica/agentica';
import { WebClient } from '@slack/web-api';
// Extract only the methods you need
const slackClient = new WebClient(process.env.SLACK_TOKEN);
const listUsers = slackClient.users.list.bind(slackClient.users);
const postMessage = slackClient.chat.postMessage.bind(slackClient.chat);
async function sendTeamUpdate(message: string): Promise<void> {
return await agentic(
"Send `message` to all team members",
{ message, listUsers, postMessage },
{ model: "openai/gpt-5" }
);
}
```
### Per-Call Scope
You can also add scope for specific invocations:
```typescript
const agent = await spawn({ premise: "Data analyzer" });
// Add resources for this specific call
const result: { [key: string]: number } = await agent.call(
"Analyze the dataset",
{ dataset, analyzerTool }, // Additional scope
);
```
## Agent Call Signatures
The `agent.call()` method has two signatures:
```typescript
// Simple call with just a prompt
const result = await agent.call<number>("What is 2+2?");
// Call with scope and configuration
const result = await agent.call<object>(
"Fetch and analyze user data",
{ databaseQuery, apiClient } // Additional scope
);
```
## Streaming
Streaming model generation is supported for both agents and agentic functions.
### Agent Streaming
```typescript
import { spawn } from '@symbolica/agentica';
// Enable streaming globally for the agent (default for all invocations)
const agent = await spawn({
premise: "You are a truth-teller.",
listener: (iid, chunk) => console.log(chunk.content)
});
// Override listener for this invocation specifically
const result = await agent.call<boolean>(
"Is Paris the capital of France?", {},
{ listener: (iid, chunk) => process.stdout.write(chunk.content) }
);
```
Each `Chunk` object contains:
- `content`: the text content of the chunk
- `role`: one of `'user'`, `'agent'`, or `'system'`
- `type`: optional — one of `'reasoning'`, `'output_text'`, `'code'`, `'usage'`, `'invocation_exit'`, or `undefined`
By default, usage chunks (`chunk.type === 'usage'`) are filtered out of the listener stream. To include them, pass `listenerIncludeUsage: true` in the spawn or call config.
### Agentic Function Streaming
Streaming is also supported for agentic functions:
```typescript
async function generateStory(topic: string): Promise<string> {
return await agentic("Write a story about the topic", { topic }, {
listener: (iid, chunk) => process.stdout.write(chunk.content)
});
}
```
## Resource Management
Agents should be properly cleaned up when done.
### Manual Cleanup
```typescript
const agent = await spawn({ premise: "Helper" });
try {
await agent.call<string>("Do something");
} finally {
await agent.close(); // Clean up resources
}
```
### Automatic Cleanup with `await using`
Modern TypeScript (5.2+) supports automatic resource disposal:
```typescript
await using agent = await spawn({ premise: "Helper agent" });
// Agent automatically cleaned up when out of scope
const result = await agent.call<string>("Process task");
// No need to call close(), automatically cleaned up at end of scope
```
## Error Handling
Agentica provides comprehensive error handling through the `@symbolica/agentica/errors` module.
### SDK Errors
All Agentica errors inherit from `AgenticaError`, making it easy to catch all SDK-related errors:
```typescript
import { agentic } from '@symbolica/agentica';
import { AgenticaError, RateLimitError, InferenceError } from '@symbolica/agentica/errors';
async function processData(data: string): Promise<object> {
return agentic<object>("Process the data.", { data });
}
try {
const result = await processData(rawData);
} catch (e) {
if (e instanceof RateLimitError) {
// Handle rate limiting
await sleep(60000);
result = await processData(rawData);
} else if (e instanceof InferenceError) {
// Handle all inference service errors
logger.error(`Inference failed: ${e}`);
result = {};
} else if (e instanceof AgenticaError) {
// Catch any other SDK errors
logger.error(`Agentica error: ${e}`);
throw e;
}
}
```
**Error hierarchy:**
- `AgenticaError` - Base for all SDK errors
- `ServerError` - Base for remote operation errors
- `GenerationError` - Base for agent generation errors
- `InferenceError` - HTTP errors from inference service
- `MaxTokensError`, `ContentFilteringError`, etc.
- `ConnectionError` - WebSocket and connection errors
- `InvocationError` - Agent invocation errors
### Custom Exceptions
You can define custom exceptions and pass them into the agentic function's scope so the agent can raise them:
```typescript
/**
* Raised when input data fails validation.
*/
class DataValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'DataValidationError';
}
}
/**
* Analyze the dataset.
*
* @throws {DataValidationError} If data is empty or malformed
* @throws {Error} If data format is not supported
*/
async function analyzeData(data: string): Promise<object> {
return agentic<object>(
`Analyze the dataset.
Throw DataValidationError if data is empty or malformed.
Throw Error if data format is not supported.
Return analysis results.`,
{ data, DataValidationError }
);
}
try {
const result = await analyzeData(rawData);
} catch (e) {
if (e instanceof DataValidationError) {
logger.warn(`Invalid data: ${e}`);
result = { status: "validation_failed" };
}
}
```
**Tip**: The agent can see your JSDoc comments (`/** ... */`)! Document exception conditions clearly using `@throws` tags, and also include them in your prompt. The agent will raise them appropriately.
## Common Patterns
### Stateful Conversations
Agents maintain context across calls:
```typescript
const assistant = await spawn({ premise: "You are a helpful coding assistant." });
// First interaction
await assistant.call<void>("My name is Alice and I'm learning TypeScript");
// Later interaction - agent remembers context
const response = await assistant.call<string>("What's my name?");
console.log(response); // "Your name is Alice"
```
### Custom Agent Classes
Wrap agents for domain-specific functionality:
```typescript
import { Agent, spawn } from '@symbolica/agentica';
class ResearchAssistant {
private agent: Agent | null = null;
constructor(private webSearch: (query: string) => Promise<any>) { }
async initialize(): Promise<void> {
this.agent = await spawn(
{ premise: "You are a research assistant." },
);
}
async research(topic: string): Promise<string> {
return await this.agent!.call<string>(
"Research the topic",
{ topic, webSearch: this.webSearch }
);
}
async summarize(text: string): Promise<string> {
return await this.agent!.call<string>(
"Summarize this text",
{ text }
);
}
async close(): Promise<void> {
await this.agent?.close();
}
}
// Usage
const researcher = new ResearchAssistant(myWebSearchFn);
await researcher.initialize();
const findings = await researcher.research("AI agents in 2025");
const summary = await researcher.summarize(findings);
await researcher.close();
```
### Resource Management Pattern
Modern pattern for automatic cleanup:
```typescript
async function processTask(task: string): Promise<string> {
await using agent = await spawn({
premise: "Task processor",
model: "openai/gpt-5"
});
const result = await agent.call<string>(task);
return result;
// Agent automatically cleaned up here
}
```
## Multi-Agent Orchestration
Coordinate multiple agents for complex tasks by wrapping agents in custom classes.
### Pattern: Lazy Agent Initialization
Defer agent creation until first use:
```typescript
import { Agent, spawn } from '@symbolica/agentica';
class ResearchAgent {
private brain: Agent | null = null;
private async ensureBrain(): Promise<Agent> {
if (this.brain === null) {
this.brain = await spawn({
premise: "You are a research assistant.",
model: "openai/gpt-4o"
});
}
return this.brain;
}
async research(topic: string): Promise<string> {
const brain = await this.ensureBrain();
// Bind instance methods for agent scope
const webSearch = this.webSearch.bind(this);
return await brain.call<string>(
"Research the topic",
{ topic, webSearch } // Pass bound methods
);
}
private async webSearch(query: string): Promise<any> {
// [implementation omitted]
return { results: [] };
}
async close(): Promise<void> {
if (this.brain !== null) {
await this.brain.close();
}
}
}
```
### Pattern: Multi-Agent Coordination
Coordinate multiple specialized agents:
```typescript
class CitationAgent { ... }
class DeepResearchSession {
private leadResearcher: Agent | null = null;
private citationAgent: CitationAgent;
constructor(private directory: string) {
this.citationAgent = new CitationAgent(directory);
}
private async ensureLeadResearcher(): Promise<Agent> {
if (this.leadResearcher === null) {
this.leadResearcher = await spawn({
premise: "You are a lead researcher. Coordinate subagents to research the query.",
model: "openai/gpt-4o"
});
}
return this.leadResearcher;
}
async run(query: string): Promise<string> {
const leader = await this.ensureLeadResearcher();
// Lead researcher can spawn and coordinate SubAgent instances
const report: string = await leader.call(
query,
{
SubAgent, // Pass the class itself
savePlan: (plan: string) => this.savePlan(plan),
loadPlan: () => this.loadPlan()
},
{ listener: (iid, chunk) => process.stdout.write(chunk.content) }
);
// Post-process with citation agent
await this.citationAgent.run(report);
return `Research complete! Check ${this.directory}/report.md`;
}
private async savePlan(plan: string): Promise<void> {
// Save to directory
}
private async loadPlan(): Promise<string> {
// Load from directory
return "";
}
async close(): Promise<void> {
if (this.leadResearcher !== null) {
await this.leadResearcher.close();
}
await this.citationAgent.close();
}
}
// Usage
const session = new DeepResearchSession('research_output');
try {
const result = await session.run("Research AI agents in 2025");
console.log(result);
} finally {
await session.close();
}
```
### Pattern: SubAgent with Instance Counter
Create multiple agent instances with unique IDs:
```typescript
let idGen = 0;
class SubAgent {
id: number;
private brain: Agent | null = null;
constructor(private directory: string) {
this.id = idGen++; // Unique ID per instance
}
private async ensureBrain(): Promise<Agent> {
if (this.brain === null) {
this.brain = await spawn({
premise: "You are a specialized research agent.",
model: "openai/gpt-4o"
});
}
return this.brain;
}
async run(task: string): Promise<string> {
const brain = await this.ensureBrain();
// Bind methods for scope
const saveResults = this.saveResults.bind(this);
const result: string = await brain.call(
task,
{ saveResults },
{ listener: (iid, chunk) => process.stdout.write(`[SubAgent ${this.id}] ${chunk.content}`) }
);
return result;
}
private async saveResults(data: any): Promise<void> {
// Save with unique path using this.id
const path = `${this.directory}/subagent_${this.id}/results.json`;
// ... save logic
}
async close(): Promise<void> {
if (this.brain !== null) {
await this.brain.close();
}
}
}
```
### Best Practices for Multi-Agent Systems
1. **Lazy Initialization**: Create agents only when needed using `ensureBrain()` pattern
2. **Resource Cleanup**: Always implement `close()` and call it in `finally` blocks
3. **Method Binding**: Use `.bind(this)` when passing instance methods to agent scope
4. **Unique IDs**: Use counters or UUIDs for agent identification
5. **Streaming Coordination**: Stream nested agent outputs for visibility
6. **Error Handling**: Wrap agent calls in try/finally for proper cleanup
7. **Type Safety**: Use TypeScript generics for type-safe agent responses
```typescript
// Complete example with best practices
class RobustAgentWrapper {
private agent: Agent | null = null;
private async ensureAgent(): Promise<Agent> {
if (this.agent === null) {
this.agent = await spawn({
premise: "Task executor",
model: "openai/gpt-5"
});
}
return this.agent;
}
async execute<T>(task: string, tools: object = {}): Promise<T> {
const agent = await this.ensureAgent();
try {
const result: T = await agent.call<T>(
task,
tools,
{ listener: (iid, chunk) => console.log(chunk.content) }
);
return result;
} catch (error) {
console.error('Agent execution failed:', error);
throw error;
}
}
async close(): Promise<void> {
if (this.agent) {
await this.agent.close();
this.agent = null;
}
}
}
// Usage with automatic cleanup
async function processTask(): Promise<string> {
const wrapper = new RobustAgentWrapper();
try {
const result = await wrapper.execute<string>("Analyze data");
return result;
} finally {
await wrapper.close();
}
}
```
## Logging and Debugging
<Note>
**More advanced listener support on par with Python is coming soon to TypeScript.**
Logging and listener functionality is currently restricted to callback-style listeners in TypeScript.
</Note>
### Current Debugging Options
For now, you can observe agent behavior through:
```typescript
// 1. Streaming responses for real-time observation
const result = await agent.call<string>("Complex task", {}, {
listener: (iid, chunk) => console.log('[Agent]:', chunk.content)
});
// 2. Standard console logging
console.log('Agent returned:', result);
```
Happy programming!
## Content quality standards
- Always include complete, runnable examples that users can copy and execute
- Show proper error handling and edge case management
- Add explanatory comments for complex logic