weft

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 18 Imported by: 0

README

weft

CI Go Reference Version

A thin, opinionated core for building AI agents in Go — designed the way the standard library is: small interfaces, context everywhere, functional options, wrapped errors, and zero required configuration.

Weft runs the agent loop — call a model, execute its tool calls in parallel with defined failure semantics, stream typed events while work is in flight — and nothing else. Concurrency is the point, not a feature: a step's tools fan out over goroutines, parallelism is a one-line dial, and tool failures never cancel their siblings.

Status: v0.1.0 — experimental, pre-1.0. The three load-bearing contracts — message model, error model, tool contract — are implemented and tested, and the first provider adapters (OpenAI + compatible servers, Anthropic, Google) wrap the vendors' official Go SDKs; see docs/adr/. Middleware seams, MCP interop, and the surrounding modules (serving, ops, devtools, cli) come next, in that order of demand.

Quick start

A tool is a plain function — the JSON Schema is reflected from the input struct. Offline, the model comes from wefttest: a scripted, deterministic stand-in for a real provider (an adapter slots into the same Model seam):

model := wefttest.Script(
    wefttest.ToolCalls(wefttest.Call{Name: "echo", Args: `{"msg":"hello"}`}),
    wefttest.Say("HELLO"),
)
agt := weft.New(
    model,                                   // or openai.Model("gpt-4o-mini") — see Providers
    weft.Instructions("You are a support agent."),
    echo,
)

res, err := agt.Generate(ctx, weft.Prompt("Echo hello."))
fmt.Println(res.Text(), res.Usage.Total())

where echo is defined once and reused:

echo := weft.Tool("echo", "Echo a message back, uppercased.",
    func(ctx context.Context, in struct {
        Msg string `json:"msg" jsonschema:"the message to echo"`
    }) (string, error) {
        return strings.ToUpper(in.Msg), nil
    })

Streaming is Go iteration — typed events, cancel via context, exactly one terminal error:

for ev, err := range agt.Stream(ctx, weft.Prompt("Echo hello.")).Events() {
    if err != nil {
        return err
    }
    switch ev := ev.(type) {
    case weft.TextDelta:
        io.WriteString(w, ev.Text)
    case weft.ToolStart:
        slog.Info("tool", "name", ev.Name, "seq", ev.Seq)
    case weft.RunFinish:
        slog.Info("done", "steps", ev.Steps, "tokens", ev.Usage.Total())
    }
}

Ending a run is a predicate, and the step budget is a separate safety net:

agt := weft.New(model,
    weft.StopWhen(weft.HasToolCall("submit_answer")), // intended end
    weft.MaxSteps(20),                                // runaway guard → ErrMaxSteps
    submit, search,
)

The manifest — weft.json

One generated, committed, diffable description of every agent and tool (the code stays the only source of truth; the file is output, never input). Gate it with a golden test so it cannot go stale:

func TestManifest(t *testing.T) {
    b, err := weft.Manifest(newAgent())
    if err != nil {
        t.Fatal(err)
    }
    wefttest.Golden(t, "weft.json", b) // regenerate: go test ./... -update
}

A tool or policy change without regenerating fails go test; the diff is the review artifact. (ADR 0012)

Providers

First-party adapters wrap the vendors' official Go SDKs — weft never owns an HTTP client — and are versioned as their own modules. One line per vendor, one adapter for the whole OpenAI-compatible long tail:

import (
    "github.com/weftgo/weft/anthropic"
    "github.com/weftgo/weft/google"
    "github.com/weftgo/weft/openai"
)

openai.Model("gpt-4o-mini")                          // or any compatible server via openai.BaseURL
anthropic.Model("claude-sonnet-4-5", anthropic.Thinking(true))
google.Model("gemini-2.5-flash")

Every adapter passes the same executable contract (wefttest/conformance): streaming tool-call fragments are assembled into whole calls, provider errors pass through unchanged for errors.As, cancellation surfaces as ctx.Err(), a stalled stream fails with ErrStreamIdle while a slow-but-streaming one never does, and WEFT_MODEL_REQUESTS=deny refuses every call before any network I/O — test suites that must stay offline get loud failures, not surprise bills. (ADR 0013)

The rules that matter

  • Tool error = data; run error = Go error. A failing (or panicking) tool becomes a result the model sees; siblings keep running. Only model failures, cancellation, and step exhaustion reach the caller, as *RunError with the partial transcript attached. (ADR 0002)
  • Messages are role + typed parts, with a versioned JSON contract: every part carries a type discriminator and transcripts round-trip through encoding/json. (ADR 0001)
  • Tools are generic functions whose schema derives from struct tags, shape-compatible with the official Go MCP SDK. (ADR 0003)
  • Concurrent tool events carry a total order (Seq), assigned and emitted atomically, so streams replay exactly. (ADR 0004)
  • Parallel by default, bounded (4); tools always start in call order; weft.Sequential() runs them one at a time for shared state; weft.Parallelism(n) for anything else.
  • String tool outputs are sent verbatim, everything else as JSON.
  • Every run has an id (RunStart, Run.ID(), RunResult.ID); every tool call can learn its own via weft.CallFromContext(ctx).
  • Truncation is visible, never silent: a max_tokens finish is recorded on RunResult.StopReason (the run still succeeds — callers decide what truncated text means), and oversized tool results are capped (64 KiB by default, weft.MaxResultBytes(n) to change, 0 to disable) with a marker the model sees.
  • The Model stream contract is enforced: a stream that ends without ModelFinish, continues after it, carries a tool call with an empty ID or name, or panics fails the run wrapping ErrModelContract — a broken adapter cannot corrupt a transcript.
  • No dependencies in the core module. The vendor SDKs live in the adapter modules (their own go.mod); the one dependency the design permits in the core is the OTel API package (a no-op tracer until an SDK registers), which lands with instrumentation; exporters stay in a satellite.

Layout

