Sistema de Agentes AI
Fox Framework v1.3 incorpora un sistema completo de agentes AI directamente en el core del framework. Permite construir agentes inteligentes que razonan, usan herramientas, mantienen memoria, y pueden orquestarse en sistemas multi-agente.
Arquitectura
tsfox/core/agents/
├── interfaces/ # IAgent, IModelProvider, ITool, IMemoryStore, IOrchestrator
├── errors/ # AgentError, MaxIterationsError, ToolExecutionError, ...
├── base/ # BaseAgent, InMemoryStore
├── react/ # ReActAgent
├── orchestrator/ # Orchestrator multi-agente
└── integrations/ # Event, Auth, Cache, MetricsInstalación
El sistema de agentes está incluido en @foxframework/core. Para usar un modelo LLM necesitas un provider:
npm install @foxframework/core
npm install @foxframework/model-openai # o model-anthropic, model-ollamaReActAgent
El ReActAgent implementa el loop Reasoning + Acting: el modelo razona qué hacer, elige una herramienta, observa el resultado, y repite hasta tener una respuesta final.
import { ReActAgent } from '@foxframework/core/agents';
import { OpenAIProvider } from '@foxframework/model-openai';
const agent = new ReActAgent({
model: new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY }),
tools: [searchTool, calculatorTool],
maxIterations: 10,
systemPrompt: 'You are a helpful research assistant.',
});
const result = await agent.run('What is the population of Tokyo?');
console.log(result.output);Definir una Tool
import { ITool } from '@foxframework/core/agents';
const searchTool: ITool = {
name: 'search',
description: 'Search the web for information',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' }
},
required: ['query']
},
execute: async ({ query }) => {
// tu lógica de búsqueda
return `Results for: ${query}`;
}
};Abortar una ejecución
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // timeout de 5s
const result = await agent.run('Long task...', { signal: controller.signal });Orchestrator
El Orchestrator coordina múltiples agentes. Usa el LLM para crear un plan de subtareas, las ejecuta en waves (respetando dependencias), y sintetiza el resultado final.
import { Orchestrator } from '@foxframework/core/agents';
import { ReActAgent } from '@foxframework/core/agents';
const orchestrator = new Orchestrator({
model: new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY }),
agents: { research: researchAgent, analysis: analysisAgent },
maxConcurrency: 3,
});
const result = await orchestrator.run(
'Research climate change and provide a summary with key statistics.'
);Ejecución por waves
Las subtareas con dependsOn se ejecutan después de que sus dependencias terminen. Las tareas sin dependencias corren en paralelo dentro de la misma wave:
Wave 1: [research_task] ← sin dependencias
Wave 2: [analysis_task] ← dependsOn: research_task
Wave 3: [summary_task] ← dependsOn: analysis_taskMemoria
Todos los agentes tienen acceso a un IMemoryStore. Por defecto usa InMemoryStore con búsqueda por palabras clave.
// Guardar en memoria
await agent.memory.set('user_preference', { theme: 'dark' });
// Recuperar
const pref = await agent.memory.get('user_preference');
// Búsqueda semántica por keyword
const results = await agent.memory.search('user preferences');Integraciones
Event Integration
Conecta el ciclo de vida del agente al sistema de eventos de Fox:
import { AgentEventBus, AgentEventSubscriber } from '@foxframework/core/agents';
// Emitir eventos del agente al bus
const eventedAgent = new AgentEventBus(agent, eventEmitter);
const result = await eventedAgent.run('task');
// Emite: agent.started, agent.completed, agent.failed, agent.tool_called
// Ejecutar agentes en respuesta a eventos
const subscriber = new AgentEventSubscriber(eventBus, agent);
subscriber.subscribe('user.message');Auth Integration
import { AuthenticatedAgent, AgentRateLimit } from '@foxframework/core/agents';
const secureAgent = new AuthenticatedAgent(agent, tokenValidator, {
requiredRoles: ['admin'],
});
// Rate limiting por userId
const rateLimited = new AgentRateLimit(agent, {
maxRequests: 10,
windowMs: 60_000,
});
const result = await secureAgent.run('task', {
authToken: 'Bearer eyJ...',
});Cache Integration
import { CachedAgent, InMemoryAgentCache } from '@foxframework/core/agents';
const cachedAgent = new CachedAgent(agent, new InMemoryAgentCache(), {
ttl: 5 * 60 * 1000, // 5 minutos
});
// Respuestas idénticas se sirven del cache (key = SHA-256 del input)Metrics Integration
import { AgentMetrics, AgentMetricsRegistry } from '@foxframework/core/agents';
const tracked = new AgentMetrics(agent);
await tracked.run('task');
console.log(tracked.metrics);
// {
// totalRuns: 1, successRuns: 1, failedRuns: 0,
// totalTokens: 342, avgLatency: 1240, p95Latency: 1240,
// lastRunAt: Date
// }
// Registro global de todos los agentes
const registry = new AgentMetricsRegistry();
registry.register('myAgent', tracked);
const report = registry.report();Interfaces
IAgent
interface IAgent {
readonly id: string;
readonly status: AgentStatus; // 'idle' | 'running' | 'completed' | 'failed' | 'aborted'
run(input: string, options?: RunOptions): Promise<AgentResult>;
abort(): void;
}IModelProvider
interface IModelProvider {
complete(messages: Message[], options?: CompletionOptions): Promise<CompletionResult>;
stream?(messages: Message[], options?: CompletionOptions): AsyncGenerator<string>;
}ITool
interface ITool {
definition: {
name: string;
description: string;
parameters: JSONSchema;
};
execute(params: Record<string, unknown>, context: AgentContext): Promise<string>;
}Built-in Tools (v1.4)
Fox Framework ships six ready-to-use tools in @foxframework/core/agents/tools. Import them individually:
import {
CalculatorTool,
HttpTool,
FilesystemTool,
createFilesystemTool,
JsonPathTool,
createSqlQueryTool,
createVectorSearchTool,
} from '@foxframework/core/agents/tools';CalculatorTool
Evaluates mathematical expressions safely (no eval). Supports arithmetic, **, %, and functions: sqrt, abs, ceil, floor, round, log, sin, cos, tan. Constants: PI, E.
const agent = new ReActAgent({
model,
tools: [CalculatorTool],
});
// Agent input: "What is sqrt(144) + 2**8?"
// Tool call: { expression: "sqrt(144) + 2**8" }
// Result: "sqrt(144) + 2**8 = 268"HttpTool
Makes HTTP requests (GET, POST, PUT, PATCH, DELETE) using native fetch. Returns formatted JSON or plain text.
const agent = new ReActAgent({
model,
tools: [HttpTool],
});
// Agent input: "Get the latest rates from https://api.exchangerate.host/latest"FilesystemTool
Reads, writes, appends, lists, and deletes files/directories. Use createFilesystemTool to restrict access to a safe directory:
// Unrestricted (default)
import { FilesystemTool } from '@foxframework/core/agents/tools';
// Restricted to a specific directory
const safeTool = createFilesystemTool({ allowedBase: '/tmp/agent-workspace' });
const agent = new ReActAgent({ model, tools: [safeTool] });Operations: read, write, append, list, exists, delete, mkdir.
JsonPathTool
Queries and extracts data from JSON using path expressions:
const agent = new ReActAgent({ model, tools: [JsonPathTool] });
// Tool call: { json: '{"users":[{"name":"Alice"}]}', path: ".users[0].name", operation: "get" }
// Result: '"Alice"'Operations: get, keys, count, flatten, pretty.
SqlQueryTool (factory)
Wraps any IQueryExecutor to let the agent run SQL. Read-only by default; set allowMutations: true to allow writes:
import { createSqlQueryTool } from '@foxframework/core/agents/tools';
import { PostgresProvider } from '@foxframework/db-postgres';
const db = new PostgresProvider({ connectionString: process.env.DATABASE_URL });
const sqlTool = createSqlQueryTool(db, {
allowMutations: false, // only SELECT (default)
maxRows: 50, // cap results (default: 100)
label: 'database', // tool name shown to the model
});
const agent = new ReActAgent({ model, tools: [sqlTool] });VectorSearchTool (factory)
Wraps any IVectorSearchProvider (Pinecone, Weaviate, Chroma, or custom) for semantic retrieval:
import { createVectorSearchTool } from '@foxframework/core/agents/tools';
import { PineconeProvider } from '@foxframework/vector-pinecone';
const vectorProvider = new PineconeProvider({
apiKey: process.env.PINECONE_API_KEY!,
indexHost: process.env.PINECONE_INDEX_HOST!,
embed: async (text) => myEmbeddingFn(text),
});
const vectorTool = createVectorSearchTool(vectorProvider, {
label: 'knowledge_search',
defaultTopK: 5,
});
const agent = new ReActAgent({ model, tools: [vectorTool] });Streaming (v1.4)
Stream agent steps to the browser over Server-Sent Events (SSE). Three layers of abstraction:
SseStream — low-level
Wraps any ServerResponse-compatible object and writes formatted SSE frames:
import { SseStream } from '@foxframework/core/agents/streaming';
app.get('/sse', (req, res) => {
const sse = new SseStream(res); // sets SSE headers automatically
sse.send({ event: 'message', data: { text: 'Hello!' } });
sse.close();
});AgentSseEmitter — mid-level
Runs an agent and streams every step as SSE events:
import { AgentSseEmitter } from '@foxframework/core/agents/streaming';
app.get('/agent/stream', async (req, res) => {
const emitter = new AgentSseEmitter(myAgent, res, {
heartbeatIntervalMs: 15_000, // keeps proxy connections alive (default: 15s)
});
await emitter.run(req.query.input as string);
});SSE events emitted:
| Event | Payload | When |
|---|---|---|
step | { stepNumber, type, content, toolCall?, toolResult? } | After each agent step |
done | { runId, answer, status, usage, stepCount } | Run completed |
error | { message } | Run threw an exception |
heartbeat | {} | Every heartbeatIntervalMs |
createAgentSseHandler — route factory
Creates a ready-to-use Express/Fox request handler:
import { createAgentSseHandler } from '@foxframework/core/agents/streaming';
// Default: reads input from req.query.input, req.body.input, or req.body.message
app.get('/stream', createAgentSseHandler(myAgent));
// Custom input extraction
app.post('/stream', createAgentSseHandler(myAgent, {
getInput: (req) => (req.body as any).question,
heartbeatIntervalMs: 10_000,
}));Client-side consumption
const es = new EventSource('/stream?input=What+is+the+capital+of+France%3F');
es.addEventListener('step', (e) => {
const step = JSON.parse(e.data);
console.log(`[${step.type}] ${step.content}`);
});
es.addEventListener('done', (e) => {
const result = JSON.parse(e.data);
console.log('Answer:', result.answer);
es.close();
});
es.addEventListener('error', (e) => {
console.error('Agent error:', JSON.parse(e.data).message);
es.close();
});Telemetry / Tracing (v1.4)
AgentTracer
AgentTracer is a proxy that wraps any IAgent and emits spans for every run and tool call. It is vendor-neutral — plug in any ITracer backend.
import { AgentTracer } from '@foxframework/core/agents/telemetry';
// With a no-op tracer (default — zero overhead)
const tracedAgent = new AgentTracer(myAgent);
// With a custom tracer
const tracedAgent = new AgentTracer(myAgent, { tracer: myTracer });Spans emitted:
| Span | Attributes |
|---|---|
agent.run | agent.id, agent.name, agent.input, run.id, run.status, run.stepCount, run.answer, llm.*_tokens |
agent.tool_call | tool.name, tool.call_id, tool.arguments, agent.id, step.number |
ITracer interface
Implement ITracer for any tracing backend:
import type { ITracer, ISpan } from '@foxframework/core/agents/telemetry';
class ConsoleTracer implements ITracer {
startSpan(name: string): ISpan {
const start = Date.now();
console.log(`[span start] ${name}`);
return {
setAttribute: (k, v) => { console.log(` ${k}=${v}`); return this as any; },
recordException: (e) => { console.error(' exception:', e); return this as any; },
setStatus: (s, m) => { console.log(` status=${s}`, m ?? ''); return this as any; },
end: () => console.log(`[span end] ${name} (${Date.now() - start}ms)`),
};
}
}
const tracedAgent = new AgentTracer(myAgent, { tracer: new ConsoleTracer() });OpenTelemetry via @foxframework/otel-agents
For production OTel tracing, install the adapter package:
npm install @foxframework/otel-agents @opentelemetry/api @opentelemetry/sdk-nodeimport { trace } from '@opentelemetry/api';
import { OtelAgentTracer } from '@foxframework/otel-agents';
import { AgentTracer } from '@foxframework/core/agents/telemetry';
// Assumes OTel SDK is already initialised (NodeSDK.start())
const tracer = new OtelAgentTracer(trace.getTracer('fox-agents', '1.0.0'));
const tracedAgent = new AgentTracer(myAgent, { tracer });
// All runs now emit OTel spans compatible with Jaeger, Zipkin, OTLP, etc.
const result = await tracedAgent.run('Summarise the quarterly report.');