Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions AGENT_DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Agent & Tool Loop

High-level API for agentic LLM workflows with automatic tool execution.

## Overview

```
┌─────────────────────────────────────┐
│ Agent │
│ - provider │
│ - system_prompt │
│ - tools (with executors) │
│ - max_iterations │
├─────────────────────────────────────┤
│ Loop │
│ - Sends messages to LLM │
│ - Executes tool calls │
│ - Repeats until done │
├─────────────────────────────────────┤
│ Runtime │
│ - HTTP transport abstraction │
│ - Erlang: gleam_httpc │
│ - JS: gleam_fetch (TBD) │
└─────────────────────────────────────┘
```

## Tool Design

Tools carry their own executor. Type safety is preserved at definition via closures:

```gleam
let weather_tool = tool.tool(
name: "get_weather",
description: "Get weather for a location",
schema: weather_schema(),
execute: fn(ctx, args) {
// args is WeatherArgs - fully typed!
Ok("Sunny in " <> args.location)
},
)
```

## Agent

Minimal config for agentic workflows:

```gleam
let my_agent = agent.new(provider)
|> agent.with_system_prompt("You are helpful.")
|> agent.with_tool(weather_tool)
|> agent.with_max_iterations(5)
```

Request-level config (max_tokens, temperature) is passed via `run_with_config`:

```gleam
loop.run_with_config(agent, ctx, messages, runtime, Some(fn(req) {
req
|> request.with_max_tokens(1000)
|> request.with_temperature(0.7)
}))
```

## Runtime

Abstracts HTTP transport:

```gleam
pub type Runtime {
Runtime(send: fn(Request(String)) -> Result(Response(String), Error))
}
```

## Usage

```gleam
// 1. Create provider
let provider = openai.new("sk-...") |> openai.provider

// 2. Create tools
let search_tool = tool.tool(
name: "search",
description: "Search the web",
schema: search_schema(),
execute: fn(ctx, args) { do_search(ctx, args.query) },
)

// 3. Create agent
let my_agent = agent.new(provider)
|> agent.with_system_prompt("You are a helpful assistant.")
|> agent.with_tool(search_tool)

// 4. Run
let messages = [gai.user_text("Search for Gleam")]
case loop.run(my_agent, ctx, messages, runtime) {
Ok(result) -> response.text_content(result.response)
Error(err) -> gai.error_to_string(err)
}
```
94 changes: 94 additions & 0 deletions src/gai/agent.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/// Agent configuration for tool-enabled LLM interactions.
///
/// An Agent bundles a provider, system prompt, and executable tools
/// together for use with the tool loop. It focuses on agentic-specific
/// concerns, leaving request-level config (max_tokens, temperature, etc.)
/// to be passed separately.
///
/// ## Example
///
/// ```gleam
/// let my_agent = agent.new(provider)
/// |> agent.with_system_prompt("You are a helpful assistant.")
/// |> agent.with_tool(weather_tool)
/// |> agent.with_tool(search_tool)
/// |> agent.with_max_iterations(5)
/// ```
import gai/provider.{type Provider}
import gai/tool.{type Tool}
import gleam/list
import gleam/option.{type Option, None, Some}

/// Agent configuration with context type parameter.
///
/// The `ctx` type represents the context passed to tool executors.
pub opaque type Agent(ctx) {
Agent(
provider: Provider,
system_prompt: Option(String),
tools: List(Tool(ctx)),
max_iterations: Int,
)
}

/// Create a new agent with a provider
pub fn new(provider: Provider) -> Agent(ctx) {
Agent(provider:, system_prompt: None, tools: [], max_iterations: 10)
}

/// Set the system prompt
pub fn with_system_prompt(agent: Agent(ctx), prompt: String) -> Agent(ctx) {
Agent(..agent, system_prompt: Some(prompt))
}

/// Add a tool to the agent
pub fn with_tool(agent: Agent(ctx), tool: Tool(ctx)) -> Agent(ctx) {
Agent(..agent, tools: [tool, ..agent.tools])
}

/// Add multiple tools to the agent
pub fn with_tools(agent: Agent(ctx), tools: List(Tool(ctx))) -> Agent(ctx) {
Agent(..agent, tools: list.append(tools, agent.tools))
}

/// Set maximum tool loop iterations (safety limit)
pub fn with_max_iterations(agent: Agent(ctx), n: Int) -> Agent(ctx) {
Agent(..agent, max_iterations: n)
}

/// Get the provider
pub fn provider(agent: Agent(ctx)) -> Provider {
agent.provider
}

/// Get the system prompt
pub fn system_prompt(agent: Agent(ctx)) -> Option(String) {
agent.system_prompt
}

/// Get tools as a list
pub fn tools(agent: Agent(ctx)) -> List(Tool(ctx)) {
agent.tools
}

/// Get max iterations setting
pub fn max_iterations(agent: Agent(ctx)) -> Int {
agent.max_iterations
}

/// Find a tool by name
pub fn find_tool(agent: Agent(ctx), name: String) -> Option(Tool(ctx)) {
agent.tools
|> list.find(fn(t) { tool.name(t) == name })
|> option.from_result
}

/// Check if the agent has any tools
pub fn has_tools(agent: Agent(ctx)) -> Bool {
!list.is_empty(agent.tools)
}

/// Get the number of tools
pub fn tool_count(agent: Agent(ctx)) -> Int {
list.length(agent.tools)
}
Loading