Tools
Tools let the model delegate work to your code. Each definition bundles a name
, a short description
that teaches the model when to call it, a JSON Schema parameters
object, and an execute
handler that receives (args, context, state)
. The handler must return an AgentToolResult
so the agent can stream success or failure back into the conversation.
Use descriptive verbs for names (search_docs
, create_ticket
) and write the description as if you were coaching the model. Parameters should only include data the model can realistically supply. If you need to resolve identifiers or fetch additional state, do so inside execute
where you still have access to the run session context
and the full RunState
history.
interface AgentTool< TContext, // eslint-disable-next-line @typescript-eslint/no-explicit-any TArgs extends Record<string, unknown> = any,> { /** * Name of the tool. */ name: string; /** * A description of the tool to instruct the model how and when to use it. */ description: string; /** * The JSON schema of the parameters that the tool accepts. The type must be "object". */ parameters: JSONSchema; /** * The function that will be called to execute the tool with given parameters and context. * * If the tool throws an error, the agent will be interrupted and the error will be propagated. * To avoid interrupting the agent, the tool must return an `AgentToolResult` with `is_error` set to true. */ execute: ( args: TArgs, ctx: TContext, state: RunState, ) => AgentToolResult | Promise<AgentToolResult>;}
interface AgentToolResult { content: Part[]; is_error: boolean;}
pub trait AgentTool<TCtx>: Send + Sync { /// Name of the tool. fn name(&self) -> String; /// A description of the tool to instruct the model how and when to use it. fn description(&self) -> String; /// The JSON schema of the parameters that the tool accepts. The type must /// be "object". fn parameters(&self) -> JSONSchema; /// The function that will be called to execute the tool with given /// parameters and context. /// /// If the tool throws an error, the agent will be interrupted and the error /// will be propagated. To avoid interrupting the agent, the tool must /// return an `AgentToolResult` with `is_error` set to true. async fn execute( &self, args: Value, context: &TCtx, state: &RunState, ) -> Result<AgentToolResult, Box<dyn Error + Send + Sync>>;}
pub struct AgentToolResult { pub content: Vec<Part>, pub is_error: bool,}
type AgentTool[C any] interface { // Name of the tool. Name() string // A description of the tool to instruct the model how and when to use it. Description() string // The JSON schema of the parameters that the tool accepts. The type must // be "object". Parameters() llmsdk.JSONSchema // The function that will be called to execute the tool with given // parameters and context. // // If the tool returns an error, the agent will be interrupted and the error // will be propagated. To avoid interrupting the agent, the tool must // return an `AgentToolResult` with `is_error` set to true. Execute(ctx context.Context, params json.RawMessage, contextVal C, runState *RunState) (AgentToolResult, error)}
type AgentToolResult struct { Content []llmsdk.Part `json:"content"` IsError bool `json:"is_error"`}
Return structured Part[]
content so multimodal models can relay text, images, or audio results. Set is_error
to true
when you want the model to acknowledge a failure instead of halting the run with an exception.
Example
Section titled “Example”The examples below show simple tools in each SDK, including how to wire them into an agent.
import { Agent, getResponseText, tool } from "@hoangvvo/llm-agent";import { typeboxTool } from "@hoangvvo/llm-agent/typebox";import { zodTool } from "@hoangvvo/llm-agent/zod";import { Type } from "@sinclair/typebox";import z from "zod";import { getModel } from "./get-model.ts";
/** * Shared context used by every tool invocation. Tools mutate this object so the agent can * keep track of the case without needing per-turn toolkits. */interface LostAndFoundContext { manifestId: string; archivistOnDuty: string; // Items waiting for confirmation before we issue a receipt. intakeLedger: Map< string, { description: string; priority: "standard" | "rush" } >; // Items that must be escalated for contraband review. flaggedContraband: Set<string>; // Notes that should appear on the final receipt. receiptNotes: string[];}
function createContext(): LostAndFoundContext { return { manifestId: "aurora-shift", archivistOnDuty: "Quill", intakeLedger: new Map(), flaggedContraband: new Set(), receiptNotes: [], };}
/** * Basic `tool` helper showcasing parameter validation plus context mutation. */const intakeItemTool = tool< LostAndFoundContext, { item_id: string; description: string; priority?: "standard" | "rush"; }>({ name: "intake_item", description: "Register an item reported by the traveller. Records a note for later receipt generation.", parameters: { type: "object", properties: { item_id: { type: "string", description: "Identifier used on the manifest ledger.", }, description: { type: "string", description: "What the traveller says it looks like.", }, priority: { type: "string", description: "Optional rush flag. Defaults to standard intake.", enum: ["standard", "rush"], }, }, required: ["item_id", "description", "priority"], additionalProperties: false, }, execute(args, ctx) { const normalizedId = args.item_id.trim().toLowerCase(); if (ctx.intakeLedger.has(normalizedId)) { return { content: [ { type: "text", text: `Item ${args.item_id} is already on the ledger—confirm the manifest number before adding duplicates.`, }, ], is_error: true, }; }
const priority = args.priority?.trim() === "" ? "standard" : (args.priority ?? "standard"); ctx.intakeLedger.set(normalizedId, { description: args.description, priority, }); ctx.receiptNotes.push( `${args.item_id}: ${args.description}${priority === "rush" ? " (rush intake)" : ""}`, );
return { content: [ { type: "text", text: `Logged ${args.description} as ${args.item_id}. Intake queue now holds ${ctx.intakeLedger.size} item(s).`, }, ], is_error: false, }; },});
/** * zodTool helper to demonstrate schema definitions using Zod as well as contextual validation. * Requires: * * npm install zod zod-to-json-schema */const flagContrabandTool = zodTool({ name: "flag_contraband", description: "Escalate a manifest item for contraband review. Prevents it from appearing on the standard receipt.", parameters: z.object({ item_id: z.string().describe("Item identifier within the manifest."), reason: z .string() .min(3) .describe("Why the item requires additional screening."), }), execute(args, ctx: LostAndFoundContext) { const key = args.item_id.trim().toLowerCase(); if (!ctx.intakeLedger.has(key)) { return { content: [ { type: "text", text: `Cannot flag ${args.item_id}; it has not been logged yet. Intake the item first.`, }, ], is_error: true, }; }
ctx.flaggedContraband.add(key); ctx.receiptNotes.push(`⚠️ ${args.item_id} held for review: ${args.reason}`);
return { content: [ { type: "text", text: `${args.item_id} marked for contraband inspection. Inform security before release.`, }, ], is_error: false, }; },});
/** * Another standard tool using Typebox that demonstrates returning a final summary and clearing state. * Requires: * * npm install @sinclair/typebox */const issueReceiptTool = typeboxTool({ name: "issue_receipt", description: "Publish a receipt for the traveller: lists cleared items, highlights contraband reminders, and clears the ledger.", parameters: Type.Object( { traveller: Type.String({ description: "Name to print on the receipt." }), }, { additionalProperties: false }, ), execute(args, ctx: LostAndFoundContext) { if (ctx.intakeLedger.size === 0) { return { content: [ { type: "text", text: `No items pending on manifest ${ctx.manifestId}. Intake something before issuing a receipt.`, }, ], is_error: true, }; }
const cleared = Array.from(ctx.intakeLedger.entries()) .filter(([id]) => !ctx.flaggedContraband.has(id)) .map(([id, entry]) => `${id} (${entry.description})`);
const contraband = ctx.flaggedContraband.size; const summaryLines: string[] = [ `Receipt for ${args.traveller} on manifest ${ctx.manifestId}:`, cleared.length > 0 ? `Cleared items: ${cleared.join(", ")}` : "No items cleared—everything is held for review.", ]; if (ctx.receiptNotes.length > 0) { summaryLines.push("Notes:"); summaryLines.push(...ctx.receiptNotes); } summaryLines.push( contraband > 0 ? `${contraband} item(s) require contraband follow-up.` : "No contraband flags recorded.", );
ctx.intakeLedger.clear(); ctx.flaggedContraband.clear(); ctx.receiptNotes.length = 0;
return { content: [ { type: "text", text: summaryLines.join("\n"), }, ], is_error: false, }; },});
const model = getModel("openai", "gpt-4o");
const lostAndFoundAgent = new Agent<LostAndFoundContext>({ name: "WaypointClerk", instructions: [ "You are the archivist completing intake for Waypoint Seven's Interdimensional Lost & Found desk.", "When travellers report belongings, call the available tools to mutate the manifest and then summarise your actions.", "If a tool reports an error, acknowledge the issue and guide the traveller appropriately.", ], model, tools: [intakeItemTool, flagContrabandTool, issueReceiptTool],});
// Successful run: exercise multiple tools and show evolving context state.const successContext = createContext();const successResponse = await lostAndFoundAgent.run({ context: successContext, input: [ { type: "message", role: "user", content: [ { type: "text", text: `Log the Chrono Locket as rush, mark the "Folded star chart" for contraband, then issue a receipt for Captain Lyra Moreno.`, }, ], }, ],});
console.log("\n=== SUCCESS RUN ===");console.dir(successResponse, { depth: null });console.log(getResponseText(successResponse));
// Failure case: demonstrate tool error handling in the same scenario.const failureContext = createContext();const failureResponse = await lostAndFoundAgent.run({ context: failureContext, input: [ { type: "message", role: "user", content: [ { type: "text", text: `Issue a receipt immediately without logging anything.`, }, ], }, ],});
console.log("\n=== FAILURE RUN ===");console.dir(failureResponse, { depth: null });console.log(getResponseText(failureResponse));
use async_trait::async_trait;use dotenvy::dotenv;use llm_agent::{Agent, AgentRequest, AgentTool, AgentToolResult};use llm_sdk::{ openai::{OpenAIModel, OpenAIModelOptions}, Message, Part,};use serde::Deserialize;use serde_json::Value;use std::{ collections::{HashMap, HashSet}, env, error::Error, sync::{Arc, Mutex},};
/// Context shared across tool invocations. Tools mutate this state directly so/// we can showcase how agents can maintain memory without involving toolkits.#[derive(Default, Clone)]struct LostAndFoundContext { manifest_id: String, archivist: String, intake_ledger: Arc<Mutex<HashMap<String, ItemRecord>>>, flagged_contraband: Arc<Mutex<HashSet<String>>>, receipt_notes: Arc<Mutex<Vec<String>>>,}
#[derive(Clone)]struct ItemRecord { description: String, priority: String,}
fn create_context() -> LostAndFoundContext { LostAndFoundContext { manifest_id: "aurora-shift".into(), archivist: "Quill".into(), intake_ledger: Arc::new(Mutex::new(HashMap::new())), flagged_contraband: Arc::new(Mutex::new(HashSet::new())), receipt_notes: Arc::new(Mutex::new(Vec::new())), }}
/// intake_item mirrors the TypeScript/Go examples: validates input and updates/// the ledger.struct IntakeItemTool;
#[derive(Deserialize)]struct IntakeItemParams { item_id: String, description: String, priority: Option<String>,}
#[async_trait]impl AgentTool<LostAndFoundContext> for IntakeItemTool { fn name(&self) -> String { "intake_item".into() } fn description(&self) -> String { "Register an item reported by the traveller.".into() } fn parameters(&self) -> llm_sdk::JSONSchema { serde_json::json!({ "type": "object", "properties": { "item_id": { "type": "string", "description": "Identifier used on the manifest ledger." }, "description": { "type": "string", "description": "What the traveller says it looks like." }, "priority": { "type": "string", "enum": ["standard", "rush"] } }, "required": ["item_id", "description"], "additionalProperties": false }) } async fn execute( &self, args: Value, context: &LostAndFoundContext, _state: &llm_agent::RunState, ) -> Result<AgentToolResult, Box<dyn Error + Send + Sync>> { let params: IntakeItemParams = serde_json::from_value(args)?; let key = params.item_id.trim().to_lowercase(); if key.is_empty() { return Err("item_id cannot be empty".into()); } let mut ledger = context .intake_ledger .lock() .expect("intake ledger mutex poisoned"); if ledger.contains_key(&key) { return Ok(AgentToolResult { content: vec![Part::text(format!( "Item {} is already on the ledger—confirm the manifest number before adding \ duplicates.", params.item_id ))], is_error: true, }); }
let priority = params.priority.unwrap_or_else(|| "standard".into()); ledger.insert( key, ItemRecord { description: params.description.clone(), priority: priority.clone(), }, );
context .receipt_notes .lock() .expect("receipt notes mutex poisoned") .push(format!( "{}: {}{}", params.item_id, params.description, if priority == "rush" { " (rush intake)" } else { "" } ));
Ok(AgentToolResult { content: vec![Part::text(format!( "Logged {} as {}. Intake queue now holds {} item(s).", params.description, params.item_id, ledger.len() ))], is_error: false, }) }}
/// flag_contraband highlights additional validation and shared-state updates.struct FlagContrabandTool;
#[derive(Deserialize)]struct FlagContrabandParams { item_id: String, reason: String,}
#[async_trait]impl AgentTool<LostAndFoundContext> for FlagContrabandTool { fn name(&self) -> String { "flag_contraband".into() } fn description(&self) -> String { "Escalate a manifest item for contraband review.".into() } fn parameters(&self) -> llm_sdk::JSONSchema { serde_json::json!({ "type": "object", "properties": { "item_id": { "type": "string" }, "reason": { "type": "string" } }, "required": ["item_id", "reason"], "additionalProperties": false }) } async fn execute( &self, args: Value, context: &LostAndFoundContext, _state: &llm_agent::RunState, ) -> Result<AgentToolResult, Box<dyn Error + Send + Sync>> { let params: FlagContrabandParams = serde_json::from_value(args)?; let key = params.item_id.trim().to_lowercase();
let ledger = context .intake_ledger .lock() .expect("intake ledger mutex poisoned"); if !ledger.contains_key(&key) { return Ok(AgentToolResult { content: vec![Part::text(format!( "Cannot flag {}; it has not been logged yet. Intake the item first.", params.item_id ))], is_error: true, }); } drop(ledger);
context .flagged_contraband .lock() .expect("flagged contraband mutex poisoned") .insert(key); context .receipt_notes .lock() .expect("receipt notes mutex poisoned") .push(format!( "⚠️ {} held for review: {}", params.item_id, params.reason ));
Ok(AgentToolResult { content: vec![Part::text(format!( "{} marked for contraband inspection. Inform security before release.", params.item_id ))], is_error: false, }) }}
/// issue_receipt summarises everything, returning a final message and clearing/// state.struct IssueReceiptTool;
#[derive(Deserialize)]struct IssueReceiptParams { traveller: String,}
#[async_trait]impl AgentTool<LostAndFoundContext> for IssueReceiptTool { fn name(&self) -> String { "issue_receipt".into() } fn description(&self) -> String { "Publish a receipt for the traveller and clear the manifest ledger.".into() } fn parameters(&self) -> llm_sdk::JSONSchema { serde_json::json!({ "type": "object", "properties": { "traveller": { "type": "string" } }, "required": ["traveller"], "additionalProperties": false }) } async fn execute( &self, args: Value, context: &LostAndFoundContext, _state: &llm_agent::RunState, ) -> Result<AgentToolResult, Box<dyn Error + Send + Sync>> { let params: IssueReceiptParams = serde_json::from_value(args)?;
let mut ledger = context .intake_ledger .lock() .expect("intake ledger mutex poisoned"); if ledger.is_empty() { return Ok(AgentToolResult { content: vec![Part::text(format!( "No items pending on manifest {}. Intake something before issuing a receipt.", context.manifest_id ))], is_error: true, }); }
let mut cleared = Vec::new(); { let flagged = context .flagged_contraband .lock() .expect("flagged contraband mutex poisoned"); for (id, record) in ledger.iter() { if !flagged.contains(id) { cleared.push(format!("{} ({})", id, record.description)); } } }
let mut summary = vec![format!( "Receipt for {} on manifest {}:", params.traveller, context.manifest_id )]; if !cleared.is_empty() { summary.push(format!("Cleared items: {}", cleared.join(", "))); } else { summary.push("No items cleared—everything is held for review.".into()); } { let notes = context .receipt_notes .lock() .expect("receipt notes mutex poisoned"); if !notes.is_empty() { summary.push("Notes:".into()); summary.extend(notes.iter().cloned()); } } let contraband_count = context .flagged_contraband .lock() .expect("flagged contraband mutex poisoned") .len(); summary.push(format!( "{} item(s) require contraband follow-up.", contraband_count ));
// Clear state for the next manifest. ledger.clear(); context .flagged_contraband .lock() .expect("flagged contraband mutex poisoned") .clear(); context .receipt_notes .lock() .expect("receipt notes mutex poisoned") .clear();
Ok(AgentToolResult { content: vec![Part::text(summary.join("\n"))], is_error: false, }) }}
#[tokio::main]async fn main() -> Result<(), Box<dyn Error>> { dotenv().ok();
let api_key = env::var("OPENAI_API_KEY")?; let model = Arc::new(OpenAIModel::new( "gpt-4o", OpenAIModelOptions { api_key, ..Default::default() }, ));
let agent = Agent::builder("WaypointClerk", model) .add_instruction( "You are the archivist completing intake for Waypoint Seven's Interdimensional Lost & \ Found desk.", ) .add_instruction( "When travellers report belongings, call the available tools to mutate the manifest \ and then summarise your actions.", ) .add_instruction( "If a tool reports an error, acknowledge the issue and guide the traveller \ appropriately.", ) .add_tool(IntakeItemTool) .add_tool(FlagContrabandTool) .add_tool(IssueReceiptTool) .build();
// Success path: multiple tools fired in one turn. let success_context = create_context(); let success_response = agent .run(AgentRequest { context: success_context.clone(), input: vec![llm_agent::AgentItem::Message(Message::user(vec![ Part::text( "Log the Chrono Locket as rush, flag the Folded star chart for contraband, \ then issue a receipt for Captain Lyra Moreno.", ), ]))], }) .await?;
println!("\n=== SUCCESS RUN ==="); println!("{success_response:#?}"); println!("{}", success_response.text());
// Failure path: illustrate tool error handling. let failure_context = create_context(); let failure_response = agent .run(AgentRequest { context: failure_context, input: vec![llm_agent::AgentItem::Message(Message::user(vec![ Part::text("Issue a receipt immediately without logging anything."), ]))], }) .await?;
println!("\n=== FAILURE RUN ==="); println!("{failure_response:#?}"); println!("{}", failure_response.text());
Ok(())}
package main
import ( "context" "encoding/json" "errors" "fmt" "log" "os" "strings"
llmagent "github.com/hoangvvo/llm-sdk/agent-go" llmsdk "github.com/hoangvvo/llm-sdk/sdk-go" "github.com/hoangvvo/llm-sdk/sdk-go/openai" "github.com/joho/godotenv")
// LostAndFoundContext mirrors the TypeScript example: agent tools mutate the manifest// directly so the RunSession sees the latest state without requiring a toolkit.type LostAndFoundContext struct { ManifestID string Archivist string IntakeLedger map[string]ItemRecord FlaggedContraband map[string]struct{} ReceiptNotes []string}
type ItemRecord struct { Description string Priority string}
func newContext() *LostAndFoundContext { return &LostAndFoundContext{ ManifestID: "aurora-shift", Archivist: "Quill", IntakeLedger: map[string]ItemRecord{}, FlaggedContraband: map[string]struct{}{}, ReceiptNotes: []string{}, }}
// intakeItemTool shows a standard AgentTool implementation that validates input,// mutates context, and returns structured output.type intakeItemTool struct{}
func (intakeItemTool) Name() string { return "intake_item" }func (intakeItemTool) Description() string { return "Register an item reported by the traveller." }
func (intakeItemTool) Parameters() llmsdk.JSONSchema { return llmsdk.JSONSchema{ "type": "object", "properties": map[string]any{ "item_id": map[string]any{ "type": "string", "description": "Identifier used on the manifest ledger.", }, "description": map[string]any{ "type": "string", "description": "What the traveller says it looks like.", }, "priority": map[string]any{ "type": "string", "enum": []string{"standard", "rush"}, }, }, "required": []string{"item_id", "description"}, "additionalProperties": false, }}
func (intakeItemTool) Execute(_ context.Context, raw json.RawMessage, ctx *LostAndFoundContext, _ *llmagent.RunState) (llmagent.AgentToolResult, error) { var params struct { ItemID string `json:"item_id"` Description string `json:"description"` Priority string `json:"priority"` } if err := json.Unmarshal(raw, ¶ms); err != nil { return llmagent.AgentToolResult{}, err }
normalized := strings.ToLower(strings.TrimSpace(params.ItemID)) if normalized == "" { return llmagent.AgentToolResult{}, errors.New("item_id cannot be empty") } if _, exists := ctx.IntakeLedger[normalized]; exists { return llmagent.AgentToolResult{ Content: []llmsdk.Part{llmsdk.NewTextPart(fmt.Sprintf("Item %s is already on the ledger—confirm the manifest number before adding duplicates.", params.ItemID))}, IsError: true, }, nil }
priority := params.Priority if priority == "" { priority = "standard" }
ctx.IntakeLedger[normalized] = ItemRecord{Description: params.Description, Priority: priority} ctx.ReceiptNotes = append(ctx.ReceiptNotes, fmt.Sprintf("%s: %s%s", params.ItemID, params.Description, ternary(priority == "rush", " (rush intake)", "")))
return llmagent.AgentToolResult{ Content: []llmsdk.Part{llmsdk.NewTextPart(fmt.Sprintf("Logged %s as %s. Intake queue now holds %d item(s).", params.Description, params.ItemID, len(ctx.IntakeLedger)))}, IsError: false, }, nil}
// flagContrabandTool showcases additional validation and context mutation.type flagContrabandTool struct{}
func (flagContrabandTool) Name() string { return "flag_contraband" }func (flagContrabandTool) Description() string { return "Escalate a manifest item for contraband review."}
func (flagContrabandTool) Parameters() llmsdk.JSONSchema { return llmsdk.JSONSchema{ "type": "object", "properties": map[string]any{ "item_id": map[string]any{ "type": "string", "description": "Identifier within the manifest.", }, "reason": map[string]any{ "type": "string", "description": "Why the item needs review.", }, }, "required": []string{"item_id", "reason"}, "additionalProperties": false, }}
func (flagContrabandTool) Execute(_ context.Context, raw json.RawMessage, ctx *LostAndFoundContext, _ *llmagent.RunState) (llmagent.AgentToolResult, error) { var params struct { ItemID string `json:"item_id"` Reason string `json:"reason"` } if err := json.Unmarshal(raw, ¶ms); err != nil { return llmagent.AgentToolResult{}, err }
normalized := strings.ToLower(strings.TrimSpace(params.ItemID)) if _, exists := ctx.IntakeLedger[normalized]; !exists { return llmagent.AgentToolResult{ Content: []llmsdk.Part{llmsdk.NewTextPart(fmt.Sprintf("Cannot flag %s; it has not been logged yet. Intake the item first.", params.ItemID))}, IsError: true, }, nil }
ctx.FlaggedContraband[normalized] = struct{}{} ctx.ReceiptNotes = append(ctx.ReceiptNotes, fmt.Sprintf("⚠️ %s held for review: %s", params.ItemID, params.Reason))
return llmagent.AgentToolResult{ Content: []llmsdk.Part{llmsdk.NewTextPart(fmt.Sprintf("%s marked for contraband inspection. Inform security before release.", params.ItemID))}, IsError: false, }, nil}
// issueReceiptTool highlights summarising context state and clearing it afterwards.type issueReceiptTool struct{}
func (issueReceiptTool) Name() string { return "issue_receipt" }func (issueReceiptTool) Description() string { return "Publish a receipt and clear the manifest ledger."}
func (issueReceiptTool) Parameters() llmsdk.JSONSchema { return llmsdk.JSONSchema{ "type": "object", "properties": map[string]any{ "traveller": map[string]any{ "type": "string", "description": "Recipient of the receipt.", }, }, "required": []string{"traveller"}, "additionalProperties": false, }}
func (issueReceiptTool) Execute(_ context.Context, raw json.RawMessage, ctx *LostAndFoundContext, _ *llmagent.RunState) (llmagent.AgentToolResult, error) { var params struct { Traveller string `json:"traveller"` } if err := json.Unmarshal(raw, ¶ms); err != nil { return llmagent.AgentToolResult{}, err }
if len(ctx.IntakeLedger) == 0 { return llmagent.AgentToolResult{ Content: []llmsdk.Part{llmsdk.NewTextPart(fmt.Sprintf("No items pending on manifest %s. Intake something before issuing a receipt.", ctx.ManifestID))}, IsError: true, }, nil }
cleared := []string{} for id, record := range ctx.IntakeLedger { if _, flagged := ctx.FlaggedContraband[id]; !flagged { cleared = append(cleared, fmt.Sprintf("%s (%s)", id, record.Description)) } }
summary := []string{ fmt.Sprintf("Receipt for %s on manifest %s:", params.Traveller, ctx.ManifestID), } if len(cleared) > 0 { summary = append(summary, fmt.Sprintf("Cleared items: %s", strings.Join(cleared, ", "))) } else { summary = append(summary, "No items cleared—everything is held for review.") } if len(ctx.ReceiptNotes) > 0 { summary = append(summary, "Notes:") summary = append(summary, ctx.ReceiptNotes...) } summary = append(summary, fmt.Sprintf("%d item(s) require contraband follow-up.", len(ctx.FlaggedContraband)))
// Clear state so subsequent turns start fresh. ctx.IntakeLedger = map[string]ItemRecord{} ctx.FlaggedContraband = map[string]struct{}{} ctx.ReceiptNotes = ctx.ReceiptNotes[:0]
return llmagent.AgentToolResult{ Content: []llmsdk.Part{llmsdk.NewTextPart(strings.Join(summary, "\n"))}, IsError: false, }, nil}
func main() { if err := godotenv.Load("../.env"); err != nil && !errors.Is(err, os.ErrNotExist) { log.Fatalf("load env: %v", err) }
apiKey := os.Getenv("OPENAI_API_KEY") if apiKey == "" { log.Fatal("OPENAI_API_KEY environment variable must be set") }
model := openai.NewOpenAIModel("gpt-4o", openai.OpenAIModelOptions{APIKey: apiKey})
agent := llmagent.NewAgent[*LostAndFoundContext]( "WaypointClerk", model, llmagent.WithInstructions( llmagent.InstructionParam[*LostAndFoundContext]{String: ptr("You are the archivist completing intake for Waypoint Seven's Interdimensional Lost & Found desk.")}, llmagent.InstructionParam[*LostAndFoundContext]{String: ptr("When travellers report belongings, call the available tools to mutate the manifest and then summarise your actions.")}, llmagent.InstructionParam[*LostAndFoundContext]{String: ptr("If a tool reports an error, acknowledge the issue and guide the traveller appropriately.")}, ), llmagent.WithTools(intakeItemTool{}, flagContrabandTool{}, issueReceiptTool{}), )
// Successful run exercises multiple tools in a single turn. successCtx := newContext() successResp, err := agent.Run(context.Background(), llmagent.AgentRequest[*LostAndFoundContext]{ Context: successCtx, Input: []llmagent.AgentItem{ llmagent.NewAgentItemMessage( llmsdk.NewUserMessage( llmsdk.NewTextPart("Log the Chrono Locket as rush, flag the Folded star chart for contraband, then issue a receipt for Captain Lyra Moreno."), ), ), }, }) if err != nil { log.Fatal(err) } fmt.Println("\n=== SUCCESS RUN ===") fmt.Printf("%#v\n", successResp) fmt.Println(successResp.Text())
// Failure run demonstrates a tool error path. failureCtx := newContext() failureResp, err := agent.Run(context.Background(), llmagent.AgentRequest[*LostAndFoundContext]{ Context: failureCtx, Input: []llmagent.AgentItem{ llmagent.NewAgentItemMessage( llmsdk.NewUserMessage( llmsdk.NewTextPart("Issue a receipt immediately without logging anything."), ), ), }, }) if err != nil { log.Fatal(err) } fmt.Println("\n=== FAILURE RUN ===") fmt.Printf("%#v\n", failureResp) fmt.Println(failureResp.Text())}
func ptr[T any](v T) *T { return &v }
func ternary[T any](cond bool, a, b T) T { if cond { return a } return b}