My App

Overview & Philosophy

Introduction to Fragola

Fragola is a hyperextensible SDK for building AI agents in Typescript, it uses an event-drivent model and is built on top of openai-node. It gives you a small set of core primitives: Agents, Tools, Stores, Events and lets you use them individually or combine them inside hooks to create re-usable logic for your agents.

Why Fragola?

Fragola is designed to start small and grow to fit your project:

  • Event-driven core: Every stage of an agent's lifecycle (onUserMessage, onBeforeModelInvocation, onToolCall, etc.) is an explicit event you can observe, pause, or mutate. You always know what's in the conversation history and what state the agent is in (idle, generating, waiting).
  • Hyper-extensible via hooks: Features aren't baked into the core - they're hooks. You can drop in pre-built hook presets (like MCP support, multi-agent orchestration, or guardrails) or easily implement your own custom hooks. Hooks can be nested to keep complex logic organized and combined to enable highly customized agent behavior. Don't need orchestration? Don't add the hook. The core SDK stays lean regardless of how much you build on top of it.
  • Minimal building blocks: Agents, Tools, Stores, and Events compose directly, with no abstract chains or graphs to learn - just concrete pieces you wire together.

Architecture at a Glance

  1. Agent: The core loop - maintains state (messages, step count, execution status) and drives turn-by-turn interaction with the LLM.
  2. Events: Lifecycle interceptors that let you pause, mutate, or observe execution at any stage.
  3. Context: AgentContext exposes the agent's state, tools, and instructions to handlers, tools, and hooks.
  4. Stores: Reactive state containers (global, local, or scoped) for data outside the LLM context - retrieved documents, sessions, metrics - with reactive change tracking.
  5. Hooks: The extension mechanism. A hook registers event listeners (and optionally scoped stores) to package a feature as an independent, pluggable unit that modifies or extends the agent's behavior.

Installation & Setup

Get up and running with the Fragola SDK in your project.

Package Installation

Fragola is available on npm. You can install it using your preferred package manager.

npm install @fragola-ai/agent

Client Configuration

To use Fragola, you need to create an instance of the Fragola class. This instance manages the underlying OpenAI SDK and provides the shared global context for all agents created from it.

Basic Setup (OpenAI API Key)

By default, the underlying OpenAI client looks for the OPENAI_API_KEY environment variable.

import { Fragola } from "@fragola-ai/agent";

// Create a Fragola instance
// Ensure your OpenAI API key is set in your .env file as OPENAI_API_KEY
const fragola = new Fragola({
    model: "gpt-6-astra",
    // apiKey: "<your_api_key>" Alternatively add your apiKey without using .env
});

Advanced Setup (LiteLLM, different baseUrl, Custom SDK)

Because Fragola is built on top of the official openai-node SDK, the Fragola instance options are exactly the same as the OpenAI client constructor options (with some additions).

Example: Using LiteLLM

import { Fragola } from "@fragola-ai/agent";

// Using LiteLLM to proxy to Claude or other models
const fragola = new Fragola({
    model: "claude-3-5-sonnet", // The model configured in your LiteLLM proxy
    baseURL: "http://0.0.0.0:4000", // Your LiteLLM proxy URL
    apiKey: process.env.LITELLM_API_KEY || "your-litellm-api-key" // API key if required
});

Example: Using Ollama

import { Fragola } from "@fragola-ai/agent";

// Using a local Ollama model
const fragola = new Fragola({
    model: "llama3", // Your local Ollama model
    baseURL: "http://localhost:11434/v1", // Ollama's OpenAI-compatible endpoint
    apiKey: "ollama" // The SDK requires an API key string, even though Ollama doesn't enforce it
});

You can also provide a custom SDK constructor, as long as it extends OpenAI:

import OpenAI from "openai";
import { Fragola } from "@fragola-ai/agent";

class CustomOpenAI extends OpenAI {
    // Custom interceptors or behaviors
}

const fragola = new Fragola(
    { model: "gpt-4o-mini" }, 
    undefined, // globalStore
    CustomOpenAI // Custom SDK constructor
);

Environment Requirements & TypeScript Configuration

Fragola relies on modern JavaScript features. Ensure your environment meets these requirements:

  • Node.js: v18.0.0 or later (or Bun / Deno).
  • TypeScript: If using TypeScript, version 5.0 or later is recommended.

In your tsconfig.json, we recommend the following settings for the best experience (especially for strict type-checking of Zod schemas and metadata):

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true
  }
}

Quickstart

This guide will walk you through creating your first Fragola agent, sending it a user message, and inspecting its conversational state.

Creating Your First Agent

First, ensure you have initialized a Fragola instance. From there, you can create a new Agent.

An agent requires at minimum a name, a description, and system instructions.

import { Fragola } from "@fragola-ai/agent";

// 1. Initialize the Fragola SDK
// Ensure your OpenAI API key is set in your .env file as OPENAI_API_KEY
const fragola = new Fragola({
    model: "gpt-6-astra",
    // apiKey: "<your_api_key>" Alternatively add your apiKey without using .env
});

// 2. Create the Agent
const agent = fragola.agent({
    name: "CalculatorAgent",
    description: "A simple assistant that helps with math",
    instructions: "You are a helpful and concise math assistant. Always be polite."
});

Sending User Messages

To interact with the agent, use the agent.userMessage() method. This will append the user's prompt to the conversation history and trigger a turn of generation.