doc.go, message.go    message model (roles, parts, versioned JSON)
errors.go, env.go     error model (sentinels, RunError, kill switch)
tool.go, schema.go    tool contract + schema reflection
model.go              provider seam (streaming-first Model interface)
events.go             sealed run-event set
agent.go, run.go      agent construction options, run/stream/result
loop.go               the loop: model call → tool fan-out → repeat
wefttest/             scripted mock model + the conformance suite
openai/               OpenAI Chat Completions (+ compatible servers)
anthropic/            Anthropic Messages (thinking, signatures)
google/               Gemini via genai
examples/             runnable core example (per-adapter: <adapter>/example)
docs/adr/             decision records for the contracts

Development

make test   # go test -race ./... in every workspace module
make vet
make lint   # golangci-lint (CI uses .golangci.yml)
make live   # adapter conformance against real keys (-tags live)
make fmt

Requires Go 1.26 or newer; the current and previous Go releases are supported and both are tested in CI.

Roadmap

  1. First provider adapters — done (OpenAI + compatible servers, Anthropic, Google; ADR 0013).
  2. The two middleware seams (model call + tool call, chi-style).
  3. MCP interop: consume MCP servers as tools, expose weft tools as MCP.
  4. Subagents as tools; execution-policy refinements.
  5. The satellites: runtime (sessions, approvals), store, serve, studio, and the eval/prompt/mem/trace modules.

License

MIT — see LICENSE.

Documentation

Overview

Package weft is a thin, opinionated core for building agents in Go.

Weft runs the agent loop — call a model, execute its tool calls in parallel with defined failure semantics, stream typed events while work is in flight — and nothing else. It is designed the way the standard library is: small interfaces, context everywhere, functional options, wrapped errors, and no required configuration.

A tool is a plain function; its JSON Schema is derived from the input struct. An agent is a value built once with options and run many times:

echo := weft.Tool("echo", "Echo a message",
	func(ctx context.Context, in struct {
		Msg string `json:"msg"`
	}) (string, error) {
		return "echo: " + in.Msg, nil
	})

agt := weft.New(model, weft.Instructions("You are helpful."), echo)
res, err := agt.Generate(ctx, weft.Prompt("Say hi."))

A run ends when the model replies without tool calls or a StopWhen condition is met; MaxSteps is the safety budget behind both. Streaming is a range loop over typed events (Agent.Stream). Tool failures are data the model sees; only model failures, cancellation, and the step budget reach the caller, as *RunError.

The model seam (Model) is streaming-first; provider adapters translate vendor wire formats into weft's events. The wefttest package provides a scriptable Model for offline tests.

Index

Examples

Constants

View Source
const SchemaVersion = 1

SchemaVersion is the version of the message wire format. The JSON encoding of Message and its parts is a compatibility contract: within a version, field names and shapes change only additively. Persistence and serving layers envelope messages with this number; the core itself never needs it.

Variables

View Source
var (
	// ErrMaxSteps is returned when the model still requests tools after the
	// last allowed step. The partial transcript rides along on RunError.
	ErrMaxSteps = errors.New("weft: run exceeded the maximum number of steps")

	// ErrNoSuchTool is returned by Agent.CallTool when the call names a
	// tool the agent does not have. Inside the loop the same condition is
	// folded into an error tool result — data for the model to
	// self-correct, not a run failure.
	ErrNoSuchTool = errors.New("weft: no tool with that name")

	// ErrInvalidToolInput is returned by ToolDef.Invoke and Agent.CallTool
	// when the arguments do not decode into the tool's input type. Like
	// ErrNoSuchTool, the loop turns it into model-visible data.
	ErrInvalidToolInput = errors.New("weft: tool input is not valid for its schema")

	// ErrRunConsumed is returned by Run.Events when the event stream has
	// already been consumed; each Run yields exactly one sequence.
	ErrRunConsumed = errors.New("weft: run events already consumed")

	// ErrModelContract is wrapped around failures of a Model
	// implementation to honor the stream contract documented on Model:
	// events after ModelFinish, a stream ending without one, a tool call
	// with an empty ID or name, or a panicking stream. It signals an
	// adapter bug, not a model outage — providers' own errors surface
	// unwrapped.
	ErrModelContract = errors.New("weft: model violated the stream contract")

	// ErrUnsupported is wrapped by a Model that cannot honour part of a
	// request — a FilePart whose media type the provider does not accept,
	// a feature the vendor lacks. It is a run error (the model call
	// fails), so callers can errors.Is on it and fall back to another
	// model.
	ErrUnsupported = errors.New("weft: request uses a feature the model does not support")

	// ErrStreamIdle is the stream error an adapter yields when the gap
	// between two chunks exceeds its IdleTimeout. The ctx deadline is the
	// hard limit on a whole call; the idle timeout only catches a stalled
	// stream, so a slow but actively streaming response is never killed.
	// It is a run error like any provider error; callers errors.Is on it
	// provider-agnostically, without knowing which adapter timed out.
	ErrStreamIdle = errors.New("weft: model stream idle timeout")

	// ErrModelRequestsDenied is the stream error every first-party
	// adapter yields from Stream when ModelRequestsAllowed is false — the
	// kill switch for test suites that must never reach the network.
	// wefttest models ignore the switch, so ordinary offline tests are
	// unaffected.
	ErrModelRequestsDenied = errors.New("weft: model requests denied by WEFT_MODEL_REQUESTS")
)

Sentinel errors for named run failures. Branch on them with errors.Is; never match on error strings.

Functions

func Manifest

func Manifest(agents ...*Agent) ([]byte, error)

Manifest renders the agents as their `weft.json` document: one generated, committed, diffable description of every agent and tool. Studio, docs, review, and compatibility checks read a file instead of a live process; the code stays the only source of truth. The file is output, never input — nothing is configured from it. Generate it in a golden test (wefttest.Golden) so a stale file fails `go test`; run that test with `-update` to regenerate.

