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.

Nghe bài viết này dưới dạng podcast

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

Chưa có bình luận

Để lại bình luận

Bình luận sẽ được phê duyệt trước khi hiển thị.

Nhận bài viết mới

Mình sẽ gửi email khi có bài mới. Không spam.