My App

Creating & Configuring Agents

Agents are the central primitive in Fragola. You create them using the fragola.agent() method. Each agent maintains its own isolated state, messages, and configuration.

Basic Configuration

When creating an agent, you provide basic metadata and system instructions.

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

const fragola = new Fragola({ model: "gpt-6-astra" });

const agent = fragola.agent({
    name: "CustomerSupportBot",
    description: "Handles initial customer inquiries and routing.",
    instructions: "You are a polite customer support agent. Be concise.",
});

Options Breakdown

  • name: A string identifying the agent.
  • description: A brief description of what the agent does. Useful when another agent (like an orchestrator) needs to know when to delegate to it.
  • instructions: The system prompt for the agent. This can be a string, an array of strings, or omitted if you prefer to set them dynamically later.
  • useDeveloperRole: (Optional, defaults to false) Instructs the SDK to use the developer role for system instructions, which is favored by some newer OpenAI models (like o1 and o3).

Model Settings

By default, an agent inherits the model and settings from the Fragola instance. However, you can override these on a per-agent basis using modelSettings.

const mathAgent = fragola.agent({
    name: "MathGenius",
    instructions: "Solve complex math problems step-by-step.",
    modelSettings: {
        model: "o3-mini", // Override the instance-level model
        temperature: 0.1, // Lower temperature for more deterministic output
        max_tokens: 4000,
        top_p: 0.9,
    }
});

Any standard OpenAI completion parameters (e.g., frequency_penalty, presence_penalty, logit_bias) can be provided in modelSettings.

Seeding Initial Message History

Sometimes you want an agent to start with an existing conversation history—for example, when restoring a session from a database, or when pre-filling context.

You can do this using the messages array in the agent configuration:

const returningUserAgent = fragola.agent({
    name: "ReturningUserAgent",
    instructions: "You are a helpful assistant.",
    messages: [
        { role: "user", content: "Hi, I need help with my account." },
        { role: "assistant", content: "Hello! I'd be happy to help. Can you provide your account ID?" }
    ]
});

Note: The instructions provided in the configuration are automatically prepended to the message history as a system (or developer) message. You do not need to manually include the system prompt in the messages array.

Execution & Stepping

Once an agent is configured, you interact with it by triggering an execution turn. An execution turn is a loop where the agent calls the model, resolves any tool calls automatically, and repeats until the model produces a final response.

agent.userMessage(...)

The most common way to trigger an agent is by sending a user message. This appends the user's prompt to the conversation history and immediately starts an execution turn.

const state = await agent.userMessage({
    content: "What is the capital of France?"
});

console.log(state.messages[state.messages.length - 1].content); 
// "The capital of France is Paris."

Multi-part User Messages

The content of a user message isn't limited to a simple string. It supports multi-part arrays, allowing you to pass text alongside images or other attachments supported by the model.