Tools are listed in registration order, agents in argument order, and encoding/json sorts map keys, so the bytes are deterministic for the same agents. Unnamed and duplicate agent names are errors: the file is a review artifact, and `agent_1` in a diff is noise.

Example

The manifest is generated output: one committed, diffable description of every agent and tool. Gate it with a golden test so it cannot go stale.

package main

import (
	"context"
	"fmt"
	"log"
	"strings"

	"github.com/weftgo/weft"
	"github.com/weftgo/weft/wefttest"
)

func main() {
	agt := weft.New(wefttest.Script(wefttest.Say("ok")),
		weft.Name("support-bot"),
		weft.Instructions("You are a support agent."),
		weft.Tool("refund_order", "Refund a customer's order.",
			func(_ context.Context, _ struct {
				OrderID string `json:"order_id"`
			}) (string, error) {
				return "refunded", nil
			}),
	)
	b, err := weft.Manifest(agt)
	if err != nil {
		log.Fatal(err)
	}
	lines := strings.Split(string(b), "\n")
	fmt.Println(lines[0])
	fmt.Println(lines[1])
}
Output:
{
  "weft": 1,

func ModelRequestsAllowed

func ModelRequestsAllowed() bool

ModelRequestsAllowed reports whether adapters may call a provider. It is false when WEFT_MODEL_REQUESTS=deny — the guard for test suites that must never reach the network (Pydantic AI's ALLOW_MODEL_REQUESTS). First-party adapters check it at the top of Stream and yield ErrModelRequestsDenied when it is false, before any network I/O; wefttest models ignore it, so ordinary offline tests are unaffected.

The environment is read on every call, not cached: a model call is network-bound so one getenv is noise, test suites can toggle the switch per test with t.Setenv, and the package keeps no state at all.

Types

type Agent

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

Agent is an immutable, reusable value: a model, a system instruction, a tool set, and an execution policy. Build it once with New; run it many times, concurrently if you like — runs share no state.

Example (Generate)
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/weftgo/weft"
	"github.com/weftgo/weft/wefttest"
)

func main() {
	echo := weft.Tool("echo", "Echo a message.",
		func(_ context.Context, in struct {
			Msg string `json:"msg"`
		}) (string, error) {
			return "echo: " + in.Msg, nil
		})
	model := wefttest.Script(
		wefttest.ToolCalls(wefttest.Call{Name: "echo", Args: `{"msg":"hello"}`}),
		wefttest.Say("I echoed your message."),
	)
	agt := weft.New(model, weft.Instructions("You echo things."), echo)

	res, err := agt.Generate(context.Background(), weft.Prompt("Echo hello."))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Text())
	fmt.Println("steps:", res.NumSteps(), "tokens:", res.Usage.Total())
}
Output:
I echoed your message.
steps: 2 tokens: 30
Example (Stream)
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/weftgo/weft"
	"github.com/weftgo/weft/wefttest"
)

func main() {
	roll := weft.Tool("roll_dice", "Roll a six-sided die.",
		func(_ context.Context, _ struct{}) (int, error) {
			return 4, nil // deterministic for the example
		})
	model := wefttest.Script(
		wefttest.ToolCalls(wefttest.Call{Name: "roll_dice"}),
		wefttest.Say("You rolled a 4!"),
	)
	agt := weft.New(model, roll)

	for ev, err := range agt.Stream(context.Background(), weft.Prompt("Roll a die.")).Events() {
		if err != nil {
			log.Fatal(err)
		}
		switch ev := ev.(type) {
		case weft.ToolStart:
			fmt.Println("tool:", ev.Name)
		case weft.TextDelta:
			fmt.Println("text:", ev.Text)
		case weft.RunFinish:
			fmt.Println("done in", ev.Steps, "steps")
		}
	}
}
Output:
tool: roll_dice
text: You rolled a 4!
done in 2 steps

func New

func New(m Model, opts ...Option) *Agent

New builds an Agent. Nil models panic — including typed nils such as var m *someModel; New(m), which would otherwise crash much later inside a run goroutine. Everything else has a working default.

func (*Agent) CallTool

func (a *Agent) CallTool(ctx context.Context, call ToolCallPart) (string, error)

CallTool dispatches one tool call by name, the way the loop does, and returns the result text the model would see. It is the seam for manual dispatchers and future tool middleware. Unlike the loop it does not contain failures: an unknown name returns an error wrapping ErrNoSuchTool, undecodable arguments one wrapping ErrInvalidToolInput, and handler errors and panics propagate.

func (*Agent) Generate

func (a *Agent) Generate(ctx context.Context, opts ...RunOption) (*RunResult, error)

Generate runs the agent to completion and returns the final result. Internally it is Stream with the events folded away; errors are returned as *RunError, with the partial transcript attached.

func (*Agent) Stream

func (a *Agent) Stream(ctx context.Context, opts ...RunOption) *Run

Stream starts a run and returns its handle. The run is lazy: nothing executes until Events is consumed. Canceling ctx aborts the model call and any in-flight tools.

func (*Agent) Tools

func (a *Agent) Tools() []*ToolDef

Tools returns the registered tool definitions in registration order.

type Call

type Call struct {
	RunID  string // the run this call belongs to
	Step   int    // zero-based index of the step that requested it
	CallID string // the provider's call identifier
	Name   string // the tool name
}

Call identifies the tool invocation a handler is serving. Retrieve it with CallFromContext — for audit logs, per-call idempotency keys, or progress reporting that must name its call.

func CallFromContext

func CallFromContext(ctx context.Context) (c Call, ok bool)

CallFromContext returns the Call a tool handler is serving. ok is false when ctx did not come from the agent loop (a direct Invoke, for example).

type Event

type Event interface {
	// contains filtered or unexported methods
}

Event is the sealed set of run progress events, yielded by Run.Events in emission order. New event types may be added additively; external types cannot join, so switches over events stay exhaustively lintable.

