sdk

package module
v0.0.16 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: Apache-2.0 Imports: 31 Imported by: 0

README ΒΆ

Golang Agent Harness SDK

Go Reference Go Report Card License

A powerful Golang SDK for building AI agents and making LLM calls across multiple providers with a unified API. Switch between OpenAI, Anthropic, Gemini, and more with just a single line change.

Features

  • πŸ”„ Multi-Provider Support - Unified API for OpenAI, Anthropic, Gemini, and more
  • πŸ€– Agent SDK - Build sophisticated AI agents with tools, memory, and multi-step reasoning
  • πŸ‘€ Human-in-the-Loop - Integrate human feedback and approval workflows
  • πŸ›‘οΈ Durable Execution - Create fault-tolerant agents with Restate or Temporal
  • πŸ”§ Tool Calling - Function calling and MCP (Model Context Protocol) tool integration
  • πŸͺ Hooks - Intercept tool calls and model calls for auth, budgets, and audit
  • 🏷️ Tool Annotations - MCP-style behavioural hints on both MCP and function tools
  • πŸ’Ύ Conversation History - Maintain context across interactions with built-in persistence
  • 🧩 Sub-Agents & Handoffs - Call a specialist as a tool, or transfer the conversation to it
  • 🎚️ Steering - Send a correction into a run already in flight
  • 🌊 Streaming Support - Real-time streaming responses for better UX
  • πŸ›‘ Cancellation - Stop in-flight runs cleanly, including mid-stream and mid-tool-call
  • πŸ“ Structured Output - JSON schema validation for reliable structured responses

Table of Contents

Installation

go get -u github.com/hastekit/agent-sdk-go

Requirements:

  • Go 1.25.0 or higher

Quick Start

Simple Agent
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    hastekit "github.com/hastekit/agent-sdk-go"
    "github.com/hastekit/agent-sdk-go/pkg/agents"
    "github.com/hastekit/agent-sdk-go/pkg/agents/history"
    "github.com/hastekit/agent-sdk-go/pkg/gateway/llm/responses"
    "github.com/hastekit/agent-sdk-go/pkg/utils"
)

func main() {
    // Configure an LLM client and bind a model.
    client := hastekit.NewLLMClient([]hastekit.ProviderConfig{
        {
            ProviderName: hastekit.ProviderOpenAI,
            ApiKeys: []*hastekit.APIKeyConfig{
                {Name: "default", APIKey: os.Getenv("OPENAI_API_KEY")},
            },
        },
    })

    // Create agent
    agent := hastekit.NewAgent(&hastekit.AgentConfig{
        Name:        "Assistant",
        Instruction: hastekit.NewPrompt("You are a helpful assistant."),
        LLM:         client.Model("OpenAI/gpt-4o-mini"),
        Parameters: responses.Parameters{
            Temperature: utils.Ptr(0.7),
        },
    })

    // Execute agent β€” returns a handle for streaming chunks + result.
    handle, err := agent.Execute(context.Background(), &agents.AgentInput{
        Message: history.Message{
            Messages: []responses.InputMessageUnion{
                responses.UserMessage("Hello! Tell me a joke."),
            },
        },
    })
    if err != nil {
        log.Fatal(err)
    }

    // Result() drains the chunk stream and returns the aggregated output.
    // For live streaming, range over handle.Chunks then call handle.Wait().
    out, err := handle.Result()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(out.Output[0].OfOutputMessage.Content[0].OfOutputText.Text)
}

agent.Execute is non-blocking and returns an *AgentHandle:

type AgentHandle struct {
    StreamID string                          // Broker channel id for this run
    Chunks   <-chan *responses.ResponseChunk // Live chunks; channel closes when run ends
}

func (h *AgentHandle) Stop(ctx context.Context) error    // graceful cancel at next iteration
func (h *AgentHandle) Wait() (*AgentOutput, error)       // pair with manual Chunks draining
func (h *AgentHandle) Result() (*AgentOutput, error)     // drain Chunks + return output

Usage

LLM Client

hastekit.NewLLMClient takes a list of provider configs and returns a client. Bind a model with client.Model("Provider/model") β€” the returned value satisfies the llm.Provider interface and exposes NewResponses, NewStreamingResponses, and friends.

// Single provider
client := hastekit.NewLLMClient([]hastekit.ProviderConfig{
    {
        ProviderName: hastekit.ProviderOpenAI,
        ApiKeys: []*hastekit.APIKeyConfig{
            {Name: "default", APIKey: os.Getenv("OPENAI_API_KEY")},
        },
    },
})

// Multiple providers β€” switch by changing the model string
client := hastekit.NewLLMClient([]hastekit.ProviderConfig{
    {
        ProviderName: hastekit.ProviderOpenAI,
        ApiKeys: []*hastekit.APIKeyConfig{
            {Name: "default", APIKey: os.Getenv("OPENAI_API_KEY")},
        },
    },
    {
        ProviderName: hastekit.ProviderAnthropic,
        ApiKeys: []*hastekit.APIKeyConfig{
            {Name: "default", APIKey: os.Getenv("ANTHROPIC_API_KEY")},
        },
    },
})

openai := client.Model("OpenAI/gpt-4o-mini")
claude := client.Model("Anthropic/claude-sonnet-4-5")

Provider constants: hastekit.ProviderOpenAI, ProviderAnthropic, ProviderGemini, ProviderXAI, ProviderBedrock, ProviderOllama, ProviderOpenRouter, ProviderElevenLabs, ProviderSarvam, ProviderDeepSeek, ProviderMoonshot (Kimi models), ProviderZAI (GLM models).

Agents
Agent with Custom Tools

hastekit.NewTool turns any func(ctx, In) (Out, error) into a tool. The input JSON schema is derived from the argument struct, and arguments/results are marshalled for you:

type WeatherArgs struct {
    Location string `json:"location" jsonschema_description:"City name"`
}

type Weather struct {
    TempC     float64 `json:"temp_c"`
    Condition string  `json:"condition"`
}

func getWeather(ctx context.Context, args WeatherArgs) (Weather, error) {
    // Your logic here
    return Weather{TempC: 22.5, Condition: "Sunny"}, nil
}

weatherTool := hastekit.NewTool(getWeather,
    hastekit.WithName("get_weather"), // optional; defaults to the function name
    hastekit.WithDescription("Get current weather for a location"),
    hastekit.WithReadOnly(true), // optional behavioural hint
)

// Use the tool
agent := hastekit.NewAgent(&hastekit.AgentConfig{
    Name:        "Weather Assistant",
    Instruction: hastekit.NewPrompt("You help users check the weather."),
    LLM:         client.Model("OpenAI/gpt-4o-mini"),
    Tools:       []hastekit.Tool{weatherTool},
})

Tools that implement the agents.Tool interface directly also work and can be mixed into the same Tools slice. The interface is two methods β€” Execute, and GetToolDescriptor() *agents.BaseTool for the tool's schema, name and flags β€” and embedding agents.BaseTool supplies the second one, so a hand-written tool only defines Execute:

type DeleteUserTool struct {
    *agents.BaseTool
}

func NewDeleteUserTool() *DeleteUserTool {
    return &DeleteUserTool{
        BaseTool: &agents.BaseTool{
            RequiresApproval: true,
            ToolUnion: responses.ToolUnion{
                OfFunction: &responses.FunctionTool{
                    Name:        "delete_user",
                    Description: utils.Ptr("Permanently deletes a user account"),
                    Parameters: map[string]any{
                        "type":       "object",
                        "properties": map[string]any{"user_id": map[string]any{"type": "string"}},
                        "required":   []string{"user_id"},
                    },
                },
            },
        },
    }
}

func (t *DeleteUserTool) Execute(ctx context.Context, params *agents.ToolCall) (*agents.ToolCallResponse, error) {
    // Your logic here
}
Sub-Agents

An agent can be given to another agent as a tool, so a specialist is called the same way a function is β€” the caller stays in charge and gets the sub-agent's answer back as a tool result:

import "github.com/hastekit/agent-sdk-go/pkg/agents/tools"

researcher := hastekit.NewAgent(&hastekit.AgentConfig{
    Name:        "Researcher",
    Instruction: hastekit.NewPrompt("You research topics thoroughly."),
    LLM:         model,
})

agent := hastekit.NewAgent(&hastekit.AgentConfig{
    Name:        "Assistant",
    Instruction: hastekit.NewPrompt("You are a helpful assistant."),
    LLM:         model,
    Tools: []agents.Tool{
        tools.NewAgentTool(
            "research",
            "Research a topic in depth",
            researcher,
            tools.SubAgentContextModeNone,
        ),
    },
})

The context mode decides who keeps track of the sub-agent's conversation. Under SubAgentContextModeNone the calling model does: thread_id is one of the tool's parameters, and the thread the sub-agent ran on comes back in the result for it to pass in next time. Under SubAgentContextModeIsolated the tool does, holding the thread in the call's own state, so the sub-agent remembers its earlier turns without the model having to carry an id around.

A sub-agent that pauses for approval pauses the whole run, however deeply it is nested, and resuming resumes it in place rather than starting it again.

Handoffs

A handoff transfers the conversation instead of borrowing an answer: the target agent takes over the thread and replies to the user directly.

agent := hastekit.NewAgent(&hastekit.AgentConfig{
    Name:        "Triage",
    Instruction: hastekit.NewPrompt("Route the user to the right specialist."),
    LLM:         model,
    Handoffs: []*agents.Handoff{
        agents.NewHandoff("Billing", "Questions about invoices and payments", billingAgent),
        agents.NewHandoff("Support", "Technical troubleshooting", supportAgent),
    },
})

The model picks a target by calling the generated transfer_to_agent tool.

By default the next turn starts at the root agent again. Set StickyHandoff: true on the agent to keep the thread with whichever specialist last handled it, so a user mid-conversation with Billing is not re-triaged on every message β€” a later handoff moves the thread on, and a handoff back to the root unsticks it.

Streaming Chunks and Cancellation

agent.Execute returns a handle. Range over handle.Chunks to forward live deltas (UI, SSE, logs); call handle.Stop(ctx) to stop the run β€” it records a "Cancelled by user" assistant turn in history and emits run.completed cleanly.

Stop does not wait for an iteration boundary. It reaches work already in flight:

  • Mid-stream β€” the model call is cut off where it is, rather than waited out. Text that had already streamed still reached the client, but the turn is recorded as cancelled rather than as a half-answer the model never finished.
  • Mid-tool-call β€” a running tool has its context cancelled; a tool that ignores cancellation is abandoned after a grace period so the run still ends.

Either way the loop's invariant holds: every function_call in history is answered, so a stopped thread is still a valid thread to resume from.

handle, err := agent.Execute(ctx, &agents.AgentInput{
    Message: history.Message{
        Messages: []responses.InputMessageUnion{
            responses.UserMessage("Walk me through how to set up Postgres replication."),
        },
    },
})
if err != nil { log.Fatal(err) }

// Cancel after 5 seconds β€” the agent finishes its current step and stops gracefully.
go func() {
    time.Sleep(5 * time.Second)
    _ = handle.Stop(context.Background())
}()

for chunk := range handle.Chunks {
    if chunk.OfOutputTextDelta != nil {
        fmt.Print(chunk.OfOutputTextDelta.Delta)
    }
}

out, err := handle.Wait()

The StreamID on the handle (also returned in the X-Stream-Id HTTP header when serving over HTTP) lets you re-subscribe to the same broker channel β€” useful for resuming a stream after a page refresh, or for stopping the run from a different process.

Steering a Running Agent

A run does not have to be left alone until it finishes. The same broker that carries a stop can carry a message into a run already in flight β€” the loop drains the queue at iteration boundaries, the same cadence at which it checks for a stop, and folds what it finds into the conversation before the next model call:

err := broker.EnqueueMessage(ctx, streamID, history.Message{
    Messages: []responses.InputMessageUnion{
        responses.UserMessage("Actually, focus on the last quarter only."),
    },
})

The agent finishes the tool calls already running, then continues with the new instruction in context, so a correction lands without losing the work so far.

AG-UI

Agents are served to the browser over the AG-UI protocol β€” the standard event-stream protocol that frontend agent frameworks (CopilotKit, raw @ag-ui/client, etc.) speak. The pkg/agui package translates the SDK's streaming chunks into canonical AG-UI events (text messages, reasoning, tool calls, steps, human-in-the-loop interrupts) over SSE:

import "github.com/hastekit/agent-sdk-go/pkg/agui"

// Agents register into a package-global registry when created.
hastekit.NewAgent(&hastekit.AgentConfig{
    Name: "Assistant",
    // ...
})

// AgentRegistry exposes the registered agents to the AG-UI handler.
registry := &hastekit.AgentRegistry{}

// Exposes:
//   GET  /agents                                   β†’ registered agent names
//   POST /agents/{agent}/run                       β†’ AG-UI run endpoint (SSE)
//   GET  /agents/{agent}/threads                   β†’ stored conversation threads, newest first
//   GET  /agents/{agent}/threads/{thread}/messages β†’ thread history as AG-UI messages
http.ListenAndServe(":8080", agui.NewHandler(registry))

// Or mount a single agent's run endpoint on an existing mux:
// mux.Handle("POST /assistant/run", agui.AgentHandler(agent))

For a zero-setup browser chat UI, the pkg/agui/web package embeds a ready-made CopilotKit chat into your binary with go:embed β€” no Node toolchain or separate frontend deploy needed to run it:

import "github.com/hastekit/agent-sdk-go/pkg/agui/web"

// Serves the embedded CopilotKit chat UI at / and the AG-UI protocol
// endpoints under /api/agui/*.
if err := web.Serve(":8080", &hastekit.AgentRegistry{}); err != nil {
    log.Fatal(err)
}