// Send a message to the agent and wait for its turn to complete
const state = await agent.userMessage({
    content: "What is the square root of 144?"
});

Note: Behind the scenes, Fragola automatically resolves tool calls, manages state transitions, and accumulates the message history until the model finishes its output.

Inspecting State & Conversation History

After an execution turn finishes, it returns the current AgentState. You can also access this state directly via agent.state.

The state contains:

  • messages: The full history of user, assistant, and tool messages.
  • status: The current execution status (idle, generating, or waiting).
  • stepCount: The number of LLM steps taken in the current execution.
// The state returned from userMessage() contains the full conversation
console.log(`Agent Status: ${state.status}`); // "idle"
console.log(`Total Steps Taken: ${state.stepCount}`); 

// Print the conversation history
for (const msg of state.messages) {
    console.log(`[${msg.role.toUpperCase()}]: ${msg.content}`);
}

Output:

Agent Status: idle
Total Steps Taken: 1
[USER]: What is the square root of 144?
[ASSISTANT]: The square root of 144 is 12.

Core Concepts

To build effectively with Fragola, it's helpful to understand the primary concepts and primitives that power the SDK.

Agents & Execution Turns

An Agent is the central loop. It holds its own isolated conversation history (messages), an execution status, and configuration (instructions, tools, model settings).

When you trigger an agent via agent.userMessage() or agent.step(), it enters an Execution Turn. During this turn, the agent will:

  1. Pass the prompt and tools to the LLM.
  2. Intercept and resolve any requested tool calls automatically.
  3. Keep looping (up to a maxStep limit) until the LLM provides a final text response.
  4. Return to an idle state and yield the updated state.
const agent = fragola.agent({
    name: "SearchAgent",
    description: "Agent that searches the web",
    instructions: "You are a helpful search assistant."
});

// Trigger an execution turn
const state = await agent.userMessage({ content: "Find the latest news" });
console.log(state.status); // "idle"

Events & Lifecycle Pipeline

Fragola is Event-Driven. Everything that happens inside the execution turn emits an event. Instead of fighting the framework, you can hook into explicit stages of the lifecycle:

  • Modify the user prompt before the turn starts (onUserMessage).
  • Change model settings dynamically before an API call (onBeforeModelInvocation).
  • Stream data to a UI in real-time (onModelInvocation, onAiMessage).
  • Rewrite tool parameters or handle tool errors gracefully (onBeforeToolCall, onAfterToolCall).

Events allow you to observe, mutate, or short-circuit (stop()) the agent's behavior cleanly.

agent.onUserMessage(({ message }) => {
    // Intercept and mutate the incoming message
    message.content = `${message.content} (Reply in Spanish)`;
    return message;
});

agent.onBeforeModelInvocation(({ config }) => {
    // Dynamically adjust temperature before calling the LLM
    config.temperature = 0.5;
    return config;
});

Agent Context & Instructions

Every agent exposes an AgentContext (agent.context). This context is passed into tool handlers and event callbacks.

The context provides safe access to:

  • Read or manipulate the agent's message history.
  • Manage dynamically scoped instructions (e.g., mixing a core persona with temporary rules).
  • Update the list of available tools mid-conversation.
  • Access local or global stores.
agent.onBeforeModelInvocation(({ context, config }) => {
    // Dynamically inject context-aware instructions
    context.instructions.add("temp-rule", "Always mention the current date.");
    return config;
});

Stores & State Management

Often, agents need to remember data that doesn't belong strictly inside the LLM prompt, like user IDs, retrieved database rows, or internal counters.

Stores (createStore(...)) are reactive state containers.

  • You can attach a Store globally (shared across all agents) or locally (scoped to one agent).
  • Stores support Scopes (formerly namespaces), allowing you to partition data (e.g., context.getStore("metrics")).
  • Because they are reactive, you can subscribe to store changes to trigger side effects in your app.
import { createStore, tool } from "@fragola-ai/agent";

const userStore = createStore({ userId: "123", name: "Alice" }, "user-data");
fragola.addStore(userStore); // Available to all agents on this instance

const greetUser = tool({
    name: "greetUser",
    description: "Greets the user by name",
    handler: (params, context) => {
        // Access the store safely from within a tool
        const userData = context.getStore("user-data")?.get();
        return `Hello ${userData?.name}!`;
    }
});

Hooks & Plugins

Hooks (FragolaHook) are Fragola's primary mechanism for composability and reuse. A hook is simply a function that receives an agent and attaches a mix of event listeners, stores, and tools to it, and returns an optional cleanup function.

If you find yourself writing the same safety checks, API logging, or tool-registration logic repeatedly, you package that logic into a Hook. When you call agent.use(myHook, "my-hook-id"), the agent absorbs all those behaviors. You can also dynamically remove hooks when they are no longer needed.

import { type FragolaHook, type FragolaEvent } from "@fragola-ai/agent";

// Define a custom hook
const withLogger: FragolaHook = (agent) => {
    let events: FragolaEvent[] = [];

    events.push(agent.onUserMessage(({ message }) => {
        console.log(`[USER]: ${message.content}`);
        return message;
    }));
    
    events.push(agent.onAiMessage(({ message }) => {
        console.log(`[AI]: ${message.content}`);
    }));

    // Return a cleanup function
    return () => {
        events.forEach(event => event.remove());
    }
};

// Apply the hook to the agent, providing a custom ID
agent.use(withLogger, "logger-hook");

On this page