On the wire every event carries a "type" discriminator (run_start, step_start, text_delta, reasoning_delta, tool_start, tool_finish, step_finish, run_finish) and UnmarshalEvent restores it — the same rule and the same compatibility contract as the message parts (ADR 0004).

func UnmarshalEvent

func UnmarshalEvent(b []byte) (Event, error)

UnmarshalEvent decodes one wire event, dispatching on its "type" discriminator. An unknown or missing type is an error, never a silent drop: a recorded stream must replay exactly what was emitted.

Example

Recorded event streams decode back into typed events: store writes them, the Inspector replays them.

package main

import (
	"fmt"
	"log"

	"github.com/weftgo/weft"
)

func main() {
	ev, err := weft.UnmarshalEvent([]byte(`{"type":"tool_start","seq":5,"call_id":"c1","name":"echo","args":{"m":"x"}}`))
	if err != nil {
		log.Fatal(err)
	}
	start := ev.(weft.ToolStart)
	fmt.Println(start.Name, start.CallID, start.Seq, string(start.Args))
}
Output:
echo c1 5 {"m":"x"}

type FilePart

type FilePart struct {
	MediaType string `json:"media_type"`
	Data      []byte `json:"data,omitempty"`
	URL       string `json:"url,omitempty"`
}

FilePart is a file the user supplies to the model: an image, a PDF, audio. Exactly one of Data (inline; base64 on the wire via []byte's default encoding) or URL is set. Adapters map it to the vendor's image/document block; an adapter that cannot carry this MediaType fails the model call with an error wrapping ErrUnsupported. The core never reads the bytes. The exactly-one rule is documented, not enforced here — the adapter is the layer that knows what it can send, and it returns ErrUnsupported for a part with both or neither set.

func (FilePart) MarshalJSON

func (p FilePart) MarshalJSON() ([]byte, error)

MarshalJSON encodes the part with its "type" discriminator.

type Message

type Message struct {
	Role    Role   `json:"role"`
	Content []Part `json:"content"`
}

Message is one turn in a conversation: a role plus an ordered list of content parts. A step's tool results are collected on a single RoleTool message; provider adapters fan out or merge as their wire format requires.

On the wire every part carries a "type" discriminator ("text", "tool_call", "tool_result", "reasoning"), so a Message round-trips through encoding/json losslessly.

func Assistant

func Assistant(text string) Message

Assistant returns an assistant message with a single text part.

func Repair

func Repair(msgs []Message) []Message

Repair makes a transcript valid model input: every tool call has a result (missing ones become visible error results), results with no call are dropped, everything else is untouched. The loop applies it to the input of every run; store and runtime call it before persisting. It is pure (the input is never mutated) and idempotent: Repair(Repair(m)) equals Repair(m). nil input yields nil; an empty non-nil input yields an empty non-nil transcript.

Example

A partial transcript (the run was interrupted mid-step) becomes valid model input: the missing result is synthesised, visibly.

package main

import (
	"encoding/json"
	"fmt"

	"github.com/weftgo/weft"
)

func main() {
	msgs := []weft.Message{
		weft.User("Where is order 1234?"),
		{Role: weft.RoleAssistant, Content: []weft.Part{
			weft.ToolCallPart{ID: "c1", Name: "lookup_order", Args: json.RawMessage(`{"order_id":"1234"}`)},
		}},
	}
	for _, m := range weft.Repair(msgs) {
		fmt.Println(m.Role)
	}
}
Output:
user
assistant
tool

func User

func User(text string) Message

User returns a user message with a single text part.

func UserParts

func UserParts(parts ...Part) Message

UserParts returns a user message with the given parts, for prompts that mix text and files: UserParts(TextPart{"What is this?"}, FilePart{MediaType: "image/png", URL: u}). The parts are copied; the caller's slice is not retained.

Example

Provider reasoning round-trips: it is preserved in the transcript, placed before the text of the same assistant turn. A prompt can mix text and files with UserParts; the core carries the bytes and the adapter maps them to the provider's image block.

package main

import (
	"encoding/json"
	"fmt"
	"log"

	"github.com/weftgo/weft"
)

func main() {
	msg := weft.UserParts(
		weft.TextPart{Text: "What is this?"},
		weft.FilePart{MediaType: "image/png", URL: "https://example.com/cat.png"},
	)
	b, err := json.Marshal(msg)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(b))
}
Output:
{"role":"user","content":[{"type":"text","text":"What is this?"},{"type":"file","media_type":"image/png","url":"https://example.com/cat.png"}]}

func (Message) Text

func (m Message) Text() string

Text returns the concatenation of the message's text parts.

func (*Message) UnmarshalJSON

func (m *Message) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes a message, dispatching each content part on its "type" discriminator. An unknown part type is an error: the wire format is versioned, and silently dropping content would corrupt transcripts.

type Model

type Model interface {
	Stream(ctx context.Context, req ModelRequest) iter.Seq2[ModelEvent, error]
}

Model is the provider seam. Implementations stream one step's output as events; first-party adapters wrap the vendors' official Go SDKs rather than re-implementing HTTP.

The stream contract:

  • Events are yielded in order: any number of ModelTextDelta, ModelReasoningDelta, and ModelToolCall values, then exactly one ModelFinish.
  • Failure is reported as a single terminal yield of (nil, err); no events follow it.
  • The sequence honors ctx: when ctx is done, the model yields (nil, ctx.Err()) if it has not finished already.

The loop enforces this contract: a stream that ends without ModelFinish, continues after it, or panics fails the run with an error wrapping ErrModelContract. A contract-violating adapter cannot corrupt a transcript silently.

type ModelEvent

type ModelEvent interface {
	// contains filtered or unexported methods
}

ModelEvent is the sealed set of events a model yields during one step. Tool calls arrive whole — assembling providers' streamed argument fragments is the adapter's job, which is what makes the core's streaming uniform across providers.