The embedded UI lists registered agents, shows a sidebar of prior conversations (select one to resume it on the same thread), streams assistant text, reasoning, and tool calls live, and renders CopilotKit's useInterrupt approval cards inline when a run pauses for human-in-the-loop tool approval.

Conversation listing works when the agent's persistence adapter implements history.ThreadLister β€” the SDK's built-in in-memory and file adapters both do. For adapters that can't enumerate threads, the listing endpoint answers 501 and the UI hides the picker.

The CopilotKit UI is a Vite/React app under pkg/agui/web/ui; its build output is committed to pkg/agui/web/static, so go build never needs Node. Rebuild only when changing the UI source (cd pkg/agui/web/ui && pnpm install && pnpm build). CopilotKit v2 can't be loaded from a public ESM CDN (its dependency graph breaks esm.sh/jsDelivr), so it's bundled. To keep the embedded weight down to ~1MB (from ~17MB), the build aliases out CopilotKit's heaviest optional dependencies β€” the markdown renderer's Shiki/Mermaid/Cytoscape stack (swapped for a lightweight react-markdown shim), KaTeX's math fonts, and the dev-console web-inspector β€” none of which the chat needs. An offline, framework-free fallback UI is embedded at /basic.html.

Options (shared by agui.NewHandler, agui.AgentHandler, and web.Serve):

web.Serve(":8080", client,
    agui.WithNamespace("user-123"), // conversation namespace (default "default")
    agui.WithSenderID("alice"),     // sender attribution (default "user")
    agui.WithFullHistory(),         // forward the client's full message list
                                    // (only for agents without persistence)
    agui.WithKeepalive(10*time.Second), // SSE keep-alive interval (default 15s)
)