const state = await agent.userMessage({
    content: [
        { type: "text", text: "What is in this image?" },
        { type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
    ]
});

agent.step(...)

If you want to trigger the agent without appending a new user message, use agent.step(). This is useful when the agent already has enough context to act, or if a previous turn hit a token limit or step limit and you want to resume generation.

// Assume the agent was previously populated with messages or hit a limit
const state = await agent.step();

Step Options & Limits

By default, Fragola limits execution turns to 5 consecutive LLM calls (steps) to prevent infinite loops (e.g., when a tool keeps failing and the model keeps retrying).

You can configure these limits and behaviors when creating the agent, or override them at runtime.

Configuration Defaults

const agent = fragola.agent({
    name: "ResearchAgent",
    instructions: "Research topics deeply using your tools.",
    // Configure default stepping behavior:
    maxStep: 10, 
    resetStepCountAfterUserMessage: true // True by default
});
  • maxStep: The maximum number of LLM invocations allowed in a single userMessage or step call before the SDK forcefully returns the state.
  • resetStepCountAfterUserMessage: If true, the internal stepCount resets to 0 every time you call userMessage(). If false, the agent has a total lifetime step budget.

Runtime Overrides

You can override stepping options for a single execution using the by parameter. This defines how many additional steps the agent is allowed to take during this specific call.

// Allow the agent to take up to 20 steps to complete this specific complex task
const state = await agent.userMessage(
    { content: "Perform a comprehensive analysis of..." },
    { by: 20 }
);

// Manually advance by exactly 1 step (e.g., for debugging or precise control)
const nextState = await agent.step({ by: 1 });

Structured Outputs (agent.json)

Fragola makes it easy to force the agent to return data matching a specific schema using agent.json(). This is incredibly useful for extraction tasks, data formatting, or anytime you need deterministic, machine-readable output rather than free-text.

Using Zod Schemas

To request structured data, pass a prompt and a Zod schema to agent.json(). Fragola utilizes OpenAI's structured outputs and tool-calling capabilities under the hood to ensure the response adheres to the schema.

import { z } from "zod";

const UserSchema = z.object({
    name: z.string(),
    age: z.number(),
    tags: z.array(z.string())
});

const result = await agent.json({
    message: "Extract info from: John Doe is 30 years old and likes skiing and coding.",
    schema: UserSchema
});

Handling Validation Results

The result returned by agent.json() is a safe-parse object. It contains a success boolean that dictates whether the output successfully validated against your schema.

if (result.success) {
    // result.data is strictly typed as { name: string, age: number, tags: string[] }
    console.log("Extracted User:", result.data.name);
} else {
    // The model failed to produce valid JSON matching the schema
    console.error("Validation failed:", result.error);
}

Advanced Options

Bypassing User Message Events

By default, the message string you pass to agent.json() triggers the standard onUserMessage lifecycle events. If you want this extraction to be "stealthy" (e.g., bypassing moderation hooks or custom event mutators), you can disable those events:

const result = await agent.json({
    message: "Extract the data...",
    schema: DataSchema,
    ignoreUserMessageEvents: true
});

Error Handling with JsonModeError

If a fundamental error occurs during the structured output generation—such as the model completely failing to produce parseable JSON, or an API error—Fragola will throw a JsonModeError.

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

try {
    const result = await agent.json({
        message: "Extract data...",
        schema: MySchema
    });
} catch (err) {
    if (err instanceof JsonModeError) {
        console.error("Critical failure during JSON generation:", err.message);
    }
}

State & Lifecycle Management

Fragola agents hold their own isolated state, tracking the entire conversation history, internal step counters, and their current execution status.

Agent State Object

You can access the current state of an agent at any time via agent.state. It contains three primary properties:

const currentState = agent.state;

console.log(currentState.messages);  // The array of all conversation messages
console.log(currentState.stepCount); // Current number of LLM invocations in this turn
console.log(currentState.status);    // 'idle', 'generating', or 'waiting'

Status Transitions

An agent is a state machine that transitions through specific statuses during an execution turn:

  1. idle: The agent is not doing anything. It is waiting for userMessage() or step() to be called.
  2. generating: The agent has initiated an execution turn and is actively communicating with the LLM or resolving tools.
  3. waiting: The execution has paused (e.g., waiting for Human-in-the-Loop approval or explicit resumption) and will not proceed until instructed to.

Resetting the Agent

If you want to clear an agent's history and start fresh, you can reset its state.

// Clears the message history and resets the stepCount to 0
agent.reset();

// You can also provide an array of messages to seed the new state
agent.reset([
    { role: "user", content: "Let's start over." }
]);

If you only need to reset the step budget without clearing the conversation history, you can do so directly:

agent.resetStepCount();

Stopping Executions

Because executions can loop automatically (when resolving multiple tool calls sequentially), you may need to stop the agent prematurely based on external events, tool failures, or moderation hooks.

agent.stop()

Stops the agent gracefully at the end of the current asynchronous operation. The agent will finish its current task (like saving a tool result) and then exit the loop, returning to an idle state.

agent.onToolCall(({ name }) => {
    if (name === "dangerousTool") {
        // Stop execution after this tool resolves
        agent.stop();
    }
});

agent.stopSync()

Immediately aborts the execution synchronously. It throws an internal signal that halts the execution loop immediately without waiting for promises to resolve.

agent.onBeforeModelInvocation(() => {
    if (userIsBanned) {
        agent.stopSync();
    }
});

Agent Forking

Experimental

Fragola provides a powerful mechanism to duplicate an agent mid-conversation using agent.fork(). This is particularly useful when you want to explore different conversational branches, simulate "what-if" scenarios, or safely execute destructive tasks without polluting the main agent's history.

Experimental Feature: The fork() API is currently experimental. While the core cloning mechanism is stable, edge cases involving deeply nested custom stores or complex asynchronous hooks are still being evaluated.

Forking an Agent

Calling .fork() creates a deep clone of the agent's current state. The new agent operates completely independently from that point onward. It is not just a simple copy of the message history and options; it deep copies the entire runtime configuration.

// The main agent starts a conversation
await agent.userMessage({ content: "I want to write a sci-fi story." });

// We fork the agent to explore a specific path
const forkA = agent.fork({ name: "BranchA" });
await forkA.userMessage({ content: "Make the main character a cyborg." });

// We fork the agent again to explore an alternative path
const forkB = agent.fork({ name: "BranchB" });
await forkB.userMessage({ content: "Make the main character a telepathic alien." });

// The original `agent` remains unaffected by forkA and forkB.

What gets Inherited?

When you fork an agent, the following runtime properties are deeply cloned or inherited:

  • State: The entire messages array, status, and stepCount are deeply cloned.
  • Tools: The fork receives all the tools currently registered to the parent at the exact moment of the fork.
  • Hooks & Listeners: Any event listeners or Hooks attached to the parent agent are deeply cloned and re-bound to the new fork.
  • Stores: Agent-scoped Store instances are deeply cloned so modifications in the fork's store do not affect the parent's store.
  • Options: Instructions, model settings, and descriptions are carried over.

Understanding the Fork Hierarchy

Every forked agent keeps a reference to its parent's ID via the forkOf property. This allows you to understand the provenance of a specific agent instance.

console.log(specializedFork.forkOf === agent.id()); // true
console.log(agent.forkOf); // undefined (it is the root agent)

On this page