type ModelFinish

type ModelFinish struct {
	Reason StopReason
	Usage  Usage
	// Raw is the provider's own stop reason when Reason had to be
	// approximated ("refusal", "pause_turn", "content_filter", ...).
	// Empty when the mapping was exact. The core never interprets it; it
	// is recorded on the step (StepRecord.RawStopReason) and the
	// StepFinish event so callers can see a refusal without a tap.
	Raw string
}

ModelFinish closes a step with its stop reason and token usage.

type ModelInfo

type ModelInfo struct {
	Provider string `json:"provider"` // "openai", "anthropic", "wefttest"
	Name     string `json:"name"`     // the vendor's model id
}

ModelInfo identifies a model for telemetry (§8.1) and the manifest. A Model that can report it implements the optional

interface{ Info() ModelInfo }

which the loop detects and surfaces on RunStart.Model; Model itself stays one method. Middleware that wraps a Model should forward Info (TODO §4.1).

type ModelReasoningDelta

type ModelReasoningDelta struct {
	Text      string
	Signature string
}

ModelReasoningDelta is an increment of provider reasoning (Anthropic thinking, Gemini thought summaries). Signature is the provider's opaque token for the block, if any; adapters set it on the delta that completes a block. The core stores and forwards reasoning and never reads it.

Block boundaries: a delta carrying a non-empty Signature closes the current reasoning block; the next reasoning delta opens a new one. Providers send a block's signature last (Anthropic's signature_delta ends a thinking block; Gemini's per-part signature is emitted after the part's text), so one ReasoningPart per provider block survives the round trip. Reasoning without any signature accumulates into a single block — nothing downstream can send unsigned blocks back anyway, so their boundaries are not load-bearing.

type ModelRequest

type ModelRequest struct {
	System   string
	Messages []Message
	Tools    []*ToolDef
	// SequentialTools asks the provider not to emit parallel tool-call
	// batches. The zero value keeps the provider default; the loop sets
	// it to true exactly under Sequential() (and Parallelism(1)), so the
	// model does not emit batches the execution policy would serialize
	// anyway. Adapters mirror it in the provider's parallel-tool-calls
	// setting.
	SequentialTools bool
}

ModelRequest is everything a model needs for one step: the system instruction, the transcript so far, and the callable tools.

Read-only: implementations must not modify Messages or Tools, and must clone anything they retain beyond the call.

type ModelTextDelta

type ModelTextDelta struct {
	Text string
}

ModelTextDelta is an increment of assistant text.

type ModelToolCall

type ModelToolCall struct {
	ID        string
	Name      string
	Args      json.RawMessage
	Signature string
}

ModelToolCall is one complete tool invocation request. ID is the provider's call identifier, echoed back on the matching ToolResultPart. Signature is the provider's opaque token attached to the call itself (Gemini attaches thought signatures to functionCall parts and requires them returned on the same part); adapters that do not have one leave it empty.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option configures an Agent at construction. Options are small values returned by Instructions, MaxSteps, Parallelism, Sequential, StopWhen, and the Tool constructor.

func Instructions

func Instructions(text string) Option

Instructions sets the agent's system prompt.

func MaxResultBytes

func MaxResultBytes(n int) Option

MaxResultBytes sets the maximum size of one tool result's text, in bytes (default 64 KiB). The loop caps longer results — successes, failures, and panics alike — cutting on a rune boundary and appending a marker the model can see, so it knows the output is partial. MaxResultBytes(0) removes the cap; negative values are ignored. Agent.CallTool returns uncapped output: the cap is a run policy, applied by the loop.

func MaxSteps

func MaxSteps(n int) Option

MaxSteps is the safety budget: the most model calls a run may make (default 10). Exceeding it fails the run with ErrMaxSteps — a runaway loop is a failure to surface, never a quiet success. Use StopWhen for the intended end of a run. Values below 1 are ignored.

func Name

func Name(name string) Option

Name names the agent: it appears on RunStart.Agent and in the manifest, which requires it (weft.Manifest errors on unnamed agents). Empty values are ignored.

func Parallelism

func Parallelism(n int) Option

Parallelism sets the maximum number of a step's tool calls executing at once (default 4). Values below 1 are ignored.

func Sequential

func Sequential() Option

Sequential restricts a step's tool calls to run one at a time, in call order: each tool finishes before the next starts — the safe setting for tools with shared state. Under any parallelism, tools *start* in call order; Sequential additionally serializes their execution. It also sets ModelRequest.SequentialTools, so adapters ask the provider not to emit parallel batches in the first place.

func StopWhen

func StopWhen(conds ...StopCondition) Option

StopWhen adds stop conditions; the run ends when any one is met. Without StopWhen a run ends when the model replies without requesting tools. Compose the built-ins — HasToolCall, StepCountIs — or write your own:

weft.StopWhen(weft.HasToolCall("submit_answer"))
weft.StopWhen(weft.StopFunc(func(steps []weft.StepRecord) bool { ... }))

Stop conditions are the intended end of a run; MaxSteps is the safety budget behind them.

func Tap

func Tap(fn func(ctx context.Context, ev Event)) Option

Tap registers an observer that sees every event of every run, including runs made with Generate, synchronously and in emission order on the emitting goroutine. It must be fast and must not block: it runs under the event-ordering lock, so a slow tap delays every tool event of its step and blocks the emitting tool goroutines. Taps run in registration order; a panic in one is recovered and dropped, so a broken observer cannot break a run. Taps observe and cannot change anything — behaviour attaches at the two middleware seams. ctx is the run's context.

Example

Tap observes every event of every run — including Generate, which has no stream to range over. Taps see; the middleware seams change.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/weftgo/weft"
	"github.com/weftgo/weft/wefttest"
)