Human-in-the-loop: when a run pauses for tool approval, the stream emits a CUSTOM event named on_interrupt (CopilotKit's useInterrupt convention) followed by RUN_FINISHED with result.status: "paused". The client resumes by POSTing decisions back on the same thread under forwardedProps.command.resume.decisions[] ({toolCallId, approved}).

Tools
MCP Tools Integration

Connect to MCP servers for access to standardized tools:

import "github.com/hastekit/agent-sdk-go/pkg/agents/mcpclient"

// Connect to MCP server
mcpClient, err := mcpclient.NewClient(
    context.Background(),
    "sample"
    "http://localhost:9001/sse",
    mcpclient.WithTransport("sse"), // or "streamable-http"
    mcpclient.WithHeaders(map[string]string{
        "Authorization": "Bearer token",
    }),
    mcpclient.WithToolFilter("list_users", "get_user"), // Optional: filter tools
)
if err != nil {
    log.Fatal(err)
}

// Create agent with MCP tools
agent := hastekit.NewAgent(&hastekit.AgentConfig{
    Name:        "MCP Agent",
    Instruction: hastekit.NewPrompt("You are a helpful assistant."),
    LLM:         model,
    McpServers:  []agents.MCPToolset{mcpClient},
})
MCP Servers over stdio

Many MCP servers ship as a command rather than a URL. WithCommand runs one as a child process and speaks to it over stdin/stdout β€” there is nothing to deploy, and the process is started on demand and reused across tool calls:

mcpClient, err := mcpclient.NewClient(context.Background(), "filesystem", "", // no endpoint
    mcpclient.WithCommand("npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"),
    mcpclient.WithEnv(map[string]string{
        "GITHUB_TOKEN": "{{github_token}}", // templated from the run context
    }),
)

WithCommand selects the stdio transport on its own. The environment is added to the one the host process already has, so the command stays findable on PATH.

Everything else is transport-agnostic: WithToolFilter, WithApprovalRequiredTools, WithDeferredTools, WithToolPrefix, and the schema cache all behave the same whichever transport carries the server.

Namespacing Tools from Several Servers

Two servers that both publish a search would collide in the single list of names the model chooses from. WithToolPrefix namespaces one server's tools in the name the model sees, while calls are still made on the server under its own name:

mcpclient.WithToolPrefix("fs__") // exposes "read_file" as "fs__read_file"

The prefix is used verbatim, separator included β€” pass "fs__", not "fs". Tool filters, approval, and deferred lists are written against the server's own names, so adding a prefix does not change them.

Tool Annotations

Tools can advertise what they do. The hints mirror MCP's tool annotations, so hints read off an MCP server and hints declared on a local function tool are the same thing β€” one policy can read both:

readTool := hastekit.NewTool(listUsers,
    hastekit.WithName("list_users"),
    hastekit.WithTitle("List users"), // human-readable, for UI
    hastekit.WithReadOnly(true),
)

writeTool := hastekit.NewTool(deleteUser,
    hastekit.WithName("delete_user"),
    hastekit.WithDestructive(true),
    hastekit.WithIdempotent(false),
    hastekit.WithOpenWorld(false),
)

MCP tools carry whatever their server declared; nothing extra is needed to pick them up. Read the hints back off the tool's descriptor:

if tool.GetToolDescriptor().Annotations.IsDestructive() {
    // gate it β€” see Hooks below
}

A tool call hook is handed the same descriptor, which is where a policy usually reads them.

Every hint is a pointer, so "nothing was said" stays distinguishable from "false was said". Prefer the Is* helpers over reading fields directly: they are nil-safe and apply MCP's defaults, which are deliberately conservative β€” an unset DestructiveHint reads as destructive, an unset ReadOnlyHint as not read-only.

Hints are self-reported: they describe intent, not enforcement. Never let a hint from an untrusted MCP server widen what a tool is allowed to do.

Skills

A skill is a folder of instructions the agent reads only when it needs them β€” a house style, a procedure, a checklist too long to keep in the system prompt every turn. Write one as a SKILL.md with YAML frontmatter, and put any supporting files beside it:

skills/
└── changelog/
    β”œβ”€β”€ SKILL.md
    └── references/
        └── style.md
---
name: changelog
description: Write a release changelog entry. Use whenever the user asks for release notes.
---

Group the changes under `Added`, `Changed`, `Fixed`, and `Removed`...
The full house style is in `references/style.md`.

Point the agent at that folder:

registry, err := hastekit.NewSkillRegistryFromDir("./skills")
if err != nil {
    log.Fatal(err)
}

agent := hastekit.NewAgent(&hastekit.AgentConfig{
    Name: "Release_Agent",
    Instruction: hastekit.NewPrompt(
        "You help maintain this project's releases.",
        prompts.WithResolver(prompts.DefaultResolvers()...), // ResolveSkills lists them
    ),
    Skills: registry,
    LLM:    model,
})

The agent lists the skills in its prompt and adds the tool that reads them to its own tools, so a prompt can never advertise a skill the model has no way to open. A prompt runs only the resolvers it is given, so one that leaves out ResolveSkills gets a model that never hears about them β€” see Prompt resolvers below.

The prompt carries only each skill's name and description. The model calls read_skill with a name to pull in the instructions, and read_skill with a file to pull in one of the bundled files β€” so a long skill costs context only on the turns it is actually used.

Pass several directories to draw from more than one library β€” a shared set plus this agent's own, say:

registry, err := hastekit.NewSkillRegistryFromDir("./skills", "/etc/agent/skills")

Reading happens once, at construction. To pick up edits on disk, build a new registry.

Shipping skills inside the binary

Where the skills are part of the program rather than of its deployment, go:embed puts the whole tree in the binary β€” no folder to mount, copy, or keep in sync:

//go:embed skills
var skillsFS embed.FS

registry, err := hastekit.NewSkillRegistry(skillsFS)

Embedding the parent folder is enough: a skill is found wherever a SKILL.md sits, so there is no fs.Sub to get right. NewSkillRegistry takes any fs.FS, so this is also the hook for skills that come from somewhere else entirely.

Rules

The name comes from the frontmatter, or from the folder when the frontmatter omits it. A folder holding a SKILL.md is one skill, and everything below it belongs to that skill β€” so a SKILL.md bundled as an example or a template stays a bundled file rather than becoming a second, half-formed skill.

Loading fails loudly on a skill with no description, on broken frontmatter, on a directory that isn't there, and on the same name defined twice. Skills decide how the agent behaves, so a bad one should stop startup rather than go quietly missing at runtime.

Only files a skill actually bundles are reachable through the tool: a path that tries to traverse out of the skill folder is refused, so one skill cannot read another or the rest of the filesystem the skills were read from.

Skills work the same under the Temporal and Restate runtimes: the durable agent registers and wraps the reader tool along with the rest, so a read_skill call is journaled like any other tool call and replays from the journal rather than re-reading the folder.

Skills from somewhere else

AgentConfig.Skills takes an agents.SkillProvider β€” a source that lists its skills, supplies the tool that reads them, and introduces them to the model:

type SkillProvider interface {
    Skills() []agents.Skill
    SkillTool() agents.Tool // nil when the model already has a way to read them
    SkillHint() string      // the prompt's prose: what they are, how to read one
}

The agent asks the source for all three, which is what keeps the prompt and the tools in step. SkillHint is the whole of the section's prose and goes in verbatim β€” the resolver writes the ## Skills heading and the catalogue, nothing else. Only the provider can write that hint honestly: a SkillRegistry names its own read_skill tool, while a host serving skills its own way names whatever the model actually has. Say nothing and the model gets the bare catalogue, which beats a prompt naming a tool the agent does not have.

A source that returns no tool is one the model can already reach. agents.SkillList lists such skills and adds nothing:

Skills: agents.SkillList{{Name: "changelog", Description: "Write a release changelog entry."}},

agents.SkillsWithHint is the same, plus the prose β€” for a host that serves skill files through a tool of its own:

Skills: agents.SkillsWithHint{
    List: agents.SkillList{{
        Name:         "changelog",
        Description:  "Write a release changelog entry.",
        FileLocation: "/skills/changelog/SKILL.md",
    }},
    Hint: "Skills are specialised instructions for particular kinds of work. " +
        "Read one with the `read_file` tool at the location listed below.",
},
Prompt resolvers

The system prompt is built by a chain of resolvers, each handed what the last produced along with the run's dependencies:

type PromptResolverFn func(prompt string, deps *agents.Dependencies) (string, error)

A prompt starts with an empty chain and is used exactly as written β€” nothing appended, no templating. prompts.DefaultResolvers() is the standard set: ResolveSkills, ResolveHandoffs, ResolveDeferredTools, ResolveTemplate β€” the sections the agent contributes, then the {{ placeholder }} pass over the whole thing. Pass what you want, in the order you want; repeated calls accumulate:

hastekit.NewPrompt("You help maintain this project's releases.",
    prompts.WithResolver(prompts.DefaultResolvers()...),
    prompts.WithResolver(func(prompt string, deps *agents.Dependencies) (string, error) {
        return prompt + "\n\n## House rules\n\nBe brief.", nil
    }),
)
Hooks

A hook wraps what the agent does, so cross-cutting concerns β€” auth, budgets, quotas, audit, approval policy β€” live in one place instead of inside every tool. Hooks can observe, or answer in place of the real call.

ToolCallHook wraps every tool call; ModelCallHook wraps every call to the model. hastekit.Hook is both. Implement only the half you care about by embedding the no-op other half:

// A budget check that has no interest in tools.
type credits struct {
    agents.NoopToolCallHook // supplies the tool-call half
}

func (c *credits) GetName() string { return "credits" }

func (c *credits) BeforeModelCall(ctx context.Context, call *agents.ModelCall) (agents.ModelCallHookResult, error) {
    if balanceFor(call.RunContext) <= 0 {
        // Answering is kinder than failing: the run ends with a message the
        // user can read rather than an error they cannot.
        return agents.HandleModelCall(
            agents.ModelCallText("You're out of credits β€” top up to continue."),
        ), nil
    }
    return agents.ContinueModelCall(), nil
}

func (c *credits) AfterModelCall(ctx context.Context, call *agents.ModelCall, res *agents.ModelCallResult) (agents.ModelCallHookResult, error) {
    recordSpend(call.RunContext, res.Usage) // res.Usage is this one call
    return agents.ContinueModelCall(), nil
}

agent := hastekit.NewAgent(&hastekit.AgentConfig{
    Name:  "Assistant",
    LLM:   client.Model("OpenAI/gpt-4o-mini"),
    Tools: []hastekit.Tool{weatherTool},
    Hooks: []agents.Hook{&credits{}},
})

The tool-call side is the same, with the tool the call is against handed over alongside it. Combined with annotations, a policy hook is a few lines:

type policy struct {
    agents.NoopModelCallHook // model-call half; this hook only guards tools
}

func (p *policy) GetName() string { return "policy" }

func (p *policy) BeforeToolCall(ctx context.Context, tool *agents.BaseTool, call *agents.ToolCall) (agents.ToolCallHookResult, error) {
    if tool.Annotations.IsDestructive() && !allowed(call.RunContext, call.Name) {
        // Short-circuit: the tool never runs, and this stands in as its output.
        return agents.HandleToolCall(
            agents.ToolCallResult(call, "Denied by policy."),
        ), nil
    }
    return agents.ContinueToolCall(), nil
}

func (p *policy) AfterToolCall(ctx context.Context, tool *agents.BaseTool, call *agents.ToolCall, resp *agents.ToolCallResponse) (agents.ToolCallHookResult, error) {
    audit(call.Name, call.RunContext)
    return agents.ContinueToolCall(), nil
}

Notes:

  • Handled is explicit. ContinueToolCall() passes the call along; HandleToolCall(resp) says the hook answered and the real call never happens. It's a flag rather than a nil check, because "I answered, and the answer is nothing to say" differs from "carry on without me".
  • Run context comes along. call.RunContext is the per-run map you set on AgentInput, so per-tenant data (a JWT, an org id) is available without threading it through every tool.
  • The tool arrives as plain data. tool is the same *agents.BaseTool its GetToolDescriptor returns β€” name, schema, annotations, meta β€” because the real tool may be a proxy for one running in another process. It is always non-nil.
  • GetName() must be unique per agent and stable across deploys. Durable runtimes name each hook's journaled step after it, so a renamed hook is a new step on replay.
  • Hooks run as their own durable steps. Under Restate or Temporal each hook call is journaled, so a check that talks to a billing service is not re-run on every replay.
  • A BeforeModelCall hook sees the shape of the call, not the prompt β€” model, tenant, loop iteration, ContextTokens, and usage so far. That's what a budget check needs, and it keeps the conversation from crossing a durable boundary twice.
Conversation History

Enable conversation memory across interactions:

// Create a file-backed conversation manager
memory := hastekit.NewFileHistory("./conversations")

agent := hastekit.NewAgent(&hastekit.AgentConfig{
    Name:        "Memory Agent",
    Instruction: hastekit.NewPrompt("You are a helpful assistant."),
    LLM:         model,
    History:     memory, // Enable history
})

threadID := uuid.NewString()

// First interaction
handle, err := agent.Execute(context.Background(), &agents.AgentInput{
    Namespace: "user-123", // Bucket conversations by namespace
    ThreadID:  threadID,
    Message: history.Message{
        Messages: []responses.InputMessageUnion{
            responses.UserMessage("My name is Alice."),
        },
    },
})
out, err := handle.Result()

// Continue conversation β€” pass the same ThreadID to keep context.
handle, err = agent.Execute(context.Background(), &agents.AgentInput{
    Namespace: "user-123",
    ThreadID:  threadID,
    Message: history.Message{
        Messages: []responses.InputMessageUnion{
            responses.UserMessage("What's my name?"),
        },
    },
})
out, err = handle.Result()

Passing ThreadID alone continues from the thread's tip. To branch from a specific earlier turn instead β€” a retry, or an edit of an earlier message β€” set PreviousRunID to the RunID of the run you want to continue from:

out, _ := handle.Result()

handle, err = agent.Execute(ctx, &agents.AgentInput{
    Namespace:     "user-123",
    ThreadID:      threadID,
    PreviousRunID: out.RunID, // continue from this run, not the thread tip
    Message:       history.Message{ /* ... */ },
})
Reading a thread back

To render a thread β€” a chat window, an audit view β€” use LoadTranscript:

transcript, err := memory.LoadTranscript(ctx, "user-123", threadID)

It returns the thread as written. That is deliberately not what the agent reads for itself: the agent's own history load is summary-aware, replacing the turns a summary covers with the summary, which is what keeps a long thread inside the context window. Right for the model, wrong for a UI β€” asking the summary-aware path for a whole summarized thread is exactly when the substitution kicks in, and the window loses its own early turns.

Adapters implement history.TranscriptReader to support this; the built-in in-memory and file adapters both do.

Durable Agents

Create fault-tolerant agents that survive crashes and failures:

A durable agent is a regular agent with a durable Runtime attached via hastekit.WithRuntime. Create the runtime, build the agent, then start the runtime; invoke agents over HTTP with hastekit.NewHTTPHandler().

Using Restate
client := hastekit.NewLLMClient([]hastekit.ProviderConfig{
    {
        ProviderName: hastekit.ProviderOpenAI,
        ApiKeys: []*hastekit.APIKeyConfig{
            {Name: "default", APIKey: os.Getenv("OPENAI_API_KEY")},
        },
    },
})

// Restate service bind address + Redis for streaming
rt, err := hastekit.NewRestateRuntime("0.0.0.0:9081", "localhost:6379")
if err != nil {
    log.Fatal(err)
}
broker, err := hastekit.NewRedisStreamBroker("localhost:6379")
if err != nil {
    log.Fatal(err)
}

// Create durable agent
agent := hastekit.NewAgent(&hastekit.AgentConfig{
    Name:        "DurableAgent",
    Instruction: hastekit.NewPrompt("You are a helpful assistant."),
    LLM:         client.Model("OpenAI/gpt-4o-mini"),
    History:     hastekit.NewFileHistory("./conversations"),
}, hastekit.WithRuntime(rt, broker))

// Start Restate service, then serve the invoke endpoint
rt.Start()
http.ListenAndServe(":8070", hastekit.NewHTTPHandler())

// Register deployment with Restate server
// restate deployments register http://localhost:9081
Using Temporal
client := hastekit.NewLLMClient([]hastekit.ProviderConfig{
    {
        ProviderName: hastekit.ProviderOpenAI,
        ApiKeys: []*hastekit.APIKeyConfig{
            {Name: "default", APIKey: os.Getenv("OPENAI_API_KEY")},
        },
    },
})

// Temporal server endpoint + Redis for streaming
rt, err := hastekit.NewTemporalRuntime("localhost:7233", "localhost:6379")
if err != nil {
    log.Fatal(err)
}
broker, err := hastekit.NewRedisStreamBroker("localhost:6379")
if err != nil {
    log.Fatal(err)
}

// Create Temporal agent
agent := hastekit.NewAgent(&hastekit.AgentConfig{
    Name:        "TemporalAgent",
    Instruction: hastekit.NewPrompt("You are a helpful assistant."),
    LLM:         client.Model("OpenAI/gpt-4o-mini"),
}, hastekit.WithRuntime(rt, broker))

// Start the Temporal worker, then serve the invoke endpoint
rt.Start()
http.ListenAndServe(":8070", hastekit.NewHTTPHandler())

Documentation

Examples

Explore complete working examples in the documentation repository:

Agents

Supported Providers

Provider Text Streaming Tool Calling Vision Embeddings Image Gen Image Edit Speech Transcription
OpenAI βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
Anthropic βœ… βœ… βœ… βœ… ❌ ❌ ❌ ❌ ❌
Gemini βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
xAI βœ… βœ… βœ… βœ… ❌ βœ… βœ… βœ…ΒΉ ❌
Bedrock βœ… βœ… βœ… βœ… ❌ ❌ ❌ ❌ ❌
ElevenLabs ❌ ❌ ❌ ❌ ❌ ❌ ❌ βœ… βœ…
Sarvam βœ…Β² βœ…Β² βœ…Β² βœ…Β² ❌ ❌ ❌ βœ…Β³ βœ…
DeepSeek βœ…Β² βœ…Β² βœ…Β² βœ…Β² ❌ ❌ ❌ ❌ ❌
Moonshot (Kimi) βœ…Β² βœ…Β² βœ…Β² βœ…Β² ❌ ❌ ❌ ❌ ❌
Z.ai (GLM) βœ…Β² βœ…Β² βœ…Β² βœ…Β² ❌ ❌ ❌ ❌ ❌

ΒΉ Non-streaming only β€” xAI has no NewStreamingSpeech. Β² Served by the chat-completions bridge (see below); vision depends on the model. Β³ NewStreamingSpeech synthesizes in one call and emits a single audio delta β€” Sarvam's incremental TTS is a WebSocket API, not an HTTP stream.

Text is the Responses API (NewResponses), which is what agents use. OpenAI additionally implements the older Chat Completions API (NewChatCompletion / NewStreamingChatCompletion); so do the bridged providers below.

Sarvam, DeepSeek, Moonshot and Z.ai speak the OpenAI /chat/completions format but have no /responses endpoint. They are served by providers/openaicompat, a generic translation in both directions: a native Responses request becomes a chat completion (instructions β†’ system message, parallel function calls collapsed onto one assistant message, tool results β†’ tool messages, text.format β†’ response_format), and the reply β€” including a streamed one β€” is reassembled into Responses output items and events. reasoning_content becomes a native reasoning item. Server-side tools (web search, image generation, code interpreter) have no equivalent and are dropped from the request. Default base URLs: Sarvam https://api.sarvam.ai, DeepSeek https://api.deepseek.com, Moonshot https://api.moonshot.ai/v1, Z.ai https://api.z.ai/api/paas/v4 β€” each overridable through BaseURL on the provider config (Moonshot's mainland endpoint, Z.ai's Coding Plan or Zhipu endpoints, a self-hosted proxy).

Any other OpenAI-compatible endpoint can be added the same way: point openaicompat.NewClient at its base URL.

ProviderOllama and ProviderOpenRouter are not in the table because their capabilities aren't the SDK's to state: both are served by the OpenAI client, so every method is wired up and each call is passed straight through. What actually answers depends on the endpoint and the model behind it. OpenRouter defaults to https://openrouter.ai/api/v1; Ollama needs an explicit BaseURL on its provider config.

A ❌ is not a graceful "unsupported" error. Unimplemented methods fall through to the embedded base provider, where the text and embedding methods panic and the media methods return (nil, nil) β€” so a call for a capability a provider doesn't have will either crash or hand back a silent nil. Check this table before reaching for a non-text method on a non-OpenAI provider.

Architecture

agent-sdk-go/
└── pkg/
    β”œβ”€β”€ agents/              # Agent orchestration, hooks, tool annotations
    β”‚   β”œβ”€β”€ runtime/         # Durable execution runtimes
    β”‚   β”‚   β”œβ”€β”€ restate_runtime/
    β”‚   β”‚   └── temporal_runtime/
    β”‚   β”œβ”€β”€ agentstate/      # Run status and state
    β”‚   β”œβ”€β”€ history/         # Conversation management
    β”‚   β”œβ”€β”€ mcpclient/       # MCP tool integration
    β”‚   β”œβ”€β”€ prompts/         # Prompt construction
    β”‚   β”œβ”€β”€ sandbox/         # Sandboxed execution
    β”‚   β”œβ”€β”€ streambroker/    # Stream brokers (memory, Redis)
    β”‚   └── tools/           # Built-in tools
    β”œβ”€β”€ agui/                # AG-UI protocol + embedded chat UI
    β”œβ”€β”€ gateway/             # LLM gateway
    β”‚   β”œβ”€β”€ llm/             # LLM request/response types
    β”‚   └── providers/       # Provider implementations
    β”‚       β”œβ”€β”€ openai/      # anthropic, gemini, xai, bedrock,
    β”‚       β”œβ”€β”€ openaicompat/# chat-completions <-> responses bridge
    β”‚       └── sarvam/      # deepseek, moonshot, zai, elevenlabs, ...
    β”œβ”€β”€ hastekitgateway/     # HasteKit Gateway adapters
    β”œβ”€β”€ knowledge/           # Knowledge / retrieval
    β”œβ”€β”€ telemetry/           # Tracing and metrics
    └── utils/               # Utilities

Runnable examples live in the documentation repository β€” see Examples above.

Contributing

We welcome contributions! Please see our contributing guidelines for details.

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Support


Documentation ΒΆ

Index ΒΆ

Constants ΒΆ

This section is empty.

Variables ΒΆ

View Source
var (
	ProviderOpenAI     = llm.ProviderNameOpenAI
	ProviderAnthropic  = llm.ProviderNameAnthropic
	ProviderBedrock    = llm.ProviderNameBedrock
	ProviderElevenLabs = llm.ProviderNameElevenLabs
	ProviderGemini     = llm.ProviderNameGemini
	ProviderXAI        = llm.ProviderNameXAI
	ProviderOllama     = llm.ProviderNameOllama
	ProviderOpenRouter = llm.ProviderNameOpenRouter
	ProviderSarvam     = llm.ProviderNameSarvam
	ProviderDeepSeek   = llm.ProviderNameDeepSeek
	ProviderMoonshot   = llm.ProviderNameMoonshot // Kimi models
	ProviderZAI        = llm.ProviderNameZAI      // GLM models
)
View Source
var (
	DefaultStreamBroker = streambroker.NewMemoryStreamBroker()
)
View Source
var NewHandoff = agents.NewHandoff
View Source
var NewMCPClient = mcpclient.NewClient
View Source
var NewPrompt = prompts.New
View Source
var NewPromptWithLoader = prompts.NewWithLoader
View Source
var NewSkillRegistry = agents.NewSkillRegistry

NewSkillRegistry loads every skill in the given filesystems β€” an embed.FS, so the skills ship inside the binary, or anything else that implements fs.FS:

//go:embed skills
var skillsFS embed.FS

registry, err := hastekit.NewSkillRegistry(skillsFS)
View Source
var NewSkillRegistryFromDir = agents.NewSkillRegistryFromDir

NewSkillRegistryFromDir loads every skill in a directory on disk:

registry, err := hastekit.NewSkillRegistryFromDir("./skills")

agent := hastekit.NewAgent(&hastekit.AgentConfig{
	Instruction: hastekit.NewPrompt("..."),
	Skills:      registry,
})

Functions ΒΆ

func NewOutputFormatJSONSchema ΒΆ

func NewOutputFormatJSONSchema(v any, strict bool) map[string]any

func NewOutputSchema ΒΆ

func NewOutputSchema(v any) map[string]any

func NewRedisStreamBroker ΒΆ

func NewRedisStreamBroker(redisEndpoint string, password string, db int) (agents.StreamBroker, error)

func NewStreamBroker ΒΆ

func NewStreamBroker() (agents.StreamBroker, error)

Types ΒΆ

type APIKeyConfig ΒΆ

type APIKeyConfig = gateway.APIKeyConfig

type Agent ΒΆ

type Agent = agents.Agent

func NewAgent ΒΆ

func NewAgent(cfg *AgentConfig, opts ...AgentOption) *Agent

NewAgent creates a new agent with the given configuration

type AgentConfig ΒΆ

type AgentConfig struct {
	Name          string
	LLM           llm.Provider
	Output        map[string]any
	Tools         []Tool
	Handoffs      []*agents.Handoff
	McpServers    []agents.MCPToolset
	MaxLoops      *int
	History       *history.CommonConversationManager
	Instruction   agents.SystemPromptProvider
	Parameters    responses.Parameters
	StickyHandoff bool

	// Skills are folders of instructions the agent reads only when it needs
	// them β€” see NewSkillRegistryFromDir. The agent lists them in its prompt
	// and adds the reader tool to Tools itself.
	Skills agents.SkillProvider

	// Hooks observe or intercept what the agent does. A ToolCallHook wraps
	// every tool it calls; a ModelCallHook wraps every call to the model, which
	// is where a budget or credit check belongs. One hook may be both.
	Hooks []agents.Hook
}

type AgentOption ΒΆ

type AgentOption func(options *agents.AgentOptions)

func WithRuntime ΒΆ

func WithRuntime(runtime agents.Runtime, broker agents.StreamBroker) AgentOption

type AgentRegistry ΒΆ

type AgentRegistry struct {
}

func (*AgentRegistry) Agent ΒΆ

func (_ *AgentRegistry) Agent(name string) (*agents.Agent, bool)

Agent returns a registered agent by name.

func (*AgentRegistry) AgentNames ΒΆ

func (_ *AgentRegistry) AgentNames() []string

AgentNames returns the names of all registered agents, sorted.

type Config ΒΆ

type Config struct {
	// ProviderConfigs is the provider configs, used for LLM calls
	ProviderConfigs []ProviderConfig
}

type FunctionTool ΒΆ

type FunctionTool[T any, S any] struct {
	// contains filtered or unexported fields
}

func NewTool ΒΆ

func NewTool[T any, S any](fn ToolFunc[T, S], opts ...ToolOption) *FunctionTool[T, S]

func (*FunctionTool[T, S]) Execute ΒΆ

func (t *FunctionTool[T, S]) Execute(ctx context.Context, params *agents.ToolCall) (*agents.ToolCallResponse, error)

func (*FunctionTool[T, S]) GetAnnotations ΒΆ

func (t *FunctionTool[T, S]) GetAnnotations() *agents.ToolAnnotations

GetAnnotations implements ToolConfig, which is how the per-hint options (WithReadOnly, WithDestructive, ...) amend the set rather than clobber it.

func (*FunctionTool[T, S]) GetMeta ΒΆ added in v0.0.14

func (t *FunctionTool[T, S]) GetMeta() map[string]any

GetMeta implements ToolConfig, letting WithMeta add to what is already there.

func (*FunctionTool[T, S]) GetToolDescriptor ΒΆ added in v0.0.16

func (t *FunctionTool[T, S]) GetToolDescriptor() *agents.BaseTool

GetToolDescriptor implements agents.Tool. A function tool keeps its state in its own fields rather than an embedded BaseTool, so the projection is built here β€” schema included, from the input type's own shape. Name is left empty: the tool has one name, and it is the one in ToolUnion.

func (*FunctionTool[T, S]) SetAnnotations ΒΆ

func (t *FunctionTool[T, S]) SetAnnotations(annotations *agents.ToolAnnotations)

SetAnnotations replaces the tool's annotations wholesale. The hint options (WithReadOnly, WithDestructive, ...) go through here too, each filling in one field of the current set.

func (*FunctionTool[T, S]) SetDeferred ΒΆ

func (t *FunctionTool[T, S]) SetDeferred(deferred bool)

func (*FunctionTool[T, S]) SetDescription ΒΆ

func (t *FunctionTool[T, S]) SetDescription(description string)

func (*FunctionTool[T, S]) SetMeta ΒΆ added in v0.0.14

func (t *FunctionTool[T, S]) SetMeta(meta map[string]any)

func (*FunctionTool[T, S]) SetName ΒΆ

func (t *FunctionTool[T, S]) SetName(name string)

func (*FunctionTool[T, S]) SetNeedsApproval ΒΆ

func (t *FunctionTool[T, S]) SetNeedsApproval(needsApproval bool)

type HTTPHandler ΒΆ

type HTTPHandler struct{}

func NewHTTPHandler ΒΆ

func NewHTTPHandler() *HTTPHandler

func (*HTTPHandler) ServeHTTP ΒΆ

func (h *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

type Handoff ΒΆ

type Handoff = agents.Handoff

type History ΒΆ

func NewFileHistory ΒΆ

func NewFileHistory(path string, opts ...history.ConversationManagerOptions) *History

type Hook ΒΆ

type Hook = agents.Hook

Hook is anything an agent can be given to observe or intercept what it does β€” see agents.Hook. Implement ToolCallHook, ModelCallHook, or both.

type LLMClient ΒΆ

type LLMClient struct {
	// contains filtered or unexported fields
}

func NewLLMClient ΒΆ

func NewLLMClient(configs []ProviderConfig) *LLMClient

func (*LLMClient) Model ΒΆ

func (c *LLMClient) Model(id string) llm.Provider

type Model ΒΆ

type Model struct {
	// contains filtered or unexported fields
}

type ModelCallHook ΒΆ

type ModelCallHook = agents.ModelCallHook

ModelCallHook wraps a call to the model β€” see agents.ModelCallHook.

type ModelParameters ΒΆ

type ModelParameters = responses.Parameters

type OutputSchema ΒΆ

type OutputSchema = jsonschema.Schema

type ProviderConfig ΒΆ

type ProviderConfig = gateway.ProviderConfig

type ProviderName ΒΆ

type ProviderName = llm.ProviderName

type RestateRuntime ΒΆ

type RestateRuntime struct {
	*restate_runtime.RestateRuntime
	// contains filtered or unexported fields
}

func NewRestateRuntime ΒΆ

func NewRestateRuntime(restateEndpoint string, broker agents.StreamBroker) (*RestateRuntime, error)

func (*RestateRuntime) Start ΒΆ

func (r *RestateRuntime) Start()

type Skill ΒΆ

type Skill = agents.Skill

Skill is one folder of instructions the agent can pull in on demand β€” see agents.Skill.

type SkillList ΒΆ

type SkillList = agents.SkillList

SkillList is a SkillProvider for skills the agent can already reach by other means, such as ones staged into its sandbox. It lists them and adds no tool, and says nothing about what they are or how to read them β€” use SkillsWithHint to say.

type SkillProvider ΒΆ

type SkillProvider = agents.SkillProvider

SkillProvider is where an agent's skills come from. Set one on AgentConfig.Skills and the agent does the rest: it lists the skills in its prompt and adds the provider's reader tool to its own tools.

type SkillRegistry ΒΆ

type SkillRegistry = agents.SkillRegistry

SkillRegistry holds the skills read out of one or more folders β€” see agents.SkillRegistry. It is a SkillProvider.

type SkillsWithHint ΒΆ

type SkillsWithHint = agents.SkillsWithHint

SkillsWithHint is a SkillProvider for skills your own host serves: it lists them, adds no tool, and introduces them to the model in your words.

type TemporalRuntime ΒΆ

type TemporalRuntime struct {
	*temporal_runtime.TemporalRuntime
	// contains filtered or unexported fields
}

func NewTemporalRuntime ΒΆ

func NewTemporalRuntime(temporalEndpoint string, broker agents.StreamBroker) (*TemporalRuntime, error)

NewTemporalRuntime creates a new Temporal runtime

func (*TemporalRuntime) Start ΒΆ

func (r *TemporalRuntime) Start()

type Tool ΒΆ

type Tool = agents.Tool

type ToolAnnotations ΒΆ

type ToolAnnotations = agents.ToolAnnotations

ToolAnnotations describes what a tool does β€” read-only, destructive, idempotent, open-world β€” in the same shape MCP servers use, so a function tool and an MCP tool feed the same permission policy.

type ToolCallHook ΒΆ

type ToolCallHook = agents.ToolCallHook

ToolCallHook wraps a tool call β€” see agents.ToolCallHook.

type ToolConfig ΒΆ

type ToolConfig interface {
	SetName(string)
	SetDescription(string)
	SetNeedsApproval(bool)
	SetDeferred(bool)
	SetAnnotations(*agents.ToolAnnotations)
	// GetAnnotations lets the per-hint options amend the current annotations
	// instead of overwriting them, so WithReadOnly and WithIdempotent can be
	// passed to the same tool.
	GetAnnotations() *agents.ToolAnnotations

	SetMeta(map[string]any)
	// GetMeta lets WithMeta add to what is already there, for the same reason
	// GetAnnotations exists.
	GetMeta() map[string]any
}

type ToolFunc ΒΆ

type ToolFunc[T any, S any] func(ctx context.Context, in T) (S, error)

type ToolOption ΒΆ

type ToolOption func(ToolConfig)

func WithAnnotations ΒΆ

func WithAnnotations(annotations *agents.ToolAnnotations) ToolOption

WithAnnotations sets the tool's behavioural hints in one go. Prefer the single-hint options below unless you already have a full set in hand.

func WithDeferred ΒΆ

func WithDeferred(deferred bool) ToolOption

func WithDescription ΒΆ

func WithDescription(desc string) ToolOption

func WithDestructive ΒΆ

func WithDestructive(destructive bool) ToolOption

WithDestructive declares whether the tool may destroy or overwrite state, as opposed to only adding to it. A tool that says nothing counts as destructive.

func WithIdempotent ΒΆ

func WithIdempotent(idempotent bool) ToolOption

WithIdempotent declares that repeating the call with the same arguments has no additional effect.

func WithMeta ΒΆ added in v0.0.14

func WithMeta(meta map[string]any) ToolOption

WithMeta attaches metadata to the tool. It is not sent to the model and has no meaning to the SDK: it rides along on the BaseTool, which is what a tool call hook is shown, so a policy can key off where a tool came from or what it belongs to without having to recognise it by name.

Repeated use merges rather than replaces, so several options can each contribute a key, and a later one wins on a key both set. It works on a copy: a map handed to several tools must not pick up one tool's keys on another.

func WithName ΒΆ

func WithName(name string) ToolOption

func WithNeedsApproval ΒΆ

func WithNeedsApproval(needsApproval bool) ToolOption

func WithOpenWorld ΒΆ

func WithOpenWorld(openWorld bool) ToolOption

WithOpenWorld declares whether the tool reaches outside a closed, known set of entities β€” a web search does, a lookup in local memory does not.

func WithReadOnly ΒΆ

func WithReadOnly(readOnly bool) ToolOption

WithReadOnly declares that the tool does not modify anything. This is what a permission policy keys off to let a call run unattended, so only claim it when the tool truly has no side effects.

func WithTitle ΒΆ

func WithTitle(title string) ToolOption

WithTitle sets a human-readable name for the tool, for UI use. The model still sees the tool by its name.

Directories ΒΆ

Path Synopsis
examples
agents/14_agent_with_stdio_mcp_tools command
An agent whose tools come from an MCP server that runs as a child process rather than as a service β€” the stdio transport.
An agent whose tools come from an MCP server that runs as a child process rather than as a service β€” the stdio transport.
speech/1_speech command
pkg
agents/runtime/local_runtime
Package local_runtime provides a goroutine-based agent runtime that streams via a StreamBroker, mirroring the structure of the Temporal and Restate runtimes.
Package local_runtime provides a goroutine-based agent runtime that streams via a StreamBroker, mirroring the structure of the Temporal and Restate runtimes.
agui
Package agui implements the AG-UI Protocol (https://github.com/ag-ui-protocol/ag-ui) as a serving surface for agents.
Package agui implements the AG-UI Protocol (https://github.com/ag-ui-protocol/ag-ui) as a serving surface for agents.
agui/web
Package web embeds a ready-made browser chat client for the AG-UI protocol and serves it alongside the protocol endpoints.
Package web embeds a ready-made browser chat client for the AG-UI protocol and serves it alongside the protocol endpoints.
gateway/providers/deepseek
Package deepseek is the provider client for DeepSeek.
Package deepseek is the provider client for DeepSeek.
gateway/providers/moonshot
Package moonshot is the provider client for Moonshot AI, which serves the Kimi model family.
Package moonshot is the provider client for Moonshot AI, which serves the Kimi model family.
gateway/providers/openaicompat
Package openaicompat is a provider client for any service that speaks the OpenAI /chat/completions wire format but does not implement the /responses API β€” Sarvam, DeepSeek, Kimi (Moonshot) and GLM (Z.ai) today.
Package openaicompat is a provider client for any service that speaks the OpenAI /chat/completions wire format but does not implement the /responses API β€” Sarvam, DeepSeek, Kimi (Moonshot) and GLM (Z.ai) today.
gateway/providers/sarvam
Package sarvam is the provider client for Sarvam AI - chat (the OpenAI-compatible /v1/chat/completions endpoint, bridged to the native Responses shape), text-to-speech and speech-to-text.
Package sarvam is the provider client for Sarvam AI - chat (the OpenAI-compatible /v1/chat/completions endpoint, bridged to the native Responses shape), text-to-speech and speech-to-text.
gateway/providers/zai
Package zai is the provider client for Z.ai (Zhipu AI), which serves the GLM model family.
Package zai is the provider client for Z.ai (Zhipu AI), which serves the GLM model family.
genai
Package genai holds the OpenTelemetry GenAI semantic-convention attribute keys and well-known values used across the SDK and the gateway.
Package genai holds the OpenTelemetry GenAI semantic-convention attribute keys and well-known values used across the SDK and the gateway.
genai/agentcore
Package agentcore exports OpenTelemetry spans to Amazon Bedrock AgentCore / CloudWatch GenAI Observability without an ADOT collector.
Package agentcore exports OpenTelemetry spans to Amazon Bedrock AgentCore / CloudWatch GenAI Observability without an ADOT collector.
knowledge/textsplitters
Package textsplitters provides text chunking by character length, token length, and allows adding more advanced strategies (semantic, sentence-aware, etc.) via the TextSplitter interface.
Package textsplitters provides text chunking by character length, token length, and allows adding more advanced strategies (semantic, sentence-aware, etc.) via the TextSplitter interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL