@aeye/core
v0.3.2
Published
@aeye Core - Core primitives for AI components (agents, tools, prompts)
Maintainers
Readme
@aeye/core
Core primitives for building AI agents, tools, and prompts with TypeScript. Provides a type-safe, composable framework for creating sophisticated AI applications with structured inputs/outputs, tool calling, and context management.
Features
- 🎯 Type-Safe Components - Prompts, Tools, and Agents with full TypeScript support
- 🔧 Tool Calling - Native support for function/tool calling with schema validation
- 📝 Template-Based Prompts - Handlebars templates for dynamic prompt generation
- ✅ Schema Validation - Zod integration for structured inputs and outputs
- 🌊 Streaming Support - First-class streaming for real-time AI responses
- 🔄 Composable Architecture - Tools can use other tools, prompts can use tools, agents orchestrate everything
- 📊 Context Management - Type-safe context threading and automatic token window management
- 🎛️ Flexible Execution - Sequential, parallel, or immediate tool execution modes
Installation
npm install @aeye/core zod handlebarsCore Concepts
Components
All AI primitives implement the Component interface:
- Prompt - Generates AI responses with optional tool usage and structured outputs
- Tool - Extends AI capabilities with custom functions and external integrations
- Agent - Orchestrates complex workflows combining prompts and tools
Context & Metadata
- Context (
TContext) - Application-specific data threaded through operations (user, db, etc.) - Metadata (
TMetadata) - Execution settings for AI requests (model, temperature, etc.)
Quick Start
Basic Prompt
import { Prompt } from '@aeye/core';
import z from 'zod';
const summarizer = new Prompt({
name: 'summarize',
description: 'Summarizes text concisely',
content: 'Summarize the following text:\n\n{{text}}',
// Transform input
input: (input: { text: string }) => ({ text: input.text }),
// Define output schema
schema: z.object({
summary: z.string().describe('A concise summary'),
keyPoints: z.array(z.string()).describe('Main points')
})
});
// Execute with a context that has an executor
const result = await summarizer.get(
'result',
{ text: 'Long article text...' },
{
execute: yourAIExecutor, // Executor function from a provider
messages: []
}
);
console.log(result.summary);
console.log(result.keyPoints);Creating Tools
import { Tool } from '@aeye/core';
import z from 'zod';
const weatherTool = new Tool({
name: 'getWeather',
description: 'Get current weather for a location',
instructions: 'Use this tool to get weather data',
schema: z.object({
location: z.string().describe('City name or coordinates'),
units: z.enum(['celsius', 'fahrenheit']).default('celsius')
}),
call: async (input, refs, ctx) => {
const response = await fetch(
`https://api.weather.com/v1/${input.location}`
);
const data = await response.json();
return {
temperature: data.temp,
condition: data.condition,
humidity: data.humidity
};
}
});Prompts with Tools
const travelAdvisor = new Prompt({
name: 'travelAdvisor',
description: 'Provides travel advice based on weather',
content: `You are a travel advisor. Help plan a trip to {{destination}}.
Use the weather tool to check current conditions, then provide:
- What to pack
- Activities to do
- Best times to visit`,
input: (input: { destination: string }) => ({
destination: input.destination
}),
tools: [weatherTool],
schema: z.object({
recommendations: z.array(z.string()),
packingList: z.array(z.string()),
weatherNotes: z.string()
})
});
// The AI will automatically call weatherTool if needed
const advice = await travelAdvisor.get(
'result',
{ destination: 'Paris' },
{ execute: yourAIExecutor, messages: [] }
);Streaming Responses
// Stream content only
for await (const chunk of summarizer.get(
'streamContent',
{ text: 'Long text...' },
{ stream: yourAIStreamer, messages: [] }
)) {
process.stdout.write(chunk);
}
// Stream all events (including tool calls)
for await (const event of summarizer.get(
'stream',
{ text: 'Long text...' },
{ stream: yourAIStreamer, messages: [] }
)) {
if (event.type === 'textPartial') {
console.log('Text:', event.content);
} else if (event.type === 'toolStart') {
console.log('Tool started:', event.tool.name);
} else if (event.type === 'toolOutput') {
console.log('Tool result:', event.result);
}
}Building Agents
import { Agent } from '@aeye/core';
const researchAgent = new Agent({
name: 'researcher',
description: 'Conducts research on topics',
refs: [searchTool, summarizeTool, analyzeTool],
call: async (input: { topic: string }, [search, summarize, analyze], ctx) => {
// Step 1: Search for information
const searchResults = await search.run(
{ query: input.topic, limit: 5 },
ctx
);
// Step 2: Summarize findings
const summaries = [];
for (const result of searchResults) {
const summary = await summarize.get(
'result',
{ text: result.content },
ctx
);
summaries.push(summary);
}
// Step 3: Analyze and synthesize
const analysis = await analyze.get(
'result',
{ topic: input.topic, sources: summaries },
ctx
);
return analysis;
}
});
const research = await researchAgent.run(
{ topic: 'Quantum Computing' },
{ execute: yourAIExecutor, messages: [] }
);Prompt Modes
The get method supports different execution modes:
| Mode | Description | Returns |
|------|-------------|---------|
| 'result' | Get final structured output | TOutput |
| 'tools' | Get tool call results | PromptToolOutput[] |
| 'stream' | Stream all events | AsyncGenerator<PromptEvent> |
| 'streamTools' | Stream tool outputs | AsyncGenerator<PromptToolOutput> |
| 'streamContent' | Stream text content only | AsyncGenerator<string> |
// Get structured result
const result = await prompt.get('result', input, ctx);
// Get tool outputs only
const tools = await prompt.get('tools', input, ctx);
// Stream everything
for await (const event of prompt.get('stream', input, ctx)) {
// Handle different event types
}Tool Execution Modes
Control how tools are executed:
const prompt = new Prompt({
name: 'multi-tool',
description: 'Uses multiple tools',
content: 'Analyze the data',
tools: [tool1, tool2, tool3],
// Tool execution mode
toolExecution: 'parallel', // 'sequential' | 'parallel' | 'immediate'
// Retry configuration
toolRetries: 2, // Retry failed tools
toolIterations: 3, // Max iterations for tool calls
toolsMax: 5, // Max total tool calls
});sequential- Wait for each tool to finish before continuingparallel- Start all tools at once, wait for all to completeimmediate- Start tools immediately as they're available
Advanced Features
Context-Aware Tools
const contextTool = new Tool({
name: 'contextAware',
description: 'A context-aware tool',
instructions: 'Use this tool...',
// Schema can depend on context
schema: (ctx) => {
if (ctx.userRole === 'admin') {
return z.object({
action: z.enum(['read', 'write', 'delete'])
});
}
return z.object({
action: z.enum(['read'])
});
},
// Check if tool is applicable
applicable: (ctx) => {
return ctx.isAuthenticated === true;
},
call: async (input, refs, ctx) => {
// Implementation with context access
}
});Custom Validation
const validatedTool = new Tool({
name: 'placeOrder',
description: 'Place an order',
schema: z.object({
itemId: z.string(),
quantity: z.number().min(1)
}),
// Additional validation beyond schema
validate: async (input, ctx) => {
const inventory = await checkInventory(input.itemId);
if (inventory < input.quantity) {
throw new Error(`Only ${inventory} items available`);
}
},
call: async (input, refs, ctx) => {
// Place order
}
});Event Tracking
import { withEvents } from '@aeye/core';
const runner = withEvents({
onStatus: (instance) => {
console.log(`${instance.component.name}: ${instance.status}`);
if (instance.status === 'completed') {
const duration = instance.completed - instance.started;
console.log(`Took ${duration}ms`);
}
},
onPromptEvent: (instance, event) => {
if (event.type === 'usage') {
console.log('Tokens used:', event.usage);
}
}
});
const result = await prompt.get(
'result',
{ text: 'Hello' },
{
execute: yourAIExecutor,
messages: [],
runner
}
);Token Management
Automatic conversation trimming when token limits are reached:
const context = {
execute: yourAIExecutor,
messages: conversationHistory,
// Token configuration
defaultCompletionTokens: 2048,
// Custom token estimation
estimateTokens: (message) => {
return message.content.length / 4; // Rough estimate
}
};
// Prompt will automatically trim messages if needed
const result = await prompt.get('result', { text: 'Query' }, context);Dynamic Reconfiguration
const adaptivePrompt = new Prompt({
name: 'adaptive',
description: 'Adapts based on execution stats',
content: 'Solve: {{problem}}',
schema: z.object({ solution: z.string() }),
// Adjust configuration during execution
reconfig: (stats, ctx) => {
// If tools are failing, change strategy
if (stats.toolCallErrors > 3) {
return {
config: { toolsOneAtATime: true },
maxIterations: 2
};
}
return {};
}
});API Reference
Prompt
class Prompt<TContext, TMetadata, TName, TInput, TOutput, TTools>Constructor Options:
name: string- Unique identifierdescription: string- Purpose descriptioncontent: string- Handlebars templateinput?: Function- Transform input for templateschema?: ZodType | Function- Output schemaconfig?: Partial<Request> | Function- AI request configtools?: Tool[]- Available toolstoolExecution?: 'sequential' | 'parallel' | 'immediate'toolRetries?: number- Retry failed toolstoolIterations?: number- Max tool call iterationsoutputRetries?: number- Retry invalid outputsmetadata?: TMetadata- Execution metadatavalidate?: Function- Post-validation hookapplicable?: Function- Applicability check
Methods:
get(input, mode, ctx)- Execute and retrieve outputrun(input, ctx)- Execute with full streamingapplicable(ctx)- Check if prompt can run
Tool
class Tool<TContext, TMetadata, TName, TParams, TOutput, TRefs>Constructor Options:
name: string- Unique identifierdescription: string- Purpose descriptioninstructions?: string- Handlebars usage instructionsschema: ZodType | Function- Input schemarefs?: Component[]- Referenced componentscall: Function- Implementationvalidate?: Function- Post-validation hookapplicable?: Function- Applicability check
Methods:
run(input, ctx)- Execute the toolcompile(ctx)- Generate tool definition for AIparse(ctx, args)- Parse and validate argumentsapplicable(ctx)- Check if tool can run
Agent
class Agent<TContext, TMetadata, TName, TInput, TOutput, TRefs>Constructor Options:
name: string- Unique identifierdescription: string- Purpose descriptionrefs?: Component[]- Referenced componentscall: Function- Implementation with refsapplicable?: Function- Applicability check
Methods:
run(input, ctx)- Execute the agentapplicable(ctx)- Check if agent can run
Context Structure
interface Context<TContext, TMetadata> {
// Required: AI execution
execute?: Executor<TContext, TMetadata>;
stream?: Streamer<TContext, TMetadata>;
// Messages
messages: Message[];
// Token management
defaultCompletionTokens?: number;
estimateTokens?: (message: Message) => number;
// Execution control
signal?: AbortSignal;
runner?: Events;
// User context (TContext) spreads here
[key: string]: any;
}Request Configuration
interface Request {
messages: Message[];
temperature?: number; // 0-2
topP?: number; // 0-1
maxTokens?: number;
stop?: string | string[];
tools?: ToolDefinition[];
toolChoice?: 'auto' | 'required' | 'none' | { tool: string };
responseFormat?: 'text' | 'json' | ResponseFormat;
// ... more options
}Best Practices
- Type Safety - Use TypeScript generics for context and metadata
- Schema Validation - Use Zod for robust input/output validation
- Error Handling - Wrap component execution in try-catch blocks
- Token Management - Provide
estimateTokensfor accurate trimming - Tool Organization - Group related tools in agents
- Testing - Unit test tools and prompts independently
- Documentation - Document template variables and schemas
- Context Minimization - Pass only necessary data in context
- Streaming - Use streaming for better UX with long responses
- Validation - Use
validatehooks for business logic validation
Examples
See the src/__tests__ directory for comprehensive examples:
- prompt-core-features.test.ts - Basic prompt usage
- prompt-streaming-tool-events.test.ts - Streaming and events
- tool.test.ts - Tool creation and usage
- agent.test.ts - Agent orchestration
- context-propagation.test.ts - Context handling
TypeScript Support
Full type inference across component hierarchies:
// Types are automatically inferred
const prompt = new Prompt({
name: 'example',
schema: z.object({ result: z.string() })
// ...
});
// TypeScript knows the output type
const output = await prompt.get({}); // { result: string }
// Tool parameters are type-safe
const tool = new Tool({
name: 'math',
schema: z.object({ a: z.number(), b: z.number() }),
call: (input, refs, ctx) => {
// input is typed as { a: number, b: number }
return input.a + input.b;
}
});Contributing
Contributions are welcome! See the main @aeye repository for contribution guidelines.
License
GPL-3.0 © ClickerMonkey