func main() {
	var calls int
	agt := weft.New(
		wefttest.Script(
			wefttest.ToolCalls(wefttest.Call{Name: "roll_dice"}),
			wefttest.Say("rolled"),
		),
		weft.Tap(func(_ context.Context, ev weft.Event) {
			if _, ok := ev.(weft.ToolStart); ok {
				calls++
			}
		}),
		weft.Tool("roll_dice", "Roll a die.",
			func(_ context.Context, _ struct{}) (int, error) { return 4, nil }),
	)
	if _, err := agt.Generate(context.Background(), weft.Prompt("Roll.")); err != nil {
		log.Fatal(err)
	}
	fmt.Println("tool calls:", calls)
}
Output:
tool calls: 1

func ToolSource

func ToolSource(fn func() []*ToolDef) Option

ToolSource replaces the tool set the loop advertises and dispatches against with the given function's return value, fetched fresh at each step — the seam for registries that change while the agent runs (plugins installed mid-run, MCP servers polled per step). The Agent stays immutable: the source is a value; synchronization and uniqueness of names belong to the source's owner. The function runs on the loop goroutine once per step for advertising and once per dispatched call; on duplicate names in the returned list the first entry wins. A nil function (the default) keeps the static construction-time list — Tool/option registration is then the only source of tools, byte-identical to an agent without a source. Manifest and Agent.Tools still report the static construction-time set: a manifest describes the code, not the registry behind a source.

type Part

type Part interface {
	// contains filtered or unexported methods
}

Part is one content part of a Message. The set of part types is closed: text, tool calls, tool results, reasoning, and files today; approval parts are planned additions that will join this interface.

type ReasoningDelta

type ReasoningDelta struct {
	Text string `json:"text"`
}

ReasoningDelta is an increment of provider reasoning, in the order the model produced it relative to TextDelta. Signatures are not streamed; they are on the ReasoningPart of the transcript.

func (ReasoningDelta) MarshalJSON

func (e ReasoningDelta) MarshalJSON() ([]byte, error)

MarshalJSON encodes the event with its "type" discriminator.

type ReasoningPart

type ReasoningPart struct {
	Text      string `json:"text"`
	Signature string `json:"signature,omitempty"`
}

ReasoningPart is one provider reasoning block surfaced by providers that expose it (Anthropic thinking blocks, Gemini thought parts). Weft preserves it in the transcript but does not act on it. Signature is the provider's opaque token for the block (Anthropic rejects thinking sent back without its signature); adapters echo it unchanged. A step yields one ReasoningPart per provider block — a delta carrying a signature closes the block (see ModelReasoningDelta) — placed before the TextPart of the same assistant message, in the order the model produced them.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/weftgo/weft"
	"github.com/weftgo/weft/wefttest"
)

func main() {
	model := wefttest.Script(
		wefttest.Think("The user greets; reply in kind.", wefttest.Say("Hello!")),
	)
	res, err := weft.New(model).Generate(context.Background(), weft.Prompt("Hi."))
	if err != nil {
		log.Fatal(err)
	}
	for _, p := range res.Messages[1].Content {
		fmt.Printf("%T\n", p)
	}
}
Output:
weft.ReasoningPart
weft.TextPart

func (ReasoningPart) MarshalJSON

func (p ReasoningPart) MarshalJSON() ([]byte, error)

MarshalJSON encodes the part with its "type" discriminator.

type Role

type Role string

Role is the author of a Message.

There is no system role: the system instruction is agent-level (Instructions) and travels on ModelRequest.System, so a transcript never carries it and adapters never have to merge it.

const (
	RoleUser      Role = "user"
	RoleAssistant Role = "assistant"
	// RoleTool carries the results of one step's tool calls back to the
	// model, one ToolResultPart per call.
	RoleTool Role = "tool"
)

type Run

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

Run is a handle to one streaming execution. Create it with Agent.Stream, then either range over Events (exactly once) or call Wait, which runs the agent to completion and reports the final result.

func (*Run) Events

func (r *Run) Events() iter.Seq2[Event, error]

Events returns the run's event stream. It is single-use; a second call yields only ErrRunConsumed.

Events arrive in emission order (see ToolStart for the concurrent-tool ordering rule). A failed run delivers its error exactly once as the final element; a successful run ends with RunFinish. Breaking out of the range cancels the run.

func (*Run) ID

func (r *Run) ID() string

ID returns the run's identifier. It is fixed at Stream time, so it can be logged or handed to a client before the first event is consumed.

func (*Run) Wait

func (r *Run) Wait() (*RunResult, error)

Wait blocks until the run finishes and returns its result. If Events has not been consumed, Wait runs the agent itself, discarding events; it is safe to call after ranging over Events, or from another goroutine while ranging over them.

type RunError

type RunError struct {
	Step   int
	Err    error
	Result *RunResult
}

RunError reports a step-scoped failure: the model stream failed, the context was canceled, or the step budget ran out. Err is the cause — use errors.Is/As on it. Result carries the transcript up to the failure, so partial work is never lost.

func (*RunError) Error

func (e *RunError) Error() string

func (*RunError) Unwrap

func (e *RunError) Unwrap() error

type RunFinish

type RunFinish struct {
	Usage Usage `json:"usage"`
	Steps int   `json:"steps"`
}

RunFinish is always the final event of a successful run and carries the run's total usage and step count.

func (RunFinish) MarshalJSON

func (e RunFinish) MarshalJSON() ([]byte, error)

MarshalJSON encodes the event with its "type" discriminator.

type RunOption

type RunOption interface {
	// contains filtered or unexported methods
}

RunOption configures a single run.

func Messages

func Messages(msgs ...Message) RunOption

Messages adds existing messages (a session transcript, few-shot examples) to the run's input.

func Prompt

func Prompt(text string) RunOption

Prompt adds a user message to the run's input.

func RunID

func RunID(id string) RunOption

RunID sets the run's identifier instead of generating one — for replays, idempotent retries, and correlating with an outer system's own ids. Empty values are ignored.

type RunResult

type RunResult struct {
	ID string
	// StopReason is the last step's finish reason. StopMaxTokens here
	// means the final reply was cut off by the output-token limit — the
	// run still succeeds, and the caller decides what truncated text
	// means.
	StopReason StopReason
	Messages   []Message
	Steps      []StepRecord
	Usage      Usage
}

RunResult is the outcome of a completed run: its id, the full transcript (including the input messages), one record per step, and summed usage.

func (*RunResult) NumSteps

func (r *RunResult) NumSteps() int

NumSteps returns how many model calls the run made.

func (*RunResult) Text

func (r *RunResult) Text() string

Text returns the final assistant text — the text parts of the last assistant message.

type RunStart

type RunStart struct {
	ID    string    `json:"id"`
	Model ModelInfo `json:"model"`
	Agent string    `json:"agent,omitempty"`
}

RunStart is always the first event of a run and carries its id and, when reported, the model's identity and the agent's name.

func (RunStart) MarshalJSON

func (e RunStart) MarshalJSON() ([]byte, error)

MarshalJSON encodes the event with its "type" discriminator.

type Schema

type Schema struct {
	Type        string             `json:"type,omitempty"`
	Format      string             `json:"format,omitempty"`
	Description string             `json:"description,omitempty"`
	Properties  map[string]*Schema `json:"properties,omitempty"`
	Required    []string           `json:"required,omitempty"`
	Items       *Schema            `json:"items,omitempty"`
}

Schema is the subset of JSON Schema (draft 2020-12) that weft derives from tool input structs. Its shape tracks what the official Go MCP SDK derives via google/jsonschema-go, so tools cross over to MCP without conversion.

type StepFinish

type StepFinish struct {
	Index  int        `json:"index"`
	Reason StopReason `json:"reason"`
	Usage  Usage      `json:"usage"`
	Raw    string     `json:"raw,omitempty"`
}

StepFinish reports that step Index's model call completed. Raw is the provider's own stop reason when Reason was approximated (see ModelFinish.Raw); empty when the mapping was exact.

func (StepFinish) MarshalJSON

func (e StepFinish) MarshalJSON() ([]byte, error)

MarshalJSON encodes the event with its "type" discriminator.

type StepRecord

type StepRecord struct {
	Index int
	// StopReason is the mapped reason (stop, tool_calls, max_tokens);
	// RawStopReason is the provider's own value when the mapping was
	// approximated ("refusal", "content_filter", ...) — see
	// ModelFinish.Raw.
	StopReason    StopReason
	RawStopReason string
	Usage         Usage
	Text          string
	ToolCalls     []ToolCallPart
	Results       []ToolResultPart
}

StepRecord captures everything one model step produced: its text, the tool calls it requested, and the results of executing them in call order.

type StepStart

type StepStart struct {
	Index int `json:"index"`
}

StepStart reports that the model is being called for step Index.

func (StepStart) MarshalJSON

func (e StepStart) MarshalJSON() ([]byte, error)

MarshalJSON encodes the event with its "type" discriminator.

type StopCondition

type StopCondition interface {
	Stop(steps []StepRecord) bool
}

StopCondition decides, after a step's tool calls have run, whether the run is complete. It sees every step so far; the last element is the step just finished. Returning true ends the run successfully without another model call. The built-ins — HasToolCall, StepCountIs — also implement fmt.Stringer so the manifest (TODO §2.9) can name them; adapt an ordinary function with StopFunc.

func HasToolCall

func HasToolCall(names ...string) StopCondition

HasToolCall stops the run once the step just finished called any of the named tools — the "final answer tool" pattern.

func StepCountIs

func StepCountIs(n int) StopCondition

StepCountIs stops the run after exactly n steps, successfully — unlike MaxSteps, which treats reaching the budget as a failure.

type StopFunc

type StopFunc func(steps []StepRecord) bool

StopFunc adapts an ordinary function to a StopCondition, the http.HandlerFunc shape.

func (StopFunc) Stop

func (f StopFunc) Stop(steps []StepRecord) bool

Stop ends the run when f says so.

type StopReason

type StopReason string

StopReason is why a model step ended.

const (
	StopEndTurn   StopReason = "stop"
	StopToolCalls StopReason = "tool_calls"
	StopMaxTokens StopReason = "max_tokens"
)

type TextDelta

type TextDelta struct {
	Text string `json:"text"`
}

TextDelta is an increment of assistant text.

func (TextDelta) MarshalJSON

func (e TextDelta) MarshalJSON() ([]byte, error)

MarshalJSON encodes the event with its "type" discriminator.

type TextPart

type TextPart struct {
	Text string `json:"text"`
}

TextPart is a span of user or assistant text.

func (TextPart) MarshalJSON

func (p TextPart) MarshalJSON() ([]byte, error)

MarshalJSON encodes the part with its "type" discriminator.

type ToolCallPart

type ToolCallPart struct {
	ID        string          `json:"id"`
	Name      string          `json:"name"`
	Args      json.RawMessage `json:"args"`
	Signature string          `json:"signature,omitempty"`
}

ToolCallPart is a tool invocation requested by the model. Args is the raw JSON the model produced; the loop unmarshals it into the tool's input type before invoking the handler. Signature is the provider's opaque token attached to the call itself (Gemini's thought signatures ride functionCall parts and must return on the same part); empty for providers without one.

func (ToolCallPart) MarshalJSON

func (p ToolCallPart) MarshalJSON() ([]byte, error)

MarshalJSON encodes the part with its "type" discriminator.

type ToolDef

type ToolDef struct {
	Name         string  `json:"name"`
	Description  string  `json:"description,omitempty"`
	InputSchema  *Schema `json:"input_schema,omitempty"`
	OutputSchema *Schema `json:"output_schema,omitempty"`
	// contains filtered or unexported fields
}

ToolDef is a named, schema-described tool the model can call. Build one with the generic Tool constructor; the agent loop (and any manual dispatcher) executes it through Invoke.

func RawTool

func RawTool(name, description string, schema *Schema, fn func(ctx context.Context, args json.RawMessage) (string, error)) *ToolDef

RawTool defines a tool from an explicit schema instead of reflection — for tools defined outside Go source: plugin manifests, MCP remotes, gateways. fn receives the model's raw JSON arguments verbatim: no unmarshalling, no ErrInvalidToolInput — validation belongs to fn. A nil schema becomes the empty object schema, so the tool accepts any object input. Panics match Tool: empty name, nil fn. The manifest records no source line (the definition is not in Go source).

func Tool

func Tool[In, Out any](name, description string, fn func(ctx context.Context, in In) (Out, error)) *ToolDef

Tool defines a tool from a plain function. In and Out are inferred from the handler and the input schema is reflected from In's struct tags, so the compiler checks the handler's shape and nothing is written twice:

type RefundInput struct {
    OrderID string `json:"order_id" jsonschema:"the order to refund"`
    Reason  string `json:"reason,omitempty"`
}

weft.Tool("refund_order", "Refund a customer's order",
    func(ctx context.Context, in RefundInput) (Receipt, error) {
        return billing.Refund(ctx, in.OrderID, in.Reason)
    })

What the model sees as the result is the text of a string Out, and the JSON encoding of any other Out. Inside the handler, CallFromContext reports which call is running.

The handler shape mirrors the official Go MCP SDK's AddTool[In, Out], so a weft tool can be exposed over MCP without an adapter layer.

Tool panics if name is empty, fn is nil, or In is not a struct (or a pointer to one): providers and MCP require an object at the top level of a tool schema, and a scalar there would fail every real call.

Example

A tool is a plain function; the input schema is derived from the struct.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	"github.com/weftgo/weft"
)

func main() {
	type WeatherInput struct {
		City string `json:"city" jsonschema:"the city to look up"`
		Days *int   `json:"days,omitempty"`
	}
	getWeather := weft.Tool("get_weather", "Get a forecast.",
		func(_ context.Context, in WeatherInput) (string, error) {
			return "sunny in " + in.City, nil
		})

	b, err := json.MarshalIndent(getWeather.InputSchema, "", "  ")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(getWeather.Name)
	fmt.Println(string(b))
}
Output:
get_weather
{
  "type": "object",
  "properties": {
    "city": {
      "type": "string",
      "description": "the city to look up"
    },
    "days": {
      "type": "integer"
    }
  },
  "required": [
    "city"
  ]
}

func (*ToolDef) Invoke

func (t *ToolDef) Invoke(ctx context.Context, args json.RawMessage) (string, error)

Invoke runs the tool with raw JSON arguments: it unmarshals into the handler's input type, calls the handler, and returns the result text the model will see (a string output verbatim, anything else JSON-encoded). A decode failure returns an error wrapping ErrInvalidToolInput.

type ToolFinish

type ToolFinish struct {
	Seq     int64  `json:"seq"`
	CallID  string `json:"call_id"`
	Name    string `json:"name"`
	Content string `json:"content"`
	IsError bool   `json:"is_error"`
}

ToolFinish reports that a tool invocation completed, successfully or not. Content is the tool's JSON output, or the failure text when IsError is set — the same value the model sees on the matching ToolResultPart, so a UI can render results as they land.

func (ToolFinish) MarshalJSON

func (e ToolFinish) MarshalJSON() ([]byte, error)

MarshalJSON encodes the event with its "type" discriminator.

type ToolResultPart

type ToolResultPart struct {
	CallID  string `json:"call_id"`
	Name    string `json:"name"`
	Content string `json:"content"`
	IsError bool   `json:"is_error"`
}

ToolResultPart is the outcome of one tool call, returned to the model as data. Content is the JSON encoding of the tool's output, or the failure message when IsError is set. Tool failures never abort a run; the model sees them and can recover.

func (ToolResultPart) MarshalJSON

func (p ToolResultPart) MarshalJSON() ([]byte, error)

MarshalJSON encodes the part with its "type" discriminator.

type ToolStart

type ToolStart struct {
	Seq    int64           `json:"seq"`
	CallID string          `json:"call_id"`
	Name   string          `json:"name"`
	Args   json.RawMessage `json:"args"`
}

ToolStart reports that a tool invocation began. Events from tools running in parallel interleave: pair them by CallID and order by Seq, a per-run counter assigned at emission that totally orders the stream.

func (ToolStart) MarshalJSON

func (e ToolStart) MarshalJSON() ([]byte, error)

MarshalJSON encodes the event with its "type" discriminator.

type Usage

type Usage struct {
	InputTokens  int64 `json:"input_tokens"`
	OutputTokens int64 `json:"output_tokens"`
}

Usage is token accounting for one step or one whole run.

func (Usage) Add

func (u Usage) Add(o Usage) Usage

Add returns the element-wise sum of u and o.

func (Usage) Total

func (u Usage) Total() int64

Total returns the sum of input and output tokens.

Directories

Path Synopsis
anthropic module
core module
examples
getting-started command
Command getting-started runs a complete weft agent offline: a scripted model (from wefttest), one tool, and the full event stream.
Command getting-started runs a complete weft agent offline: a scripted model (from wefttest), one tool, and the full event stream.
google module
mcp module
obsdb module
clickhouse module
openai module
otel module
runtime module
store module
studio module
cmd module
thread module
sqlite module
Package wefttest provides a scriptable weft.Model, in the spirit of httptest: write the agent's dialogue as a sequence of scripted model steps and test agents offline, deterministically, with no network.
Package wefttest provides a scriptable weft.Model, in the spirit of httptest: write the agent's dialogue as a sequence of scripted model steps and test agents offline, deterministically, with no network.
conformance
Package conformance is the provider adapter contract as an executable table.
Package conformance is the provider adapter contract as an executable table.

Jump to

Keyboard shortcuts

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