flare-agent Bài 5: Core — Agent Loop và Tool Registry
Xây dựng @flare-agent/core — trái tim của framework với Agent class, AgentLoop xử lý tool calling, và ToolRegistry quản lý tools type-safe.
Tập này đang được chuẩn bị, quay lại sau nhé.
flare-agent Bài 5: Core — Agent Loop và Tool Registry
Series: Build Your Own AI Agent Framework trên Cloudflare Bài: 5 / 7 —
@flare-agent/core
Đây là trái tim của framework
@flare-agent/core là nơi mọi thứ được kết nối lại:
- LLMRouter từ
@flare-agent/providers - MemoryManager từ
@flare-agent/memory - ToolRegistry quản lý tools
- AgentLoop chạy vòng lặp tool calling
- Agent class — public API mà developer dùng
Cấu trúc package
packages/core/
src/
tool.ts # tool() helper, ToolRegistry
AgentLoop.ts # vòng lặp chính
Agent.ts # public API
stream.ts # SSE helpers
durable.ts # Durable Object wrapper
index.ts
Tool Registry
// src/tool.ts
import type { ToolDefinition, ToolSchema, AgentContext } from '@flare-agent/types';
// Helper tạo tool với type inference
export function tool<TArgs, TResult>(
definition: ToolDefinition<TArgs, TResult>
): ToolDefinition<TArgs, TResult> {
return definition;
}
export class ToolRegistry {
private tools = new Map<string, ToolDefinition>();
register(definition: ToolDefinition): this {
this.tools.set(definition.schema.name, definition);
return this;
}
registerMany(definitions: ToolDefinition[]): this {
definitions.forEach((d) => this.register(d));
return this;
}
get(name: string): ToolDefinition | undefined {
return this.tools.get(name);
}
getSchemas(): ToolSchema[] {
return [...this.tools.values()].map((t) => t.schema);
}
async execute(
name: string,
args: Record<string, unknown>,
ctx: AgentContext
): Promise<unknown> {
const toolDef = this.tools.get(name);
if (!toolDef) throw new Error(`Tool "${name}" not found in registry`);
return toolDef.execute(args, ctx);
}
}
Agent Loop
Đây là vòng lặp cốt lõi — while loop chạy đến khi LLM trả về text response hoặc hết max iterations:
// src/AgentLoop.ts
import type {
Message, AgentContext, AgentResult, LLMProvider
} from '@flare-agent/types';
import type { MemoryManager } from '@flare-agent/memory';
import type { ToolRegistry } from './tool';
interface LoopConfig {
systemPrompt: string;
provider: LLMProvider;
memory: MemoryManager;
tools: ToolRegistry;
maxIterations: number;
}
export class AgentLoop {
constructor(private config: LoopConfig) {}
async run(
userMessage: string,
ctx: AgentContext
): Promise<AgentResult> {
const { provider, memory, tools, maxIterations } = this.config;
const toolCallLog: AgentResult['toolCalls'] = [];
// Thêm user message vào memory
await memory.addMessage(ctx.sessionId, {
role: 'user',
content: userMessage,
});
const systemMessage: Message = {
role: 'system',
content: this.config.systemPrompt,
};
for (let i = 0; i < maxIterations; i++) {
const history = await memory.getMessages(ctx.sessionId);
const messages: Message[] = [systemMessage, ...history];
const toolSchemas = tools.getSchemas();
const response = await provider.chat(messages, toolSchemas);
// --- Tool call branch ---
if (response.type === 'tool_call' && response.toolCall) {
const { id, name, args } = response.toolCall;
let result: unknown;
let isError = false;
try {
result = await tools.execute(name, args, ctx);
} catch (err) {
result = { error: err instanceof Error ? err.message : String(err) };
isError = true;
}
toolCallLog.push({ name, args, result });
// Thêm tool result vào memory để LLM tiếp tục
await memory.addMessage(ctx.sessionId, {
role: 'tool',
content: JSON.stringify(result),
toolCallId: id,
});
// Nếu tool bị lỗi, dừng loop sớm
if (isError) break;
continue; // tiếp tục vòng lặp
}
// --- Final text response ---
const finalContent = response.content ?? '';
await memory.addMessage(ctx.sessionId, {
role: 'assistant',
content: finalContent,
});
return {
output: finalContent,
iterations: i + 1,
toolCalls: toolCallLog,
};
}
throw new Error(
`Agent exceeded max iterations (${maxIterations}). ` +
`Tool calls made: ${toolCallLog.map((t) => t.name).join(', ')}`
);
}
}
Agent Class — Public API
// src/Agent.ts
import type { AgentConfig, AgentContext, AgentResult } from '@flare-agent/types';
import { LLMRouter } from '@flare-agent/providers';
import { MemoryManager, KVMemoryAdapter, D1MemoryAdapter } from '@flare-agent/memory';
import { ToolRegistry, tool } from './tool';
import { AgentLoop } from './AgentLoop';
export class Agent {
private registry = new ToolRegistry();
constructor(private config: AgentConfig) {}
// Fluent API — chain tools
use(...tools: Parameters<ToolRegistry['register']>[0][]): this {
this.registry.registerMany(tools);
return this;
}
private buildProvider(env: Record<string, unknown>) {
const { provider, model } = this.config.model;
const router = new LLMRouter();
if (provider === 'groq') {
router.register('groq', {
apiKey: env.GROQ_API_KEY as string,
model,
});
} else if (provider === 'workersai') {
router.register('workersai', { ai: env.AI as Ai, model });
} else if (provider === 'ollama') {
router.register('ollama', {
baseUrl: env.OLLAMA_BASE_URL as string,
model,
});
}
return router.get(provider);
}
private buildMemory(env: Record<string, unknown>): MemoryManager {
const type = this.config.memory ?? 'kv';
return new MemoryManager({
shortTerm: type !== 'none'
? new KVMemoryAdapter(env.KV as KVNamespace)
: undefined,
longTerm: type === 'd1'
? new D1MemoryAdapter(env.DB as D1Database)
: undefined,
});
}
private getSystemPrompt(ctx: AgentContext): string {
const { systemPrompt } = this.config;
if (!systemPrompt) return 'You are a helpful assistant.';
if (typeof systemPrompt === 'function') return systemPrompt(ctx);
return systemPrompt;
}
// Run agent — main entry point
async run(input: string, ctx: AgentContext): Promise<AgentResult> {
const provider = this.buildProvider(ctx.env);
const memory = this.buildMemory(ctx.env);
const loop = new AgentLoop({
systemPrompt: this.getSystemPrompt(ctx),
provider,
memory,
tools: this.registry,
maxIterations: this.config.maxIterations ?? 10,
});
return loop.run(input, ctx);
}
// Convert sang Durable Object
toDurableObject() {
const agent = this;
return class extends DurableObject {
async fetch(request: Request): Promise<Response> {
const body = await request.json<{
input: string;
sessionId: string;
userId?: string;
}>();
const ctx: AgentContext = {
sessionId: body.sessionId,
userId: body.userId,
env: this.env as any,
};
const result = await agent.run(body.input, ctx);
return Response.json(result);
}
};
}
}
SSE Stream Helper
// src/stream.ts
export function createSSEResponse(
stream: ReadableStream<string>
): Response {
const encoder = new TextEncoder();
const sseStream = stream.pipeThrough(
new TransformStream<string, Uint8Array>({
transform(chunk, controller) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ text: chunk })}\n\n`)
);
},
flush(controller) {
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
},
})
);
return new Response(sseStream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
Dùng trong Worker
// apps/worker/src/index.ts
import { Hono } from 'hono';
import { Agent, tool } from '@flare-agent/core';
const vocabularyAgent = new Agent({
name: 'vocabulary-tutor',
model: { provider: 'groq', model: 'llama-3.3-70b-versatile' },
memory: 'd1',
systemPrompt: 'Bạn là trợ lý học từ vựng tiếng Anh. Hãy giúp người dùng học từ vựng hiệu quả.',
})
.use(
tool({
schema: {
name: 'get_word',
description: 'Lấy thông tin chi tiết về một từ vựng',
parameters: {
type: 'object',
properties: { word: { type: 'string' } },
required: ['word'],
},
},
execute: async ({ word }, ctx) => {
const db = ctx.env.DB as D1Database;
return db.prepare('SELECT * FROM vocabulary WHERE word = ?')
.bind(word).first();
},
})
);
export const AgentDO = vocabularyAgent.toDurableObject();
const app = new Hono();
app.post('/chat', async (c) => {
const { input, sessionId, userId } = await c.req.json();
const result = await vocabularyAgent.run(input, {
sessionId,
userId,
env: c.env as any,
});
return c.json(result);
});
export default app;
Checklist
- Tạo
packages/core/ - Implement
ToolRegistry,AgentLoop,Agent - Test agent loop với mock provider
- Verify tool calling flow end-to-end
Bài tiếp theo: Bài 6 — Workflow Engine: Graph-based Multi-step