Documentation
¶
Overview ¶
Package weft is a modular framework 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.
This package is the framework's front door: it re-exports the agent loop that github.com/weftgo/weft/core implements — tools from plain Go functions, parallel tool calls with defined failure semantics, typed streaming events, structured output, approvals, steering, subagents — so one import gives the loop and one module gives the whole framework:
github.com/weftgo/weft the loop (this package) github.com/weftgo/weft/openai OpenAI and OpenAI-compatible servers github.com/weftgo/weft/anthropic Anthropic github.com/weftgo/weft/google Google Gemini github.com/weftgo/weft/mcp MCP both ways github.com/weftgo/weft/mw reference middleware github.com/weftgo/weft/wefttest the scripted model for offline tests github.com/weftgo/weft/thread durable sessions github.com/weftgo/weft/otel recording over OpenTelemetry github.com/weftgo/weft/obsdb the store the records land in github.com/weftgo/weft/studio the Inspector, devtools panel, playground github.com/weftgo/weft/runtime the playground's in-app side
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."))
Every name here is an alias of, or a one-line wrapper around, the same name in core, so values flow between the two without conversion; the godoc of each is the authority. A service that wants the loop alone, with the OpenTelemetry API as its only dependency, imports github.com/weftgo/weft/core instead and writes core.New.
Index ¶
- Constants
- Variables
- func Manifest(agents ...*Agent) ([]byte, error)
- func MetadataFromContext(ctx context.Context) map[string]string
- func ModelRequestsAllowed() bool
- func ModelRetry(hint string) error
- func OutputOf[Out any](res *RunResult) (Out, error)
- type Agent
- type AttemptInfo
- type Call
- type ContentKind
- type Event
- type FilePart
- type InstructionsOption
- type MaxStepsOption
- type Message
- type Model
- type ModelEvent
- type ModelFinish
- type ModelInfo
- type ModelMiddleware
- type ModelReasoningDelta
- type ModelRequest
- type ModelTextDelta
- type ModelToolCall
- type ModelToolCallDelta
- type Nested
- type Option
- func Content(capture bool) Option
- func DetectLoops(repeats int) Option
- func Logger(l *slog.Logger) Option
- func LoggerProvider(lp log.LoggerProvider) Option
- func MaxModelRetries(n int) Option
- func Name(name string) Option
- func OnRunEnd(fn func(ctx context.Context, res *RunResult, err error)) Option
- func Options(opts ...Option) Option
- func Output[Out any]() Option
- func PrepareStep(fn func(ctx context.Context, step int, req ModelRequest) (ModelRequest, error)) Option
- func StopWhen(conds ...StopCondition) Option
- func Tap(fn func(ctx context.Context, ev Event)) Option
- func ToolSource(fn func() []*ToolDef) Option
- func TracerProvider(tp trace.TracerProvider) Option
- func UsageLimit(max Usage) Option
- func WrapModel(mw ...ModelMiddleware) Option
- type OutputDecoder
- type ParallelismOption
- type ParamsOption
- type Part
- type PolicyOption
- type RawPair
- type ReasoningDelta
- type ReasoningPart
- type ReplayPolicy
- type Reporter
- type RequestParams
- type Role
- type Run
- type RunError
- type RunFinish
- type RunOption
- func Approve(callID string) RunOption
- func Deny(callID, reason string) RunOption
- func Messages(msgs ...Message) RunOption
- func Metadata(kv map[string]string) RunOption
- func OnMessages(fn func(ctx context.Context, step int, msgs []Message)) RunOption
- func OnlyTools(names ...string) RunOption
- func ParkAllExcept(names ...string) RunOption
- func ParkOn(tools ...string) RunOption
- func Prompt(text string) RunOption
- func Resolve(callID, content string) RunOption
- func ResolveError(callID, content string) RunOption
- func RunID(id string) RunOption
- func Steering(fn SteerFunc) RunOption
- func UseModel(m Model) RunOption
- type RunResult
- type RunStart
- type Schema
- type SteerFunc
- type SteerPoint
- type Steered
- type StepFinish
- type StepRecord
- type StepStart
- type StopCondition
- type StopFunc
- type StopReason
- type TextDelta
- type TextPart
- type ThinkingConfig
- type ThinkingLevel
- type ThinkingOption
- type ToolArgsDelta
- type ToolCallPart
- type ToolCaller
- type ToolChoiceConfig
- type ToolChoiceMode
- type ToolChoiceOption
- type ToolDef
- type ToolError
- type ToolFinish
- type ToolMiddleware
- type ToolOption
- type ToolResultPart
- type ToolStart
- type Usage
Examples ¶
- Agent (Conversation)
- Agent (Generate)
- Agent (Stream)
- DetectLoops
- GenerateAs
- Manifest
- Metadata
- ModelRetry
- OnRunEnd
- OnlyTools
- Options
- OutputDecoder
- Params
- ParkAllExcept
- ParkOn
- PrepareStep
- PromptSnippet
- ReasoningPart
- Repair
- Replay
- RequireApproval
- Sequential
- Steering
- StripContent
- Subagent
- Tap
- Tap (Async)
- Thinking
- Timeout
- Tool
- ToolChoice
- ToolError
- ToolOptions
- UnmarshalEvent
- UsageLimit
- UserParts
- WrapModel
- WrapTools
- WrapTools (Context)
Constants ¶
const ( ContentText = core.ContentText // assistant text, text deltas ContentReasoning = core.ContentReasoning // reasoning text and deltas ContentArgs = core.ContentArgs // tool call arguments and arg deltas ContentResult = core.ContentResult // tool results ContentMessages = core.ContentMessages // whole transcript batches (weft/otel redacts them per part with the four kinds above) ContentPrompt = core.ContentPrompt // the composed system text of a prompt record (ADR 0028) ContentStop = core.ContentStop // one stop sequence of a request record's params (ADR 0028) )
The seven kinds of content the core ever puts in a record.
const ( RoleUser = core.RoleUser RoleAssistant = core.RoleAssistant // RoleTool carries the results of one step's tool calls back to the // model, one ToolResultPart per call. RoleTool = core.RoleTool )
const ( StopEndTurn = core.StopEndTurn StopToolCalls = core.StopToolCalls StopMaxTokens = core.StopMaxTokens )
const ( ThinkUnset = core.ThinkUnset // provider default; nothing is sent ThinkOff = core.ThinkOff // suppress reasoning where allowed ThinkLow = core.ThinkLow ThinkMedium = core.ThinkMedium ThinkHigh = core.ThinkHigh )
const ( ToolChoiceAuto = core.ToolChoiceAuto // provider default; nothing is sent ToolChoiceAny = core.ToolChoiceAny // some tool must be called ToolChoiceNamed = core.ToolChoiceNamed // the tool named by Name must be called // ToolChoiceNone forbids tool calls while keeping the catalogue // advertised. It exists for the prompt-cache interplay: removing // tools from the request to stop the model calling them invalidates // the cached prefix (ADR 0013's 2026-09-22 amendment), while none // keeps the bytes and forbids the calls. ToolChoiceNone = core.ToolChoiceNone )
const ( // CodeSubagentFailed marks a child run that failed. The message the // model sees carries the child's step and cause; the *RunError // itself stays on ToolError.Err, reachable through errors.As, so // middleware can branch on it without parsing that text. CodeSubagentFailed = core.CodeSubagentFailed // CodeSubagentPending marks a child run that ended awaiting an // approval decision. Approvals belong in the orchestrator, not in a // child: the parent's transcript has nowhere to carry the child's // pending call, so the delegation is refused loudly instead of // silently (ADR 0014). CodeSubagentPending = core.CodeSubagentPending // CodeSubagentCycle marks a delegation to an agent already running // in this call chain — refused before any model call. CodeSubagentCycle = core.CodeSubagentCycle )
Codes the Subagent tool renders its failures with — model-visible contract, pinned by tests (ADR 0002's table; ADR 0014). Exported like the loop's own codes so policy middleware can branch on them.
const ( // CodeInvalidInput marks tool arguments that do not decode into // the tool's input struct; the message names the field in the // schema's own vocabulary. CodeInvalidInput = core.CodeInvalidInput // CodeNoSuchTool marks a call naming a tool the agent does not // have. CodeNoSuchTool = core.CodeNoSuchTool // CodeDenied marks a call the approval boundary (Deny, or no // decision) or a policy middleware refused — mw.Allow renders it // too: to the model, a refused call is a refused call, whoever // refused it (ADR 0007). CodeDenied = core.CodeDenied )
Codes the loop renders its own tool failures with — model-visible contract (ADR 0002, "tool errors have codes"), exported so external policy middleware can produce the same wire strings without duplicating them, the same class of constant as SchemaVersion.
const ( // ReplayNever is the default: the call has side effects, or nobody // has said otherwise. A re-run answers it from the record or parks. ReplayNever = core.ReplayNever // ReplaySafe vouches that the call is idempotent and side-effect // free (a read): a re-run may execute it for real. ReplaySafe = core.ReplaySafe )
const CodeRetry = core.CodeRetry
CodeRetry is the code ModelRetry renders with — the one code whose result is an instruction rather than a failure report: try the call again with the hint applied.
const SchemaVersion = core.SchemaVersion
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 ¶
var ( // ErrMaxSteps is returned when the model still requests tools after the // last allowed step. The partial transcript rides along on RunError. ErrMaxSteps = core.ErrMaxSteps // ErrUsageLimit is returned when a run's usage exceeds its // UsageLimit and the loop would otherwise call the model again. A // step that ends the run may overshoot and still succeed: a budget // stops further spend, it does not discard finished work. ErrUsageLimit = core.ErrUsageLimit // ErrModelRetriesExceeded is returned when one tool has produced // more consecutive RETRY results than MaxModelRetries allows — a // model that cannot self-correct is a run failure, not an infinite // loop. ErrModelRetriesExceeded = core.ErrModelRetriesExceeded // ErrLoopDetected is returned when repeats consecutive steps // requested the same set of tool calls (DetectLoops). ErrLoopDetected = core.ErrLoopDetected // 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 = core.ErrNoSuchTool // 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 = core.ErrInvalidToolInput // ErrRunConsumed is returned by Run.Events when the event stream has // already been consumed; each Run yields exactly one sequence. ErrRunConsumed = core.ErrRunConsumed // 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 = core.ErrModelContract // 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 = core.ErrUnsupported // 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 = core.ErrStreamIdle // 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 = core.ErrModelRequestsDenied // ErrApprovalRequired marks a tool call that must not run until a // human (or an outer system) decides. The loop raises it for tools // built with RequireApproval; tool middleware may return an error // wrapping it to defer any call. The run then ends successfully with // the call on RunResult.Pending; resume with Approve or Deny. ErrApprovalRequired = core.ErrApprovalRequired // ErrApprovalDenied is the cause on the error result the model sees // for a pending call that was denied (Deny, or no decision on // resume). It is a tool error — data — never a run error. ErrApprovalDenied = core.ErrApprovalDenied // ErrDuplicateTool is a run error raised when a step's tool // snapshot contains a name twice — the runtime analogue of New's // duplicate-name panic. Fix the tool source; the run fails rather // than silently dropping the second tool. Agent.CallTool reports // the same condition as an error. ErrDuplicateTool = core.ErrDuplicateTool // ErrNilTool is a run error raised when a tool-source snapshot // contains a nil entry — a malformed snapshot, not a tool. The run // fails rather than advertising a dereference every adapter would // panic on; Agent.CallTool reports the same condition as an error. ErrNilTool = core.ErrNilTool // ErrInvalidSteer is returned when a steering source (Steering) // delivers a message whose role is not RoleUser: the model's own // turns come from the model. The run fails at the drain point with // nothing from that drain appended; earlier steers stay on the // partial transcript riding on RunError (ADR 0019 §3). ErrInvalidSteer = core.ErrInvalidSteer // ErrContextOverflow is wrapped by every first-party adapter around // its provider's error for a request that exceeds the model's // context window (ADR 0020 §5). Overflow is a request-shape // problem: the same bytes cannot succeed, so mw.Retry never retries // it — the session layer (weft/thread, v0.3) routes it to // compaction and one re-run instead. The provider's own error stays // reachable underneath for errors.As. ErrContextOverflow = core.ErrContextOverflow )
Sentinel errors for named run failures. Branch on them with errors.Is; never match on error strings.
var ErrInvalidRunOption = core.ErrInvalidRunOption
ErrInvalidRunOption marks a per-run configuration the agent refuses: an unknown tool name in OnlyTools, or a MaxSteps/Parallelism raise (per run they may only lower). Returned wrapped in *RunError before any model call — a run that would misconfigure itself does not start (WEFT-PLAYGROUND §10.1 [D5]).
var ErrNoOutput = core.ErrNoOutput
ErrNoOutput is returned by GenerateAs and OutputOf when the run ended without a valid submit_output call — the model answered in text, hit the step budget, or never produced arguments that decode into Out.
Functions ¶
func Manifest ¶
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 MetadataFromContext ¶ added in v0.6.0
MetadataFromContext returns a copy of the metadata in force on ctx — the run's own merged over its ancestors'. nil when there is none.
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 — but only for a client they built themselves from credentials; a client the caller injected through the adapter's Client(c) option is a test double by construction and stays reachable (ADR 0013's kill-switch clause). wefttest models ignore the switch entirely.
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.
func ModelRetry ¶ added in v0.2.0
ModelRetry is a tool error asking the model to try the call again with the hint applied: it renders as "RETRY: <hint>" and the loop continues. Return it when the arguments are well-formed but wrong in a way the model can fix — an ambiguous date, an id that needs a prefix. The loop counts RETRY results per tool name per run and fails the run with ErrModelRetriesExceeded when a tool exceeds MaxModelRetries (default 3); a successful result for that tool resets its count. Middleware-produced retries count too — the loop sees the code through the chain, not who returned it.
Example ¶
A retry hint: the model sees "RETRY: <hint>", fixes the arguments, and the loop continues; a tool that cannot be satisfied fails the run after MaxModelRetries consecutive asks.
package main
import (
"context"
"fmt"
"log"
"sync/atomic"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
var calls atomic.Int32
parse := weft.Tool("parse_date", "Parse a date.",
func(_ context.Context, in struct {
D string `json:"d" jsonschema:"the date, ISO-8601"`
}) (string, error) {
if in.D != "2026-09-19" {
return "", weft.ModelRetry("date must be ISO-8601, e.g. 2026-09-19")
}
_ = calls.Add(1)
return "2026-09-19", nil
})
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "parse_date", Args: `{"d":"tomorrow"}`}),
wefttest.ToolCalls(wefttest.Call{Name: "parse_date", Args: `{"d":"2026-09-19"}`}),
wefttest.Say("Parsed."),
), parse)
res, err := agt.Generate(context.Background(), weft.Prompt("When is it?"))
if err != nil {
log.Fatal(err)
}
fmt.Println("first attempt:", res.Steps[0].Results[0].Content)
fmt.Println("second attempt:", res.Steps[1].Results[0].Content)
}
Output: first attempt: RETRY: date must be ISO-8601, e.g. 2026-09-19 second attempt: 2026-09-19
Types ¶
type Agent ¶
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, and the registered tool set is frozen at construction (see ToolDef).
Example (Conversation) ¶
Continuing a conversation: feed the transcript back with the next question. The agent value is unchanged — the history lives in the messages you pass, never in the agent.
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
agt := weft.New(wefttest.Script(
wefttest.Say("Order 1234? It shipped yesterday."),
wefttest.Say("Order 5678? Still pending."),
))
res, err := agt.Generate(context.Background(), weft.Prompt("Where is order 1234?"))
if err != nil {
log.Fatal(err)
}
res2, err := agt.Generate(context.Background(),
weft.Messages(res.Messages...), weft.Prompt("And order 5678?"))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Text())
fmt.Println(res2.Text())
}
Output: Order 1234? It shipped yesterday. Order 5678? Still pending.
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 AgentFromContext ¶ added in v0.3.6
AgentFromContext returns the agent running on ctx — the head of the ancestry chain: the agent whose run is producing the events a Tap or OnRunEnd observer receives, and that a tool handler is running for. For a subagent's child run it is the child (the chain grows by one per nesting level, ADR 0014). Nil outside a run — a manually dispatched Agent.CallTool, a handler invoked directly — so observers must not depend on it. Only the head is exposed, read-only: an Agent is immutable after New, so nothing can be changed through it. The consumer it ships for is the store's Record (ADR 0010), which describes the run by the agent that ran it (manifest hash, logger).
type AttemptInfo ¶ added in v0.10.0
type AttemptInfo = core.AttemptInfo
AttemptInfo is one provider request inside a model call, as the code that made it saw it: a retry middleware's try, a fallback's model, an adapter's request. Fields the reporter does not know stay zero. The attempt's number is not the caller's to give: the reporter numbers the model call's attempts 1, 2, … in the order they are reported, so numbers stay unique however many layers report.
type Call ¶
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.
type ContentKind ¶ added in v0.6.0
type ContentKind = core.ContentKind
ContentKind names what a content field holds. weft/otel's Redact receives it — for event fields, deltas, and each part of a transcript batch; the core only uses it in StripContent's table, to say which field of which event is content (ADR 0024 S1.1, [D2]).
type Event ¶
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_args_delta, tool_start, tool_finish, step_finish, steered, run_finish, nested) and UnmarshalEvent restores it — the same rule and the same compatibility contract as the message parts (ADR 0004).
Every event except RunStart carries RunID: concurrent runs on one agent emit interleaved streams, and a per-run Seq counter is unique only within its run, so RunID is what attributes an event to its run.
Events are snapshots. Their fields — including the Args byte slices — do not alias the run's transcript; a consumer may retain or write into them freely.
func StripContent ¶ added in v0.6.0
StripContent returns ev with every content field emptied (the table below) — what a content-off destination receives. Nested recurses. The shape is kept: ids, names, counts, positions and usage survive, so a stripped record still attributes and orders; it is no longer replay-grade, by design.
The request, prompt and tools records (ADR 0028) are not events and have their own rule, which weft/otel applies: a content-off destination keeps the request record with its params.stop emptied (hashes, names and numbers survive) and drops the prompt and tools records, as it drops transcript batches.
Event Emptied Kept text_delta text run_id reasoning_delta text run_id tool_args_delta args run_id, name tool_start args (becomes null) seq, call_id, name tool_finish content seq, call_id, name, is_error steered messages (becomes []) seq, step run_finish each pending call's args usage, steps, pending ids and names run_start nothing (no content) all, instructions_hash included step_start nothing (no content) all step_finish nothing (no content) all nested recurses into event the envelope
Example ¶
StripContent empties every content field of an event — what a content-off destination receives. Identity survives; content does not.
package main
import (
"encoding/json"
"fmt"
"github.com/weftgo/weft"
)
func main() {
ev := weft.ToolFinish{RunID: "r", Seq: 3, CallID: "c1", Name: "lookup", Content: `{"status":"shipped"}`}
b, _ := json.Marshal(weft.StripContent(ev))
fmt.Println(string(b))
}
Output: {"type":"tool_finish","run_id":"r","seq":3,"call_id":"c1","name":"lookup","content":"","is_error":false}
func UnmarshalEvent ¶
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 ¶
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.
type InstructionsOption ¶ added in v0.6.0
type InstructionsOption = core.InstructionsOption
InstructionsOption is accepted by both New and Stream/Generate (the ThinkingOption shape): the system prompt is as per-question as reasoning depth. On an agent it is every run's default; on a run it replaces that default for the run alone.
func Instructions ¶
func Instructions(text string) InstructionsOption
Instructions sets the agent's system prompt. As an Option it is every run's default; as a RunOption it replaces the prompt for one run — the playground's prompt experiment, one run wide, no second agent built (WEFT-PLAYGROUND §10.1 [D5]). Manifest keeps reporting the agent's construction-time prompt.
type MaxStepsOption ¶ added in v0.6.0
type MaxStepsOption = core.MaxStepsOption
MaxStepsOption is accepted by both New and Stream/Generate (the ThinkingOption shape), with one rule on the run side: a run may only lower the agent's budget.
func MaxSteps ¶
func MaxSteps(n int) MaxStepsOption
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. As an Option it bounds every run; as a RunOption it may only LOWER the agent's value for the run — a raise fails the run with ErrInvalidRunOption before any model call, because a per-run raise is not a budget, it is the budget escaping (WEFT-PLAYGROUND §10.1 [D5]). Values below 1 are ignored.
type Message ¶
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 Repair ¶
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 UserParts ¶
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"}]}
type Model ¶
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 — optionally interleaved with ModelToolCallDelta progress as argument fragments stream — 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, yields a tool call with an empty ID or name, two tool calls sharing an ID in one step, or panics fails the run with an error wrapping ErrModelContract. A contract-violating adapter cannot corrupt a transcript silently.
func Unwrap ¶ added in v0.3.7
Unwrap reports the Model one level inside m — the model a middleware wrapper wraps — or nil when m does not implement the optional
interface{ Unwrap() Model }
convention (the same shape as InfoOf's). Model middleware declares its inner model with it — func (w *wrapper) Unwrap() core.Model { return w.next } — so a caller walks a chain one middleware at a time, without knowing the wrapper types:
for m := agt.Model(); m != nil; m = core.Unwrap(m) { … }
The walk ends at the first non-wrapper, which is the model New was given unless user middleware wrapped something of its own.
type ModelEvent ¶
type ModelEvent = core.ModelEvent
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 = core.ModelFinish
ModelFinish closes a step with its stop reason and token usage.
type ModelInfo ¶
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 ModelMiddleware ¶ added in v0.2.0
type ModelMiddleware = core.ModelMiddleware
ModelMiddleware wraps a Model, the chi shape: it sees every ModelRequest the loop builds and every event the inner model yields, and may retry, substitute, log, or rewrite. Implementations should forward Info (see InfoOf) so RunStart.Model and the manifest still name the underlying model, and declare the model they wrap with Unwrap (see the Unwrap helper) so a caller can walk the chain one middleware at a time.
type ModelReasoningDelta ¶
type ModelReasoningDelta = core.ModelReasoningDelta
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 = core.ModelRequest
ModelRequest is everything a model needs for one step: the system instruction, the transcript so far, and the callable tools.
Read-only: the loop builds each request with fresh copies of the Messages and Tools slices, so appending to them or reassigning their elements cannot reach the run or the agent. The values inside remain shared — message Content parts, and ToolDef fields frozen at construction — so implementations must still not modify them, and must clone anything they retain beyond the call.
type ModelTextDelta ¶
type ModelTextDelta = core.ModelTextDelta
ModelTextDelta is an increment of assistant text.
type ModelToolCall ¶
type ModelToolCall = core.ModelToolCall
ModelToolCall is one complete tool invocation request. ID is the provider's call identifier, echoed back on the matching ToolResultPart; it must be unique among one step's calls (results and approval decisions key on it). 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 ModelToolCallDelta ¶ added in v0.2.0
type ModelToolCallDelta = core.ModelToolCallDelta
ModelToolCallDelta is an increment of a streamed tool call's arguments — progress only: the assembled call still arrives whole as a ModelToolCall before ModelFinish. Adapters whose providers stream argument fragments (OpenAI-compatible function.arguments pieces, Anthropic input_json_delta) yield these so consumers can show the model "writing" a call instead of dead air; adapters whose calls arrive whole (Google) simply yield none. Index is the provider's fragment key where one exists (OpenAI's delta index); Name is the best-known name so far — for many providers only the first fragment of a call carries it.
type Nested ¶ added in v0.2.0
Nested wraps one event of a child run started by a Subagent tool. CallID is the parent's tool call that owns the child run; Seq is from the parent's counter, so the parent stream stays totally ordered with the child's events in place. Event is any child event — including a Nested from a grandchild — and its own RunID and Seq are the child's. A child's RunStart..RunFinish all arrive inside the parent's ToolStart..ToolFinish for the delegating call, and none arrive after its ToolFinish (the late-event rule, ADR 0004). On the wire the type is "nested" and UnmarshalEvent restores the inner event recursively.
type Option ¶
Option configures an Agent at construction. Options are small values returned by Instructions, MaxSteps, Parallelism, Sequential, StopWhen, Output, the PolicyOptions (MaxResultBytes, Timeout, StrictInput), and the Tool constructor.
func Content ¶ added in v0.6.0
Content sets whether this agent's runs put content into their records, overriding whatever the logger in force asks for. The zero Option (no Content call) means "as the logger says": capture is resolved at each emission, not at New, because agents are usually built before the observability pipeline installs and the global provider delegates.
Content(false): never, even if a destination wants it. Content(true): always, even with no destination asking (tests).
Either way, spans carry no content (ADR 0016 O7); this option governs record bodies only. The core reads no environment variable — turning capture on per destination is weft/otel's job, through the same standard Enabled channel the core consults.
func DetectLoops ¶ added in v0.2.0
DetectLoops fails a run with ErrLoopDetected when repeats consecutive steps request the same set of tool calls — the same names with the same arguments, in any order. Varying arguments are not a loop: a corrected retry has a different signature. Results are not part of the signature: a tool whose output carries a timestamp must not hide a loop, and a result-inclusive hash would miss it. Off by default (values below 2 are ignored); the CLI scaffold writes DetectLoops(5).
Example ¶
A stuck model repeating one request: DetectLoops fails the run loudly instead of burning the step budget.
package main
import (
"context"
"errors"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
echo := weft.Tool("echo", "", func(_ context.Context, _ struct{}) (string, error) {
return "ok", nil
})
turns := []wefttest.Turn{}
for range 3 {
turns = append(turns, wefttest.ToolCalls(wefttest.Call{Name: "echo", Args: `{"i":1}`}))
}
agt := weft.New(wefttest.Script(turns...), echo, weft.DetectLoops(3))
_, err := agt.Generate(context.Background(), weft.Prompt("q"))
fmt.Println(errors.Is(err, weft.ErrLoopDetected))
}
Output: true
func Logger ¶ added in v0.2.0
Logger sets the logger the agent's runs report to, at Debug level: one line when a run starts and ends, one per model call, one per tool call — run and call ids, the model, durations, usage, stop reasons and outcomes; never message text, tool arguments or tool results. Error text is the one exception: a failed run, model call or tool call logs the error it returned, because a log is the caller's. nil (the default) means slog.Default, which is silent until its handler enables Debug, so weft logs nothing in a program that did not ask; slog.New(slog.DiscardHandler) turns the lines off outright. The lines carry the span-carrying context, so a handler that bridges to OTel correlates them with the spans for free. mw.Log and mw.Audit are separate: middleware the caller places, at the level the caller chooses.
func LoggerProvider ¶ added in v0.6.0
func LoggerProvider(lp log.LoggerProvider) Option
LoggerProvider sets the OpenTelemetry logger provider the agent's runs emit records to. Without it, runs use the global provider (go.opentelemetry.io/otel/log/global), a no-op until an SDK registers one — so a program that sets up an SDK gets weft's records with no weft option at all, and capture stays off until a destination asks for it. Tests and dependency-injected programs pass their own provider here instead of touching the global. Records are the run's events, deltas and transcript batches as OTel log records (ADR 0024); spans still go to the tracer provider (TracerProvider).
func MaxModelRetries ¶ added in v0.2.0
MaxModelRetries sets how many consecutive RETRY results (see ModelRetry) one tool may produce in a run before the run fails with ErrModelRetriesExceeded (default 3). The count is per tool name — two parallel calls to the same tool both retrying count as two — and a successful result for the tool resets it. Calls resumed under Approve do not feed the counter: they run before step 0 and belong to no step (ADR 0007). Values below 1 are ignored.
func Name ¶
Name names the agent: it appears on RunStart.Agent and in the manifest, which requires it (core.Manifest errors on unnamed agents). Empty values are ignored.
func OnRunEnd ¶ added in v0.3.5
OnRunEnd registers an observer the loop calls exactly once per run, after RunFinish is delivered or the RunError is built and before Run returns — the outcome signal a tap cannot carry: a failed run emits no event after its last delivered one (ADR 0004), so failure is invisible to Tap, and neither middleware seam wraps the run. res is the run's result and err its error: on success err is nil and res is complete; on failure err is the *RunError and res its partial transcript (ADR 0002) — the same value RunError.Result holds; on cancellation err is the RunError wrapping the ctx error. For a Subagent's child run it fires inside the parent's tool call, like the child's events, and res.ID names the child. A run that panics (a PrepareStep function is arbitrary user code) never fires it: the run crashed, it did not end — the panic still reaches the caller. Like Tap it observes and cannot change anything (ADR 0006's note: not a third seam — a seam wraps a call, this observes an outcome); like Tap a panic in it is contained and counted (TapPanics). Several OnRunEnd options run in registration order. ctx is the run's span-carrying context. The one consumer this ships for is the store (TODO §11): store.Record pairs a Tap for the event stream with OnRunEnd for the result and the failure.
Example ¶
OnRunEnd is the outcome observer: unlike Tap it sees the run's end even when the run fails, because a failed run emits no event after its last delivered one. The store's Record option pairs the two.
package main
import (
"context"
"errors"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
agt := weft.New(
wefttest.Script(wefttest.Fail(errors.New("provider down"))),
weft.OnRunEnd(func(_ context.Context, res *weft.RunResult, err error) {
fmt.Printf("run ended: has id=%v failed=%v steps=%d\n", res.ID != "", err != nil, res.NumSteps())
}),
)
_, _ = agt.Generate(context.Background(), weft.Prompt("hi"))
}
Output: run ended: has id=true failed=true steps=0
func Options ¶ added in v0.2.0
Options composes several options into one, applied in order — a family of tools and policy closed over its dependencies as a single value. A plugin is `func(deps) core.Option`, and dependencies are parameters, never globals; nothing registers itself, so there is no registry, no scopes, no dedup. Nil entries are ignored; duplicate tool names still panic at New. Nesting composes by construction.
Example ¶
Composing agents: a plugin is func(deps) weft.Option — a family of tools and its policy closed over its dependencies as one value. Dependencies are parameters, never globals.
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
orders := func(deps *string) weft.Option {
return weft.Options(
weft.Instructions("You handle orders."),
weft.Tool("lookup_order", "Look up an order by id.",
func(_ context.Context, in struct {
ID string `json:"id" jsonschema:"the order id"`
}) (string, error) {
return "order " + in.ID + ": " + *deps, nil
}),
weft.MaxResultBytes(1024),
)
}
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "lookup_order", Args: `{"id":"42"}`}),
wefttest.Say("Done."),
), orders(new(string)))
res, err := agt.Generate(context.Background(), weft.Prompt("Where is order 42?"))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Steps[0].Results[0].Content)
}
Output: order 42:
func Output ¶ added in v0.2.0
Output constrains the run's final answer to Out. It registers a tool named submit_output whose input schema is reflected from Out, exactly as Tool reflects a handler's input, and stops the run once the model has called it with arguments that decode. Invalid arguments come back to the model as an ErrInvalidToolInput result naming the field, so repair is the ordinary tool-error loop — nothing special happens. Read the value with GenerateAs, or OutputOf after Stream:
type Verdict struct {
Approved bool `json:"approved"`
Reason string `json:"reason" jsonschema:"one sentence"`
}
agt := core.New(model, core.Output[Verdict](), lookup)
v, res, err := core.GenerateAs[Verdict](ctx, agt, core.Prompt("Review order 42."))
Tool mode works on every provider; adapters with a native JSON-schema mode may use it later behind the same API. Output panics if Out is not a struct (or a pointer to one), for the reason Tool does.
func PrepareStep ¶ added in v0.2.0
func PrepareStep(fn func(ctx context.Context, step int, req ModelRequest) (ModelRequest, error)) Option
PrepareStep installs a function the loop calls before every model call, with the request it built for step: the agent's instructions, the transcript so far, the step's tool snapshot, the run's thinking level. The function returns the request the step uses — trimmed messages, a subset of tools, a rewritten system — or an error that fails the run. What it returns is what the step advertises and dispatches against: a tool it removes cannot be called that step, and a ToolDef it adds can. PromptSnippets are composed after it, from the tools it returns, so removing a tool removes its snippet without the function having to know snippets exist. The request is a copy the function may mutate freely: message parts, argument bytes, and tool definitions are cloned before the chain runs, so in-place writes reach neither the transcript nor the agent's frozen registry. The transcript in RunResult is never affected; only the request is. It runs before the model seam, so WrapModel middleware sees the prepared request. Several PrepareStep options run in order, each receiving the previous one's result. A nil function is ignored. Like ToolSource, it is one of the two knobs that can break a prompt-cache prefix — trim deliberately.
Example ¶
Phased tool exposure: the first step plans with read-only tools; the second acts. PrepareStep is the one loop knob — what it returns is what the step both advertises and dispatches against.
package main
import (
"context"
"fmt"
"log"
"slices"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
lookup := weft.Tool("lookup", "Look up an order.", func(_ context.Context, _ struct{}) (string, error) {
return "order 1234: broken item", nil
})
refund := weft.Tool("refund", "Refund an order.", func(_ context.Context, _ struct{}) (string, error) {
return "refunded", nil
})
phase := func(_ context.Context, step int, req weft.ModelRequest) (weft.ModelRequest, error) {
if step == 0 { // investigate before acting
req.Tools = slices.DeleteFunc(req.Tools, func(t *weft.ToolDef) bool { return t.Name == "refund" })
}
return req, nil
}
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "refund"}), // not advertised in step 0
wefttest.ToolCalls(wefttest.Call{Name: "refund"}), // now it is
wefttest.Say("Done."),
), lookup, refund, weft.PrepareStep(phase))
res, err := agt.Generate(context.Background(), weft.Prompt("Refund order 1234."))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Steps[0].Results[0].Content)
fmt.Println(res.Steps[1].Results[0].Content)
}
Output: NO_SUCH_TOOL: no tool named "refund" refunded
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:
core.StopWhen(core.HasToolCall("submit_answer"))
core.StopWhen(core.StopFunc(func(steps []core.StepRecord) bool { ... }))
Stop conditions are the intended end of a run; MaxSteps is the safety budget behind them.
func Tap ¶
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 — the same consumer-speed coupling Run.Events documents. Slow observation (a database write, a network sink) must not happen inside the tap: hand each event to a queue and drain it on your own goroutine; ExampleTap_async is the tested pattern. Taps run in registration order; a panic in one is recovered, counted (see TapPanics), 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, which carries the run's span, so a tap that starts its own spans parents them under it for free.
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
Example (Async) ¶
Slow observers (a database write, a network sink) must not run inside a Tap: taps are synchronous and run under the step's event-ordering lock, so a slow tap delays every tool event of its step. The pattern: hand each event to a queue inside the tap — never block — and drain it on your own goroutine, which may be as slow as it likes. A bounded queue with a visible drop counter keeps a stuck drain from wedging the run.
package main
import (
"context"
"fmt"
"log"
"sync/atomic"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
events := make(chan weft.Event, 1024)
var dropped atomic.Int64
done := make(chan int)
go func() {
starts := 0
for ev := range events {
if _, ok := ev.(weft.ToolStart); ok {
starts++
}
}
done <- starts
}()
agt := weft.New(
wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "ping"}),
wefttest.Say("done"),
),
weft.Tap(func(_ context.Context, ev weft.Event) {
select {
case events <- ev: // fast: hand to the belt
default: // full belt: drop, but visibly
dropped.Add(1)
}
}),
weft.Tool("ping", "", func(_ context.Context, _ struct{}) (string, error) {
return "pong", nil
}),
)
if _, err := agt.Generate(context.Background(), weft.Prompt("hi")); err != nil {
log.Fatal(err)
}
close(events)
fmt.Println("tool starts:", <-done, "dropped:", dropped.Load())
}
Output: tool starts: 1 dropped: 0
func ToolSource ¶
ToolSource replaces the tool set the loop advertises and dispatches against with the given function's return value, fetched fresh exactly once per step (and once per manual Agent.CallTool): the step's advertisement and its dispatch both resolve against that one snapshot, so what the model was shown is exactly what runs. The seam is for registries that change while the agent runs (plugins installed mid-run, MCP servers polled per step); a tool registered mid-step becomes callable on the next step's fetch. The Agent stays immutable: the source is a value; synchronization and freshness of the list belong to the source's owner. A snapshot with a duplicate name fails the run with ErrDuplicateTool and one with a nil entry with ErrNilTool (the runtime analogues of New's duplicate-name panic) rather than silently dropping the second tool or advertising a dereference. 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.
func TracerProvider ¶ added in v0.2.0
func TracerProvider(tp trace.TracerProvider) Option
TracerProvider sets the OpenTelemetry tracer provider the agent's runs report spans to. Without it, runs use the global provider (otel.GetTracerProvider), which is a no-op until an SDK registers one — so a program that sets up an SDK gets weft's spans with no option at all. Tests and dependency-injected programs pass their own provider here instead of touching the global. Every run reports one invoke_agent span, one chat span per model call, and one execute_tool span per executed tool call, with the GenAI semantic attributes (ADR 0016); no message or tool-argument content is ever put on a span.
func UsageLimit ¶ added in v0.2.0
UsageLimit bounds a run's total token usage — the run's own model calls plus every subagent's (RunResult.Usage). A field left zero is unlimited. The limit is checked after each step, before the loop makes another model call: a step that ends the run — final answer, StopWhen, pending approvals — may overshoot and still succeed, because a budget's job is to stop further spend, not to discard finished work. Exceeding it fails the run with ErrUsageLimit and the partial transcript on RunError.Result. There is no default limit: the right value is workload-specific, and MaxSteps is the default budget. Usage.Total() is not a separate limit — a caller who wants a total sets both fields.
Example ¶
A token budget: exceeded, the run fails with ErrUsageLimit before the next model call; the partial transcript rides on RunError.Result.
package main
import (
"context"
"errors"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
echo := weft.Tool("echo", "", func(_ context.Context, _ struct{}) (string, error) {
return "ok", nil
})
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "echo"}),
wefttest.Say("never reached"),
), echo, weft.UsageLimit(weft.Usage{OutputTokens: 4}))
_, err := agt.Generate(context.Background(), weft.Prompt("again"))
var re *weft.RunError
if errors.As(err, &re) {
fmt.Println(errors.Is(err, weft.ErrUsageLimit), "steps kept:", len(re.Result.Steps))
}
}
Output: true steps kept: 1
func WrapModel ¶ added in v0.2.0
func WrapModel(mw ...ModelMiddleware) Option
WrapModel installs model middleware around the agent's model. The first middleware listed is the outermost — WrapModel(a, b) calls a(b(model)) — and successive WrapModel options append inward. The chain is built once, at New, and sees each step's ModelRequest as the loop built it. The reference set is in package mw: Retry, Fallback, Log, RepairJSON. Nil entries are ignored.
Example ¶
Model middleware wraps the agent's model, chi-style: the first listed is the outermost. The reference set lives in package mw.
package main
import (
"context"
"fmt"
"iter"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
logged := func(next weft.Model) weft.Model {
return loggingModel{next: next}
}
agt := weft.New(wefttest.Script(wefttest.Say("hello")), weft.WrapModel(logged))
res, err := agt.Generate(context.Background(), weft.Prompt("hi"))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Text())
}
type loggingModel struct{ next weft.Model }
func (m loggingModel) Info() weft.ModelInfo { return weft.InfoOf(m.next) }
func (m loggingModel) Stream(ctx context.Context, req weft.ModelRequest) iter.Seq2[weft.ModelEvent, error] {
fmt.Printf("model call: %d messages\n", len(req.Messages))
return m.next.Stream(ctx, req)
}
Output: model call: 1 messages hello
type OutputDecoder ¶ added in v0.3.0
type OutputDecoder[Out any] = core.OutputDecoder[Out]
OutputDecoder turns a streaming run's submit_output argument deltas into a filling-in Out — the UI half of structured output: a consumer rendering a form as the model writes it. It is a decoder value, not an event-stream wrapper: feed it the events you already consume and render what comes back.
dec := core.NewOutputDecoder[Form]()
for ev, err := range run.Events() {
if err != nil { return err }
if p, ok := dec.Feed(ev); ok { render(p) }
}
form, err := dec.Result()
The decoder keys on the submit_output stream identity ToolArgsDelta carries — the tool name and step boundaries — resets its buffer on StepStart and on a fresh submit_output ToolStart, and closes it on the matching ToolFinish. Nested events are ignored: a subagent's structured output is its own decoder's job. Decode is lenient and prefix-shaped: after each delta it attempts the longest closed prefix of the arguments so far (see partial_json.go) and reports it when it changed and decoded — fields not yet present stay zero, a garbage mid-stream prefix keeps the last good partial, and errors surface only at Result, which follows the OutputOf rule (the last submit_output call with a non-error result) and returns ErrNoOutput when none finished. Never model-visible: the decoder reads the stream and writes nothing back.
Cost: every delta rescans the buffered arguments (the close is linear in what has arrived), so a submission's decode cost grows with the square of its size — the price of a fresh partial on every delta. At tool-argument scale (a few KiB) it is noise; a UI feeding very large submissions can trade freshness for linearity by calling Feed less often — the decoder keeps the last good partial across the deltas it skips.
Example ¶
An OutputDecoder renders structured output while it streams: feed it the events you already consume and draw the filling-in form.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
type Form struct {
Name string `json:"name"`
Count int `json:"count"`
}
turn := wefttest.Raw(
weft.ModelToolCallDelta{Index: 0, Name: "submit_output", Args: `{"name":"Ada",`},
weft.ModelToolCallDelta{Index: 0, Name: "submit_output", Args: `"count":3}`},
weft.ModelToolCall{ID: "c1", Name: "submit_output", Args: json.RawMessage(`{"name":"Ada","count":3}`)},
weft.ModelFinish{Reason: weft.StopToolCalls, Usage: weft.Usage{InputTokens: 3, OutputTokens: 2}},
)
run := weft.New(wefttest.Script(turn), weft.Output[Form]()).Stream(context.Background(), weft.Prompt("fill the form"))
dec := weft.NewOutputDecoder[Form]()
for ev, err := range run.Events() {
if err != nil {
log.Fatal(err)
}
if p, ok := dec.Feed(ev); ok {
fmt.Printf("render: name=%q count=%d\n", p.Name, p.Count)
}
}
form, err := dec.Result()
if err != nil {
log.Fatal(err)
}
fmt.Printf("final: name=%q count=%d\n", form.Name, form.Count)
}
Output: render: name="Ada" count=0 render: name="Ada" count=3 final: name="Ada" count=3
func NewOutputDecoder ¶ added in v0.3.0
func NewOutputDecoder[Out any]() *OutputDecoder[Out]
NewOutputDecoder returns a decoder ready to feed a run's events.
type ParallelismOption ¶ added in v0.6.0
type ParallelismOption = core.ParallelismOption
ParallelismOption is accepted by both New and Stream/Generate (the ThinkingOption shape), with the MaxStepsOption rule: a run may only lower the agent's width.
func Parallelism ¶
func Parallelism(n int) ParallelismOption
Parallelism sets the maximum number of a step's tool calls executing at once (default 4). As an Option it bounds every run; as a RunOption it may only LOWER the agent's value for the run — a raise fails with ErrInvalidRunOption before any model call (the MaxSteps rule; the per-run knob is for safety, not for escape). Values below 1 are ignored.
type ParamsOption ¶ added in v0.3.0
type ParamsOption = core.ParamsOption
ParamsOption is accepted by both New and Stream/Generate: sampling knobs are as per-question as a forced choice (the ThinkingOption shape).
func Params ¶ added in v0.3.0
func Params(p RequestParams) ParamsOption
Params sets the agent's default sampling knobs — Temperature, TopP, MaxTokens, Stop, Seed — for every run's model calls; as a RunOption it overrides that default for one run:
agt := core.New(m, core.Params(core.RequestParams{Temperature: ptr(0.2)}))
agt.Generate(ctx, core.Params(core.RequestParams{Temperature: ptr(0.9)}), core.Prompt(q))
A nil or empty field keeps the adapter's construction-time default for that knob (its Temperature option, and so on); a run-level Params replaces the agent's struct whole, never merging field by field — set every knob the override should carry. A PrepareStep function can edit the request's Params per step ("cold for classification steps, creative for drafting" is a two-line function). Adapters drop knobs their provider lacks, and the provider's own limits apply (google narrows Seed to int32).
Example ¶
Params sets per-step sampling: a PrepareStep function turns the temperature down for the classifying step and back for drafting — one struct, edited per step, no second Model construction.
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
p := func(f float64) *float64 { return &f }
classify := weft.Tool("classify", "Classify the request.",
func(_ context.Context, _ struct{}) (string, error) { return "billing", nil })
agt := weft.New(
wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "classify"}),
wefttest.Say("A billing question, answered at temperature 0.9."),
),
weft.Params(weft.RequestParams{Temperature: p(0.9)}),
weft.PrepareStep(func(_ context.Context, step int, req weft.ModelRequest) (weft.ModelRequest, error) {
if step == 0 {
req.Params.Temperature = p(0) // cold for classification
}
return req, nil
}),
classify,
)
res, err := agt.Generate(context.Background(), weft.Prompt("Why did my invoice double?"))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Text())
}
Output: A billing question, answered at temperature 0.9.
type Part ¶
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 PolicyOption ¶ added in v0.2.0
type PolicyOption = core.PolicyOption
PolicyOption is accepted by both New and Tool. On an agent it is the default policy for every tool call; on a tool it overrides the agent's default for that tool. The manifest records both levels.
func MaxResultBytes ¶
func MaxResultBytes(n int) PolicyOption
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. On a tool it overrides the agent's cap for that tool alone — a per-tool MaxResultBytes(0) lifts the cap for a tool whose output must arrive whole. Agent.CallTool returns uncapped output: the cap is a run policy, applied by the loop.
func Sequential ¶
func Sequential() PolicyOption
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.
On a tool, Sequential is a barrier: the dispatcher lets every in-flight call of the step finish, runs this call alone, then resumes the step's parallelism for the calls after it. Other tools keep running in parallel; only this one is serialized.
Example ¶
A Sequential tool is a barrier: it runs alone. The step's calls in flight finish first, and the calls after it wait — under any parallelism. Results stay in call order either way.
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
write := weft.Tool("write", "Append to the ledger.", func(_ context.Context, _ struct{}) (string, error) {
return "written", nil
}, weft.Sequential())
check := weft.Tool("check", "Verify the ledger.", func(_ context.Context, _ struct{}) (string, error) {
return "verified", nil
})
model := wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "check"}, wefttest.Call{Name: "write"}, wefttest.Call{Name: "check"}),
wefttest.Say("done"),
)
res, err := weft.New(model, write, check, weft.Parallelism(4)).Generate(context.Background(), weft.Prompt("x"))
if err != nil {
log.Fatal(err)
}
for _, r := range res.Steps[0].Results {
fmt.Println(r.Name, "->", r.Content)
}
}
Output: check -> verified write -> written check -> verified
func StrictInput ¶ added in v0.2.0
func StrictInput() PolicyOption
StrictInput rejects tool arguments that carry fields the input struct does not declare, as an ErrInvalidToolInput result naming the field. The default is lenient — undeclared fields are ignored, the encoding/json default — because models routinely add stray keys and a rejection costs a round trip, not accuracy. Use it where an ignored field would be a silent misinterpretation of the call. On the agent it applies to every tool; on a tool to that tool alone. RawTool handlers are unaffected: they receive the raw arguments.
func Timeout ¶ added in v0.2.0
func Timeout(d time.Duration) PolicyOption
Timeout bounds one tool call. On a tool it is that tool's deadline — Timeout(0) on a tool removes the agent's default for that tool alone, the timeout analogue of MaxResultBytes(0); on the agent it is the default for every tool without its own, and non-positive values are ignored there. The handler's ctx carries the deadline; when it expires the loop records an error result ("tool X timed out after 10s") the model sees and moves on, abandoning the handler's goroutine — handlers must honour ctx to release their resources. Without any Timeout only the run's ctx bounds a call.
Example ¶
Per-tool policy: trailing options on Tool override the agent's defaults for that tool alone. Here a slow tool times out into an error result the model sees, and the run carries on.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
slow := weft.Tool("slow", "Takes a while.",
func(ctx context.Context, _ struct{}) (string, error) {
<-ctx.Done() // a well-behaved handler honours the deadline
return "", ctx.Err()
},
weft.Timeout(10*time.Millisecond))
model := wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "slow"}),
wefttest.Say("It did not answer in time."),
)
agt := weft.New(model, slow)
res, err := agt.Generate(context.Background(), weft.Prompt("Try the slow tool."))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Steps[0].Results[0].Content)
fmt.Println(res.Text())
}
Output: tool "slow" timed out after 10ms It did not answer in time.
func WrapTools ¶ added in v0.2.0
func WrapTools(mw ...ToolMiddleware) PolicyOption
WrapTools installs tool middleware. On the agent it wraps every tool call the loop (and Agent.CallTool) dispatches; on a tool it wraps that tool alone, inside the agent's chain: agent middleware → tool middleware → decode → handler. The first middleware listed is the outermost, so WrapTools(a, b) runs a(b(call)), and successive WrapTools options append inward. Middleware sees CallFromContext and may decorate ctx for the handler (see ExampleWrapTools_context); it returns the result text or an error — a *ToolError to give the model a code, an error wrapping ErrApprovalRequired to park the call on RunResult.Pending. The reference set is in package mw: Allow, Audit, MapErrors. Nil entries are ignored.
Example ¶
Tool middleware wraps every call the loop dispatches. A middleware that returns an error produces an error result the model sees; a *weft.ToolError gives it a code.
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
readOnly := func(next weft.ToolCaller) weft.ToolCaller {
return func(ctx context.Context, call weft.ToolCallPart) (string, error) {
if strings.HasPrefix(call.Name, "delete_") {
return "", &weft.ToolError{Code: "DENIED", Message: "this agent is read-only"}
}
return next(ctx, call)
}
}
del := weft.Tool("delete_order", "", func(_ context.Context, _ struct{}) (string, error) { return "deleted", nil })
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "delete_order"}),
wefttest.Say("I cannot do that."),
), del, weft.WrapTools(readOnly))
res, err := agt.Generate(context.Background(), weft.Prompt("delete order 1"))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Steps[0].Results[0].Content)
fmt.Println(res.Text())
}
Output: DENIED: this agent is read-only I cannot do that.
Example (Context) ¶
The context-decoration convention: middleware that has verified something (here, who is calling) adds it to ctx before next, and the handler reads it back through a typed accessor — the same shape as weft.CallFromContext. Decorate only with data the middleware has verified; derive business values in the handler after decode.
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
authUser := func(next weft.ToolCaller) weft.ToolCaller {
return func(ctx context.Context, call weft.ToolCallPart) (string, error) {
user, err := verifyUser(ctx) // a session lookup, a token check, ...
if err != nil {
return "", &weft.ToolError{Code: "UNAUTHENTICATED", Message: "sign in first", Err: err}
}
return next(withUser(ctx, user), call)
}
}
myOrders := weft.Tool("my_orders", "List the caller's orders.",
func(ctx context.Context, _ struct{}) (string, error) {
u, ok := userFromContext(ctx)
if !ok {
return "", weft.Errorf("UNAUTHENTICATED", "no user on the call")
}
return "orders for " + u.Name, nil
})
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "my_orders"}),
wefttest.Say("Here they are."),
), myOrders, weft.WrapTools(authUser))
res, err := agt.Generate(context.Background(), weft.Prompt("show my orders"))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Steps[0].Results[0].Content)
}
type exampleUser struct{ Name string }
type userKey struct{}
func withUser(ctx context.Context, u exampleUser) context.Context {
return context.WithValue(ctx, userKey{}, u)
}
func userFromContext(ctx context.Context) (exampleUser, bool) {
u, ok := ctx.Value(userKey{}).(exampleUser)
return u, ok
}
func verifyUser(context.Context) (exampleUser, error) { return exampleUser{Name: "ada"}, nil }
Output: orders for ada
type RawPair ¶ added in v0.10.0
RawPair is one attempt's wire bodies: the request as sent and the response as received, as the reporter holds them. The bytes are content (the request carries the prompt), governed by the content policy wherever they are recorded.
type ReasoningDelta ¶
type ReasoningDelta = core.ReasoningDelta
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.
type ReasoningPart ¶
type ReasoningPart = core.ReasoningPart
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: core.ReasoningPart core.TextPart
type ReplayPolicy ¶ added in v0.7.0
type ReplayPolicy = core.ReplayPolicy
ReplayPolicy is a tool's side-effect class: what a re-run of a recorded conversation (a playground experiment, a replay fixture) may do with a call to this tool. The zero value, ReplayNever, is what an unannotated tool counts as — nobody has vouched for it, so its calls are substituted with the recorded result or parked for a human decision, never silently re-fired (WEFT-PLAYGROUND.md §6 rule 3; the one class a refund belongs to).
type Reporter ¶ added in v0.10.0
Reporter is the reporting path from the model chain into the loop's own record (ADR 0016): middleware and adapters inside the chain tell the run's observer about attempts and wire bodies, which the loop cannot see from outside the chain. It is reporting, not a seam (ADR 0006): no report alters a step, a retry, a tool call or a model choice. A report never returns an error, never blocks and never panics into the caller — a panicking tracer or log handler is contained and counted in Agent.TapPanics, the report dropped. A report made after its model call ended is dropped, best-effort: a goroutine the chain left behind that reports while the call is ending may still land one attempt under the ended chat span. The zero Reporter, and the one ReportFromContext returns outside a run's model call, discards every report; a Reporter is safe for concurrent use.
Layers that report must not double-report one provider request. A model or middleware that reports its own attempts says so with an optional method, ReportsAttempts() bool, returning true; a reporting layer above it (mw.Retry, mw.Fallback) walks the Unwrap chain, finds the marker and stays silent. A layer that unwraps to a self-reporting model but may not stream through it (a router) returns false, which ends the walk. The marker is a convention, not a core type (ADR 0013).
func ReportFromContext ¶ added in v0.10.0
ReportFromContext returns the reporter of the model call whose context ctx is (or derives from): the loop puts one on the context it hands to the model chain, so a ModelMiddleware or a Model adapter reaches it from the ctx of its Stream. Outside a run's model call — a tool handler, a bare Model.Stream, a run started on the chain's context (it is masked at run start), any context the loop did not hand the chain — it is a no-op Reporter, never nil, so callers do not check. Using it is optional for adapters (ADR 0013).
type RequestParams ¶ added in v0.3.0
type RequestParams = core.RequestParams
RequestParams is per-step sampling: the knobs a caller turns between "cold for classification, creative for drafting". Every field is a pointer or slice so the three states stay distinguishable — nil or empty keeps the adapter's construction default (its Temperature, TopP, MaxTokens, Stop, or Seed option), set overrides it for this request alone, and the adapter never replaces a construction value with a zero. A set pointer to 0 is a value (Temperature of exactly 0 is sent), with one provider exception: an anthropic MaxTokens of 0 falls to the adapter's default, because the API requires a positive value. A negative MaxTokens fails the run at the step that carries it — no provider accepts one, and the loop names the bug rather than letting each adapter improvise. A run-level Params option replaces the agent's struct whole, it does not merge field by field; PrepareStep can edit it per step. Adapters drop knobs their provider lacks (Seed on anthropic) under the "adapters document what they drop" rule.
type Role ¶
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.
type Run ¶
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.
type RunError ¶
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.
type RunFinish ¶
RunFinish is always the final event of a successful run and carries the run's total usage and step count.
type RunOption ¶
RunOption configures a single run.
func Approve ¶ added in v0.2.0
Approve resumes a call left on RunResult.Pending by an earlier run: pass the earlier transcript with Messages and the decision, and the loop executes the call — through the ordinary tool chain, with Call.Approved set — before its next model call. Ids that are not pending are ignored.
func Deny ¶ added in v0.2.0
Deny resolves a pending call without running it: the model sees an error result reading "DENIED: <reason>" and the loop continues. Pending calls given neither Approve nor Deny are denied with the reason "no decision" ("DENIED: no decision").
func Messages ¶
Messages adds existing messages (a session transcript, few-shot examples) to the run's input.
func Metadata ¶ added in v0.6.0
Metadata returns the RunOption attaching caller key/value pairs to the run: every span and every record of the run carries them, and so do the runs of its subagents (the pairs ride the context). Several Metadata options merge in order; a later key wins. Keys under "weft." are the weft modules' namespace by convention (thread writes weft.session.id); the core does not police callers.
Limits, because metadata rides every span and record: at most 64 keys, a key at most 128 bytes, a value at most 1024 bytes. An entry over a limit — or with an empty key — is dropped, never truncated, and counted on the run's invoke_agent span (weft.metadata.dropped); under the key cap the "weft." keys are kept first, then the rest in sorted order, so a large tag set never costs a run its session identity. Read the merged, limited view back with MetadataFromContext, e.g. inside a Tap, a tool handler, or a Subagent's child run.
Example ¶
Metadata attaches caller key/value pairs to one run: every span and record of the run carries them, and a Subagent's child run inherits them through the context. Keys under "weft." are the weft modules' namespace — thread stamps weft.session.id this way.
package main
import (
"context"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
agt := weft.New(wefttest.Script(wefttest.Say("ok")),
weft.Tap(func(ctx context.Context, ev weft.Event) {
if _, ok := ev.(weft.RunStart); !ok {
return
}
md := weft.MetadataFromContext(ctx)
fmt.Println("tenant =", md["tenant"], "session =", md["weft.session.id"])
}))
_, _ = agt.Generate(context.Background(),
weft.Metadata(map[string]string{
"tenant": "acme",
"weft.session.id": "s_01",
}),
weft.Prompt("hello"))
}
Output: tenant = acme session = s_01
func OnMessages ¶ added in v0.5.0
OnMessages returns the RunOption registering an observer the loop calls whenever messages join the run's transcript: the assistant message a step produced (reasoning, text and calls in their final shape, signatures included), the tool message that follows its calls, and the messages a steering drain delivered. msgs is exactly what joined, in transcript order, as a deep copy — retaining or mutating it changes nothing the run sees — and step is the step the messages belong to (a steer's messages name the step whose drain delivered them). The calls are synchronous on the run's goroutine, in transcript order, so a consumer that appends each batch to durable storage persists a mid-run crash's worth of exact transcript; like Tap an observer must be fast and must not block (the run waits for it), and like Tap a panic in one is contained and counted (TapPanics), never breaking the run. Observers cannot change anything — the transcript is the run's; behaviour attaches at the two seams. Several OnMessages options run in registration order. A Subagent's child run does not inherit them: a child's transcript belongs to whoever runs the child (the same rule as Steering). This is TODO §5.12's shape (b), the answer to "reconstruct messages from events (lossy: signatures, block boundaries) or wait for the run to end": the exact bytes, as they join.
func OnlyTools ¶ added in v0.6.0
OnlyTools narrows this run to the named tools among the agent's registered ones — the ToolSource snapshot when one exists, fetched fresh per step as ever. Narrowing only: the playground cannot add a tool, because a new tool is code. A name the agent does not have fails the run with ErrInvalidRunOption before any model call. The step's advertisement and its dispatch resolve against the same narrowed snapshot, so what the model was shown is exactly what runs; calls to a tool a PrepareStep function dropped fail as unknown, as today. Manifest and Agent.Tools keep reporting the static set: they describe the code, not one run's experiment (WEFT-PLAYGROUND §10.1 [D5]). With no names, the run keeps the agent's full set. Several OnlyTools options add up: the run keeps every tool any of them names.
Example ¶
The playground's per-run configuration (WEFT-PLAYGROUND §10.1): one run of an immutable agent, changed without rebuilding it. OnlyTools narrows to registered tools; UseModel swaps in an allowed alternate.
package main
import (
"context"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
lookup := weft.Tool("lookup", "Look up an order.", func(ctx context.Context, in struct {
ID string `json:"id"`
}) (string, error) {
return `{"status":"shipped"}`, nil
})
agt := weft.New(wefttest.Script(wefttest.Say("order shipped")),
weft.Name("support"), lookup)
_, _ = agt.Generate(context.Background(),
weft.Prompt("where is order 4411?"),
weft.OnlyTools("lookup"), // narrowing; unknown name → ErrInvalidRunOption
weft.Instructions("Answer in one short line."), // this run's prompt
)
}
Output:
func ParkAllExcept ¶ added in v0.8.0
ParkAllExcept parks, at the approval boundary (ADR 0007), every tool call of the run whose tool is not named — ParkOn turned around: the caller lists what may run, and everything else waits for a decision. It is the rule for a run that must not fire a side effect nobody vouched for (a playground re-run, WEFT-PLAYGROUND §6 rule 3; ADR 0024 D7), where a list of tools to park cannot be complete:
- The rule is applied by name to the tool each call resolves to in its step's dispatch snapshot, so a tool only a ToolSource supplies — absent from Agent.Tools and the manifest — parks like any other.
- It reaches the runs started inside this run: a Subagent's child run (any run on a tool call's context) applies the same rule to its own tools, where ParkOn stops at the run it was given to. A child that parks ends as a child approval boundary always has — the delegating call's result is SUBAGENT_PENDING (ADR 0014); the parent does not park. Names are matched in parent and child alike, so name a tool only if every tool of that name down the delegation may run. A child run's own ParkAllExcept can narrow the inherited list, never widen it.
A parked call is ParkOn's parked call: its ToolStart and no ToolFinish, the run ending successfully with it on RunResult.Pending, Approve/Deny/Resolve on the resuming run deciding it — an approved call runs once even when the resume carries the rule again, and the next call to the tool parks again. The rules only add up toward parking: a tool ParkOn names or built with RequireApproval parks whether or not it is named here, and several ParkAllExcept options let through only the names all of them list. OnlyTools is independent — it decides what is offered, this decides what of it runs unasked. A name no tool carries is not an error (a ToolSource's names are not known up front) and with no names every call parks — except an agent's own Output submission (submit_output on an agent built with Output, in this run or a child's): it is the run's answer, not a side effect, so it never needs naming; ParkOn can still park it.
Example ¶
ParkAllExcept is the default-deny park rule: the caller names what may run, and every other tool call parks — including a tool only a ToolSource supplies, which no list built from Agent.Tools could name.
package main
import (
"context"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
lookup := weft.Tool("lookup", "Look up an order.", func(context.Context, struct{}) (string, error) {
return "shipped", nil
})
wire := weft.Tool("wire_money", "Send a payment.", func(context.Context, struct{}) (string, error) {
return "sent", nil
})
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(
wefttest.Call{Name: "lookup", ID: "c1"},
wefttest.Call{Name: "wire_money", ID: "c2"},
),
wefttest.Say("paid"),
), weft.ToolSource(func() []*weft.ToolDef { return []*weft.ToolDef{lookup, wire} }))
res, err := agt.Generate(context.Background(),
weft.Prompt("pay invoice 4411"), weft.ParkAllExcept("lookup"))
if err != nil {
return
}
fmt.Println("ran:", res.Steps[0].Results[0].Name)
fmt.Println("pending:", res.Pending[0].Name)
// A human decides; the next run resumes under the same rule:
_, _ = agt.Generate(context.Background(),
weft.Messages(res.Messages...), weft.Approve("c2"), weft.ParkAllExcept("lookup"))
}
Output: ran: lookup pending: wire_money
func ParkOn ¶ added in v0.6.0
ParkOn parks a call to any of the named tools at the approval boundary (ADR 0007), exactly as if the tool had been built with RequireApproval: the call gets its ToolStart and no ToolFinish, the run ends successfully with the call on RunResult.Pending, and Approve/Deny/Resolve on a resuming run decide it. This is how a breakpoint or side-effect parking reaches a runtime-started run without touching the agent, which is immutable after New (ADR 0024 D7, WEFT-PLAYGROUND §10.1). With no names, nothing parks. Names are not validated — a name no tool carries parks nothing — and the set covers this run only: a Subagent's child run does not inherit it. ParkAllExcept is the default-deny form, for when the tools to park cannot all be named.
Example ¶
ParkOn parks the named tool's calls at the approval boundary, exactly as RequireApproval would — the breakpoint that reaches a run without touching the immutable agent. Approve resumes it.
package main
import (
"context"
"fmt"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
refund := weft.Tool("refund", "Refund an order.", func(ctx context.Context, in struct{}) (string, error) {
return "refunded", nil
})
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "refund", ID: "c1"}),
wefttest.Say("refunded"),
), refund)
res, err := agt.Generate(context.Background(),
weft.Prompt("refund order 4411"), weft.ParkOn("refund"))
if err != nil {
return
}
fmt.Println("pending:", len(res.Pending))
// A human decides; the next run resumes:
_, _ = agt.Generate(context.Background(),
weft.Messages(res.Messages...), weft.Approve("c1"))
}
Output: pending: 1
func Resolve ¶ added in v0.3.0
Resolve resumes a pending call with a result computed outside the process — the human-as-tool-executor shape: run the query in prod, paste what happened, and the next model call sees it. The content becomes the call's ToolResultPart verbatim; the handler never runs, no execute_tool span or ToolStart/ToolFinish is emitted (nothing executed), and Call.Approved is never set. MaxResultBytes applies as to any result. Composes with Approve and Deny in one resuming call; the last option for an id wins.
Resolve on a call that is not pending in the resumed transcript is a loud run error at step 0 — a deliberate asymmetry with Approve and Deny, which ignore unknown ids: those are yes/no marks over an id set, while Resolve carries a payload the caller expects the model to see, and dropping it silently is the one thing the error model forbids (ADR 0007's 2026-09-22 amendment).
func ResolveError ¶ added in v0.3.0
ResolveError is Resolve with the result marked as an error: the model sees the content on an error result, the shape a failed execution would have produced.
func RunID ¶
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.
func Steering ¶ added in v0.4.0
Steering installs a steering source for this run: a pull hook the loop drains at two fixed points — after a step's tool batch, once every call of the batch has its result, and at a final step, where a delivered message redirects the run into one more step. Delivered messages are appended to the transcript before the next step's PrepareStep chain runs, so request rewrites see them, and reported as a Steered event between that step's StepFinish and the next StepStart.
The hook is never drained when the run ends at the approval boundary or through a StopWhen condition: those ends stay ends, and the source keeps its messages for a follow-up. A redirect at a final point consumes a step and goes through the same continuation checks as any continuation (MaxSteps, UsageLimit, DetectLoops); if they fail, the steer is in RunError.Result.Messages, delivered but unanswered.
It is a run option on purpose (ADR 0019): the run it steers is the one its queue belongs to, and a child run started by a Subagent tool does not inherit it — forwarding a steer to a child is a session decision made explicitly. A delivered message with any role other than RoleUser fails the run with ErrInvalidSteer: the model's own turns come from the model.
Example ¶
Steering delivers a user's message to a running turn at a safe point: after the tool batch (every call paired with its result), or at what would have been the final step, which the steer redirects into one more step. The delivered message is ordinary transcript — the model, the record, and the next turn all see it — reported as a Steered event between StepFinish and the next StepStart (ADR 0019).
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
lookup := weft.Tool("lookup", "Look up an order.",
func(_ context.Context, _ struct{}) (string, error) { return "shipped yesterday", nil })
src := wefttest.NewSteers().At(0, weft.User("That is order 1234 — I meant 5678."))
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "lookup"}),
wefttest.Say("Order 5678 is still pending."),
), lookup)
res, err := agt.Generate(context.Background(), weft.Prompt("Where is my order?"), src.Option())
if err != nil {
log.Fatal(err)
}
fmt.Println(res.NumSteps(), "steps")
fmt.Println(res.Text())
last := res.Messages[len(res.Messages)-2] // the steer, an ordinary user message
fmt.Println(last.Role, last.Text())
}
Output: 2 steps Order 5678 is still pending. user That is order 1234 — I meant 5678.
func UseModel ¶ added in v0.6.0
UseModel replaces the agent's model for this run, rebuilding the WrapModel chain over it (first registered = outermost, the New rule): the run's model calls go through the same middleware over m. Pass a model the runtime registered as an allowed alternate; a nil model is ignored. RunStart.Model and the chat spans report the run's model (middleware forwards Info); Agent.Model and the manifest keep naming the agent's own (WEFT-PLAYGROUND §10.1 [D5]).
type RunResult ¶
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 GenerateAs ¶ added in v0.2.0
GenerateAs runs the agent and returns its structured output, decoded from the last valid submit_output call. The agent must have been built with Output[Out]; a run that ends without a valid submission returns ErrNoOutput alongside the result, so the transcript is still inspectable. Run errors are *RunError as for Generate.
Example ¶
Structured output: Output constrains the final answer to a struct, and GenerateAs returns it decoded. An invalid submission is an ordinary tool error the model repairs; a valid one ends the run.
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
type Verdict struct {
Approved bool `json:"approved"`
Reason string `json:"reason" jsonschema:"one sentence"`
}
model := wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "submit_output", Args: `{"approved":true,"reason":"within policy"}`}),
)
agt := weft.New(model, weft.Instructions("Review refund requests."), weft.Output[Verdict]())
v, res, err := weft.GenerateAs[Verdict](context.Background(), agt, weft.Prompt("Refund order 42?"))
if err != nil {
log.Fatal(err)
}
fmt.Println(v.Approved, v.Reason)
fmt.Println("steps:", res.NumSteps())
}
Output: true within policy steps: 1
type RunStart ¶
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.
InstructionsHash is the lowercase hex sha256 of the run's raw configured instructions — the agent's Instructions, or the run's override, before PrepareStep and before PromptSnippets are composed in (ADR 0028 §4). The loop always sets it: a run with no instructions carries the hash of the empty string. It is a hash, not content, so it survives StripContent; an event built elsewhere may leave it empty, and it is then absent on the wire.
type Schema ¶
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.
func ParseSchema ¶ added in v0.2.0
func ParseSchema(b json.RawMessage) (*Schema, error)
ParseSchema reads a JSON Schema document from outside Go into a *Schema: the structured fields it knows are populated (Type, Properties, Required, … — the manifest and any reader that walks the tree see them), and the document's own bytes are kept and re-emitted by Schema.MarshalJSON, so an enum or a oneOf the Schema type cannot express still reaches the model exactly as written. The top-level type must be an object — providers and MCP both require it, and failing here, at import, beats failing at the first model call.
The structured view is lenient: a keyword whose shape the Schema type cannot hold — a boolean additionalProperties, a type array such as ["string","null"], tuple or boolean items, a non-string description — leaves that field zero (an unconstrained node) and is not an error, because the bytes carry it whole and the view is for readers, not the model. Only the document itself is checked: invalid JSON, trailing data, or a non-object top level return an error naming the problem.
type SteerFunc ¶ added in v0.4.0
SteerFunc returns the messages to deliver at a safe point, or nil. It must not block: drain a queue, do not wait on one — the loop calls it between steps, so a source that waits stalls the run. A source with nothing to deliver returns nil. A panic in a SteerFunc fails the run like a panic in a PrepareStep function (the deferred recover ends the run span and re-panics): it is arbitrary user code the caller owns.
The returned messages become ordinary transcript messages — the transcript, the record, and the next turn all see them — and are reported as a Steered event. Ownership passes to the run, like the messages given to Messages: the source must not reuse or mutate them.
type SteerPoint ¶ added in v0.4.0
type SteerPoint = core.SteerPoint
SteerPoint tells a steering source where the run is. RunID is the run being steered; Step is the step that just finished; Final is true when the run would otherwise end here, so a non-empty return redirects the run into one more step instead of ending it.
type Steered ¶ added in v0.4.0
Steered reports messages the run's steering source delivered at the step's drain point — after the tool batch, or at a final step whose steer redirected the run into one more step. It sits between that step's StepFinish and the next StepStart, numbered from the run's Seq counter like every Seq-carrying event. Messages are the delivered values (RoleUser) as a snapshot: they do not alias the run's transcript (ADR 0019).
type StepFinish ¶
type StepFinish = core.StepFinish
StepFinish reports that step Index is complete: the model call finished and, when the step requested tools, they have run — it follows the step's ToolFinish events and precedes the stop-condition check (docs/life-of-a-call.md). Raw is the provider's own stop reason when Reason was approximated (see ModelFinish.Raw); empty when the mapping was exact.
LatencyMS and TTFTMS are the step's model call timed by the loop as it consumes the stream (ADR 0016's 2026-10-07 A4 note), in whole milliseconds rounded up, so a measured interval is never 0 and 0 (absent on the wire) means not measured — an event recorded before the fields existed, or one built by hand. LatencyMS runs from the call's start (the chain's Stream) to the stream's end (the ModelFinish); TTFTMS from the same start to the first TextDelta or ToolArgsDelta — reasoning does not count — and is 0 when the call yielded neither (a non-streaming adapter, a script's bare tool call). The timing spans the whole model chain: a retry's backoff and a fallback's failed tries are inside it. Both are observations, not behaviour: nothing in the loop reads them.
type StepRecord ¶
type StepRecord = core.StepRecord
StepRecord captures everything one model step produced: its text, the tool calls it requested, and the results of executing them in call order.
type StopCondition ¶
type StopCondition = core.StopCondition
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 ¶
StopFunc adapts an ordinary function to a StopCondition, the http.HandlerFunc shape.
type ThinkingConfig ¶ added in v0.2.0
type ThinkingConfig = core.ThinkingConfig
ThinkingConfig is the per-run reasoning request: a level on the neutral scale, plus a token budget for providers whose depth control is a cap (Anthropic budget_tokens, Gemini thinkingBudget). A level without a Budget leaves the depth to the provider (Anthropic adaptive thinking, Gemini's own level mapping); a Budget without a level pins it. Off wins over a Budget when both are set.
type ThinkingLevel ¶ added in v0.2.0
type ThinkingLevel = core.ThinkingLevel
ThinkingLevel is a provider-neutral reasoning-effort scale. The zero value, ThinkUnset, sends nothing and keeps the provider default; every other value asks the adapter to express that depth on the wire in whatever form the provider has — reasoning_effort, a thinking object, a token budget. Adapters map what the provider can express and document what they drop (TODO §5.14).
type ThinkingOption ¶ added in v0.2.0
type ThinkingOption = core.ThinkingOption
ThinkingOption is accepted by both New and Stream/Generate: reasoning depth is a per-question concern, not a per-agent one. On an agent it is the default for every run; on a run it overrides that default.
func Thinking ¶ added in v0.2.0
func Thinking(cfg ThinkingConfig) ThinkingOption
Thinking sets the reasoning level for the agent's model calls. As an Option it is every run's default; as a RunOption it overrides that default for one run — the quick-ask shape: fast by default, think on demand, without rebuilding the agent.
agt := core.New(m, core.Thinking(core.ThinkingConfig{Level: core.ThinkOff}))
agt.Generate(ctx, core.Thinking(core.ThinkingConfig{Level: core.ThinkHigh}), core.Prompt(q))
The zero Level keeps the provider default; adapters map what the provider can express and document what they drop.
Example ¶
Fast by default, think on demand: the agent option sets every run's default, a run option overrides it for that run alone. The scripted model records what each run asked for.
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
model := wefttest.Script(wefttest.Say("ok"), wefttest.Say("ok"))
agt := weft.New(model, weft.Thinking(weft.ThinkingConfig{Level: weft.ThinkOff}))
deep := weft.Thinking(weft.ThinkingConfig{Level: weft.ThinkHigh, Budget: 2048})
if _, err := agt.Generate(context.Background(), deep, weft.Prompt("hard")); err != nil {
log.Fatal(err)
}
if _, err := agt.Generate(context.Background(), weft.Prompt("quick")); err != nil {
log.Fatal(err)
}
for _, req := range model.Requests() {
fmt.Println(req.Thinking == weft.ThinkingConfig{Level: weft.ThinkHigh, Budget: 2048},
req.Thinking.Level == weft.ThinkOff)
}
}
Output: true false false true
type ToolArgsDelta ¶ added in v0.2.0
type ToolArgsDelta = core.ToolArgsDelta
ToolArgsDelta reports an increment of a tool call's arguments as the model streams them — the model is "writing" the call, which can take a while for large arguments (generated code, long documents). It is progress only: the call has not been made, and ToolStart still arrives when it executes. Name is the best-known name so far; a provider that streams fragments of several calls interleaves their deltas, distinguished by name where the provider supplies one.
type ToolCallPart ¶
type ToolCallPart = core.ToolCallPart
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.
type ToolCaller ¶ added in v0.2.0
type ToolCaller = core.ToolCaller
ToolCaller is one link of the tool-call chain: it takes a call and returns the result text the model will see, or an error. The innermost caller decodes the arguments and runs the handler; every ToolMiddleware wraps one.
type ToolChoiceConfig ¶ added in v0.3.0
type ToolChoiceConfig = core.ToolChoiceConfig
ToolChoiceConfig constrains what a step's model call may emit. The zero value is the provider default. Name is required when Mode is ToolChoiceNamed and must be empty under every other mode; the loop fails the run on a mismatch rather than sending a malformed choice (a programming error, not a sentinel condition).
type ToolChoiceMode ¶ added in v0.3.0
type ToolChoiceMode = core.ToolChoiceMode
ToolChoiceMode selects how the provider must shape a step's tool calls. The zero value, ToolChoiceAuto, keeps the provider default and sends nothing; every other value asks the adapter to express the constraint in the provider's own tool_choice form.
type ToolChoiceOption ¶ added in v0.3.0
type ToolChoiceOption = core.ToolChoiceOption
ToolChoiceOption is accepted by both New and Stream/Generate: which tool calls a step must make is a per-question concern as much as a per-agent one (the ThinkingOption shape).
func ToolChoice ¶ added in v0.3.0
func ToolChoice(cfg ToolChoiceConfig) ToolChoiceOption
ToolChoice forces the agent's model calls to include (or forbear from) tool calls. As an Option it is every run's default; as a RunOption it overrides that default for one run:
// a router: the first step must call classify
agt := core.New(m, classify, core.ToolChoice(core.ToolChoiceConfig{Mode: core.ToolChoiceNamed, Name: "classify"}))
// a final step that must answer in text, cache prefix intact
agt.Generate(ctx, core.ToolChoice(core.ToolChoiceConfig{Mode: core.ToolChoiceNone}), core.Prompt(q))
Modes: Any (some tool must be called), Named (Name must be — the router and eval-harness shape), None (no call may be made; the catalogue stays advertised, so a prompt-cache prefix on the tool definitions survives), Auto (the zero value: provider default, nothing sent). A PrepareStep function can rewrite the request's ToolChoice per step — force classify on step 0, then auto — the same way it rewrites tools. A non-auto choice the step's tool snapshot cannot satisfy (no tools, a name not advertised, a name under another mode) fails the run with a descriptive error.
Example ¶
ToolChoice forces a step's tool calls — the router shape: classify must be the first call, the rest of the run is unconstrained. One PrepareStep function rewrites the request's ToolChoice per step; the agent-level option would force every step instead.
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
classify := weft.Tool("classify", "Classify the request.",
func(_ context.Context, _ struct{}) (string, error) { return "billing", nil })
agt := weft.New(
wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "classify"}),
wefttest.Say("This is a billing question."),
),
weft.PrepareStep(func(_ context.Context, step int, req weft.ModelRequest) (weft.ModelRequest, error) {
if step == 0 {
req.ToolChoice = weft.ToolChoiceConfig{Mode: weft.ToolChoiceNamed, Name: "classify"}
}
return req, nil
}),
classify,
)
res, err := agt.Generate(context.Background(), weft.Prompt("Why did my invoice double?"))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Text())
}
Output: This is a billing question.
type ToolDef ¶
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.
A ToolDef is mutable until it is registered with New and frozen thereafter: New keeps a deep copy, so mutating the value that was passed in — or a copy returned by Agent.Tools — never reaches the agent or a running run.
func RawTool ¶
func RawTool(name, description string, schema *Schema, fn func(ctx context.Context, args json.RawMessage) (string, error), opts ...ToolOption) *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, and StrictInput has no effect. 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). Trailing options set per-tool policy as for Tool.
func Subagent ¶ added in v0.2.0
func Subagent(name, description string, child *Agent, opts ...ToolOption) *ToolDef
Subagent defines a tool that delegates to another agent. The model calls it with one argument, prompt; the child runs on a fresh transcript holding only that prompt, on the parent call's context, and the tool's result is the child's final text (or, for a child built with Output, the submitted JSON). The child's events arrive in the parent's stream wrapped in Nested; its usage is added to the parent's RunResult.Usage and recorded on StepRecord.SubagentUsage.
A subagent is an ordinary tool: Timeout bounds the child run, MaxResultBytes caps its answer, RequireApproval gates the delegation, Sequential makes it a barrier, WrapTools wraps the delegation once (the child's own seams govern inside it), and the manifest lists it.
A child run that fails is a tool error the parent model sees (SUBAGENT_FAILED), never a parent run error; a child that ends awaiting approval is SUBAGENT_PENDING; a child that is already running above this call is refused with SUBAGENT_CYCLE. Subagent panics if child is nil.
Example ¶
Delegating to another agent: a subagent is a tool whose handler runs another agent on the prompt alone. The child's events arrive wrapped in Nested; its usage rolls into the parent's total.
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
researcher := weft.New(wefttest.Script(
wefttest.Say("order 1234 shipped yesterday"),
))
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "research", Args: `{"prompt":"where is order 1234?"}`}),
wefttest.Say("Researched."),
), weft.Subagent("research", "Research a question in depth.", researcher))
res, err := agt.Generate(context.Background(), weft.Prompt("Where is order 1234?"))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Steps[0].Results[0].Content)
fmt.Println("total tokens:", res.Usage.Total())
}
Output: order 1234 shipped yesterday total tokens: 45
func Tool ¶
func Tool[In, Out any](name, description string, fn func(ctx context.Context, in In) (Out, error), opts ...ToolOption) *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"`
}
core.Tool("refund_order", "Refund a customer's order",
func(ctx context.Context, in RefundInput) (Receipt, error) {
return billing.Refund(ctx, in.OrderID, in.Reason)
},
core.Timeout(10*time.Second))
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. Trailing options set per-tool policy: Timeout, MaxResultBytes, StrictInput.
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" ] }
type ToolError ¶ added in v0.2.0
ToolError is a tool failure with a stable code the model can branch on. Code is SCREAMING_SNAKE by convention ("ORDER_NOT_FOUND"); Message is what the model reads; Err is the internal cause — available to tool middleware and audit logs through errors.As/Unwrap, and never shown to the model. A handler returning *ToolError produces the result "<CODE>: <Message>"; the loop renders its own failures with codes too: INVALID_INPUT (arguments that do not decode) and NO_SUCH_TOOL. Codes are not validated. Plain errors keep rendering as err.Error(); install mw.MapErrors to code them centrally.
Example ¶
A *ToolError carries a code the model can branch on and a cause it never sees.
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
lookup := weft.Tool("lookup_order", "", func(_ context.Context, in struct {
ID string `json:"id"`
}) (string, error) {
return "", &weft.ToolError{
Code: "ORDER_NOT_FOUND",
Message: "order " + in.ID + " does not exist",
Err: errors.New("pg: no rows in result set"), // for logs and middleware only
}
})
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "lookup_order", Args: `{"id":"42"}`}),
wefttest.ToolCalls(wefttest.Call{Name: "lookup_order", Args: `{"id":42}`}),
wefttest.Say("No such order."),
), lookup)
res, err := agt.Generate(context.Background(), weft.Prompt("order 42?"))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Steps[0].Results[0].Content)
fmt.Println(res.Steps[1].Results[0].Content)
}
Output: ORDER_NOT_FOUND: order 42 does not exist INVALID_INPUT: tool "lookup_order": field "id": expected string, got number
type ToolFinish ¶
type ToolFinish = core.ToolFinish
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.
type ToolMiddleware ¶ added in v0.2.0
type ToolMiddleware = core.ToolMiddleware
ToolMiddleware wraps a ToolCaller, the chi shape: it may act before next (deny, decorate ctx, log), after it (map errors, audit), or instead of it. Panic containment stays outside the chain — a panic in middleware is still a tool error result, never a run error.
type ToolOption ¶ added in v0.2.0
type ToolOption = core.ToolOption
ToolOption configures one tool at definition time. The options that make sense at both levels — MaxResultBytes, Timeout, StrictInput — are PolicyOptions: on the agent they set the default for every tool, on a tool they override that default for the tool alone.
func Origin ¶ added in v0.10.0
func Origin(name string) ToolOption
Origin names where a tool came from, for the observability record only: it is the tools record's source (ADR 0028 §5), and changes nothing the model sees or the loop does. A tool is "local" by default; Subagent sets "subagent" and weft/mcp's Tools sets "mcp". Any other string is recorded verbatim; the last Origin wins.
func PromptSnippet ¶ added in v0.2.0
func PromptSnippet(text string) ToolOption
PromptSnippet attaches system-prompt lines to a tool. The loop appends the snippets of the tools it advertises to the agent's Instructions for every model call — one paragraph per tool, in registration order, separated by blank lines — so usage rules live with the tool instead of in a central prompt that drifts as the set grows. Empty snippets add nothing. The manifest records the snippet.
Example ¶
PromptSnippet keeps a tool's usage rules next to the tool; the loop appends them to the instructions of every model call.
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
search := weft.Tool("search", "Search the docs.", func(_ context.Context, _ struct{}) (string, error) { return "", nil },
weft.PromptSnippet("Cite the search result you used."))
model := wefttest.Script(wefttest.Say("ok"))
if _, err := weft.New(model, weft.Instructions("You answer questions."), search).Generate(context.Background(), weft.Prompt("hi")); err != nil {
log.Fatal(err)
}
fmt.Println(model.Requests()[0].System)
}
Output: You answer questions. Cite the search result you used.
func Replay ¶ added in v0.7.0
func Replay(p ReplayPolicy) ToolOption
Replay sets the tool's ReplayPolicy. Only ReplaySafe needs to be said: ReplayNever is the zero value and the default, and any other value counts as never rather than being trusted (an unknown class is never's, the safe default). Like every tool option the last one wins, so Replay(ReplayNever) after an earlier Replay(ReplaySafe) — shared defaults, then this tool's own word — is never's. The manifest records the class.
Example ¶
Replay declares a tool's side-effect class for re-runs: safe vouches the call is idempotent (a re-run may execute it for real); every unannotated tool counts as never — substituted or parked, never silently re-fired (WEFT-PLAYGROUND.md §6 rule 3).
package main
import (
"context"
"fmt"
"github.com/weftgo/weft"
)
func main() {
lookup := weft.Tool("lookup_order", "Look up an order.", func(_ context.Context, _ struct{}) (string, error) {
return "shipped", nil
}, weft.Replay(weft.ReplaySafe))
refund := weft.Tool("refund", "Refund an order.", func(_ context.Context, _ struct{}) (string, error) {
return "refunded", nil
})
fmt.Println("lookup:", lookup.ReplayPolicy())
fmt.Println("refund:", refund.ReplayPolicy())
}
Output: lookup: safe refund: never
func RequireApproval ¶ added in v0.2.0
func RequireApproval() ToolOption
RequireApproval marks a tool whose calls never run without a decision. When the model calls it the loop executes the step's other tools, then ends the run successfully with the call on RunResult.Pending (and RunFinish.Pending) and no result in the transcript. Resume with the transcript plus Approve or Deny:
res, _ := agt.Generate(ctx, core.Prompt("Refund order 42"))
for _, call := range res.Pending { /* ask someone */ }
res, _ = agt.Generate(ctx, core.Messages(res.Messages...), core.Approve(call.ID))
Approved calls run through the ordinary chain with Call.Approved set; denied ones (and pending calls given no decision) become error results the model sees. This is a policy and UX seam, not a security boundary: the boundary is the sandbox a tool runs in.
Example ¶
A RequireApproval tool parks its calls: the run ends successfully with them on Pending, and a later run resumes with a decision.
package main
import (
"context"
"fmt"
"log"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
refund := weft.Tool("refund", "Refund an order.", func(_ context.Context, in struct {
Order string `json:"order"`
}) (string, error) {
return "refunded " + in.Order, nil
}, weft.RequireApproval())
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{ID: "c1", Name: "refund", Args: `{"order":"42"}`}),
wefttest.Say("Done."),
), refund)
res, err := agt.Generate(context.Background(), weft.Prompt("refund order 42"))
if err != nil {
log.Fatal(err)
}
for _, call := range res.Pending {
fmt.Printf("awaiting approval: %s %s\n", call.Name, call.Args)
}
// Someone decided. Resume with the transcript and the decision.
res, err = agt.Generate(context.Background(), weft.Messages(res.Messages...), weft.Approve("c1"))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Text())
}
Output: awaiting approval: refund {"order":"42"} Done.
func ToolOptions ¶ added in v0.2.0
func ToolOptions(opts ...ToolOption) ToolOption
ToolOptions composes several tool options into one, applied in order — the Tool-defining counterpart of Options, so a package of policy (Timeout, MaxResultBytes, StrictInput, a WrapTools chain) can be named and reused:
productPolicy := core.ToolOptions(core.Timeout(5*time.Second), core.StrictInput())
core.Tool("lookup", "…", fn, productPolicy)
Nil entries are ignored.
Example ¶
ToolOptions composes tool options into one named value, so a package of per-tool policy travels under one name.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/weftgo/weft"
"github.com/weftgo/weft/wefttest"
)
func main() {
productPolicy := weft.ToolOptions(
weft.Timeout(5*time.Second),
weft.MaxResultBytes(1024),
)
agt := weft.New(wefttest.Script(
wefttest.ToolCalls(wefttest.Call{Name: "lookup_order", Args: `{"id":"42"}`}),
wefttest.Say("Done."),
), weft.Tool("lookup_order", "Look up an order by id.",
func(_ context.Context, in struct {
ID string `json:"id" jsonschema:"the order id"`
}) (string, error) {
return "order " + in.ID + " shipped", nil
}, productPolicy))
res, err := agt.Generate(context.Background(), weft.Prompt("Where is order 42?"))
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Steps[0].Results[0].Content)
}
Output: order 42 shipped
type ToolResultPart ¶
type ToolResultPart = core.ToolResultPart
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.
type ToolStart ¶
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. A call parked by the approval boundary has a ToolStart and no ToolFinish; it is listed on RunFinish.Pending instead.
type Usage ¶
Usage is token accounting for one step or one whole run.
The totals are inclusive: CachedInputTokens and CacheWriteTokens are subsets of InputTokens (tokens billed as input — read from or written to a provider prompt cache), ReasoningTokens is a subset of OutputTokens (provider-side reasoning the model burned). The splits are reporting, not budget bases: UsageLimit and Total keep reading the two totals, so a cached-heavy run budgets identically to an uncached one with the same totals. All three are omitempty on the wire — an event from before they existed round-trips unchanged (SchemaVersion stays 1, ADR 0001/0004).
Directories
¶
| Path | Synopsis |
|---|---|
|
Package anthropic is the weft adapter for the Anthropic Messages API, wrapping the official anthropic-sdk-go.
|
Package anthropic is the weft adapter for the Anthropic Messages API, wrapping the official anthropic-sdk-go. |
|
example
command
Command example runs a two-step agent conversation against the real Anthropic API.
|
Command example runs a two-step agent conversation against the real Anthropic API. |
|
cmd
|
|
|
weft
command
Command weft is the framework's one binary (plan B1): setup B's local Studio and a terminal over the Studio API, for scripts and CI.
|
Command weft is the framework's one binary (plan B1): setup B's local Studio and a terminal over the Studio API, for scripts and CI. |
|
core
module
|
|
|
examples
|
|
|
approval
command
Command approval shows the approval boundary end to end, offline: a tool marked RequireApproval parks its call, the run ends with the call on Pending, a person decides, and a second run resumes with the transcript plus the decision.
|
Command approval shows the approval boundary end to end, offline: a tool marked RequireApproval parks its call, the run ends with the call on Pending, a person decides, and a second run resumes with the transcript plus the decision. |
|
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. |
|
Package google is the weft adapter for the Gemini API via the official google.golang.org/genai SDK.
|
Package google is the weft adapter for the Gemini API via the official google.golang.org/genai SDK. |
|
example
command
Command example runs a two-step agent conversation against the real Gemini API.
|
Command example runs a two-step agent conversation against the real Gemini API. |
|
internal
|
|
|
adapterkit
Package adapterkit holds the helpers every first-party adapter needs but no vendor SDK touches: schema rendering, the terminal-error rule (with the context-overflow mapping and the marker table mw's retry classifier reads), and the FilePart exactly-one guard.
|
Package adapterkit holds the helpers every first-party adapter needs but no vendor SDK touches: schema rendering, the terminal-error rule (with the context-overflow mapping and the marker table mw's retry classifier reads), and the FilePart exactly-one guard. |
|
cmd/genfacade
command
Command genfacade writes a facade package (internal/facadegen): the framework's github.com/weftgo/weft, weft/mw, weft/wefttest and weft/wefttest/conformance re-export the core module's packages of the same name.
|
Command genfacade writes a facade package (internal/facadegen): the framework's github.com/weftgo/weft, weft/mw, weft/wefttest and weft/wefttest/conformance re-export the core module's packages of the same name. |
|
discovery
Package discovery is how an app finds the running Studio with no configuration (plan B3): `weft studio` and `weft dev` write a small file, studio.json, naming the Studio they serve, and weft/otel and weft/runtime read it when nothing else names one.
|
Package discovery is how an app finds the running Studio with no configuration (plan B3): `weft studio` and `weft dev` write a small file, studio.json, naming the Studio they serve, and weft/otel and weft/runtime read it when nothing else names one. |
|
doctor
Package doctor is `weft doctor` (plan B5): it asks a running Studio what it is and how it is wired, and prints one line per check.
|
Package doctor is `weft doctor` (plan B5): it asks a running Studio what it is and how it is wired, and prints one line per check. |
|
facadegen
Package facadegen writes a facade package: one that re-exports another package's whole exported API under a new import path, so github.com/weftgo/weft (the framework) offers the loop that github.com/weftgo/weft/core (the slim module) implements, under the same names.
|
Package facadegen writes a facade package: one that re-exports another package's whole exported API under a new import path, so github.com/weftgo/weft (the framework) offers the loop that github.com/weftgo/weft/core (the slim module) implements, under the same names. |
|
listen
Package listen is the Studio command's port policy (plan B2): one default port, 7331, stable and reusable, never silently different.
|
Package listen is the Studio command's port policy (plan B2): one default port, 7331, stable and reusable, never silently different. |
|
Package mcp is the bridge between weft and the Model Context Protocol, both directions over the official Go SDK (github.com/modelcontextprotocol/go-sdk, aliased `sdk` in examples — this package keeps the name `mcp`):
|
Package mcp is the bridge between weft and the Model Context Protocol, both directions over the official Go SDK (github.com/modelcontextprotocol/go-sdk, aliased `sdk` in examples — this package keeps the name `mcp`): |
|
examples/client
command
Command client consumes MCP servers as weft tools: it connects two in-memory servers concurrently under one deadline, imports their tools with per-server prefixes, registers them on a scripted agent, runs it, and prints the manifest — the imported tools' raw schemas show in it.
|
Command client consumes MCP servers as weft tools: it connects two in-memory servers concurrently under one deadline, imports their tools with per-server prefixes, registers them on a scripted agent, runs it, and prints the manifest — the imported tools' raw schemas show in it. |
|
examples/server
command
Command server exposes a weft agent as an MCP server over stdio — the getting-started agent plus its lookup tool, both directions of §7 in one process:
|
Command server exposes a weft agent as an MCP server over stdio — the getting-started agent plus its lookup tool, both directions of §7 in one process: |
|
Package mw is the framework's import path for the reference middleware that github.com/weftgo/weft/core/mw implements: model middleware for weft.WrapModel (Retry, Fallback, Log, RepairJSON) and tool middleware for weft.WrapTools (Allow, Audit, MapErrors).
|
Package mw is the framework's import path for the reference middleware that github.com/weftgo/weft/core/mw implements: model middleware for weft.WrapModel (Retry, Fallback, Log, RepairJSON) and tool middleware for weft.WrapTools (Allow, Audit, MapErrors). |
|
Package obsdb is the observability database: what weft/otel's local sink writes and Weft Studio reads, in one module so neither owns the schema (ADR 0024, S3).
|
Package obsdb is the observability database: what weft/otel's local sink writes and Weft Studio reads, in one module so neither owns the schema (ADR 0024, S3). |
|
clickhouse
Package clickhouse — see doc.go for the package documentation.
|
Package clickhouse — see doc.go for the package documentation. |
|
internal/reqread
Package reqread holds what obsdb's two backends share to answer DB.Prompt, DB.Tools and DB.Catalogs over the records they read: the lookups by hash and the one-per-hash catalog list.
|
Package reqread holds what obsdb's two backends share to answer DB.Prompt, DB.Tools and DB.Catalogs over the records they read: the lookups by hash and the one-per-hash catalog list. |
|
obsdbtest
Package obsdbtest is the shared conformance table for obsdb.DB backends — the executable form of S3.5's promises, the storetest pattern.
|
Package obsdbtest is the shared conformance table for obsdb.DB backends — the executable form of S3.5's promises, the storetest pattern. |
|
sqlite
Package sqlite is obsdb's default backend: the observability schema in a single SQLite file on the CGO-free modernc.org/sqlite driver (thread/sqlite's choice).
|
Package sqlite is obsdb's default backend: the observability schema in a single SQLite file on the CGO-free modernc.org/sqlite driver (thread/sqlite's choice). |
|
Package openai is the weft adapter for the OpenAI Chat Completions API and OpenAI-compatible servers (gateways, local models), wrapping the official openai-go SDK.
|
Package openai is the weft adapter for the OpenAI Chat Completions API and OpenAI-compatible servers (gateways, local models), wrapping the official openai-go SDK. |
|
example
command
Command example runs a two-step agent conversation against the real OpenAI API (or any OPENAI_BASE_URL server).
|
Command example runs a two-step agent conversation against the real OpenAI API (or any OPENAI_BASE_URL server). |
|
Package otel wires weft's observability to one or more OpenTelemetry destinations at once — the local sink, Weft Studio, Datadog, Langfuse, any OTLP endpoint or your own exporters — each with its own signals and content policy (ADR 0024, S2).
|
Package otel wires weft's observability to one or more OpenTelemetry destinations at once — the local sink, Weft Studio, Datadog, Langfuse, any OTLP endpoint or your own exporters — each with its own signals and content policy (ADR 0024, S2). |
|
Package runtime is the playground's in-app side (WEFT-PLAYGROUND.md §10.2, [D6]): it registers the agents your code built with the Studio the app is already observed by, receives experiment commands over the runtime link, and executes them as real runs of those agents — your tools, your model keys, your process.
|
Package runtime is the playground's in-app side (WEFT-PLAYGROUND.md §10.2, [D6]): it registers the agents your code built with the Studio the app is already observed by, receives experiment commands over the runtime link, and executes them as real runs of those agents — your tools, your model keys, your process. |
|
examples/local
command
Command local is setup A's playground in one process: an embedded Studio on loopback, the app's own pipeline exporting into it, one agent with a scripted model and two tools, and the runtime link — WEFT-PLAYGROUND.md §7's P0 slice, drivable with curl.
|
Command local is setup A's playground in one process: an embedded Studio on loopback, the app's own pipeline exporting into it, one agent with a scripted model and two tools, and the runtime link — WEFT-PLAYGROUND.md §7's P0 slice, drivable with curl. |
|
Package scope is the Go side of the devtools' Scope (plan §13.3): the unit every discovery rung, deep link and API call carries — one conversation's public id and, optionally, the session, flow and run inside it — and the net/http middleware that hands it to the page.
|
Package scope is the Go side of the devtools' Scope (plan §13.3): the unit every discovery rung, deep link and API call carries — one conversation's public id and, optionally, the session, flow and run inside it — and the net/http middleware that hands it to the page. |
|
store
module
|
|
|
Package studio is the Inspector: the UI, the JSON API, the live stream and the OTLP receiver over one observability database (weft/obsdb), served by Go alone.
|
Package studio is the Inspector: the UI, the JSON API, the live stream and the OTLP receiver over one observability database (weft/obsdb), served by Go alone. |
|
examples/basic
command
Command basic records demo runs — a tool call, a subagent, a failure — into an obsdb sqlite database and serves Studio on 127.0.0.1:7331:
|
Command basic records demo runs — a tool call, a subagent, a failure — into an obsdb sqlite database and serves Studio on 127.0.0.1:7331: |
|
ingest
Package ingest is Studio's OTLP/HTTP receiver (S4.4): POST /v1/traces and /v1/logs, protobuf and JSON, optional gzip, a 16 MiB limit on the decompressed body, and the publish-then-write pipeline —
|
Package ingest is Studio's OTLP/HTTP receiver (S4.4): POST /v1/traces and /v1/logs, protobuf and JSON, optional gzip, a 16 MiB limit on the decompressed body, and the publish-then-write pipeline — |
|
runtime
Package runtime is the runtime link's server side (WEFT-PLAYGROUND §10.3, S4.2): the registry of connected runtimes and the three routes a weft/runtime client speaks to — register, the SSE command stream, acks.
|
Package runtime is the runtime link's server side (WEFT-PLAYGROUND §10.3, S4.2): the registry of connected runtimes and the three routes a weft/runtime client speaks to — register, the SSE command stream, acks. |
|
cmd
module
|
|
|
Package thread gives weft sessions: a conversation as an append-only tree of entries, durable through a Storage backend, with turns, branching, compaction, approvals and delegation all built on that one tree.
|
Package thread gives weft sessions: a conversation as an append-only tree of entries, durable through a Storage backend, with turns, branching, compaction, approvals and delegation all built on that one tree. |
|
backend
Package backend is for authors of thread.Storage backends: it resolves the open options an application passes — thread.Salvage, thread.FsyncOnFlush, thread.NoLock, thread.OpenLogger — into the configuration a backend acts on.
|
Package backend is for authors of thread.Storage backends: it resolves the open options an application passes — thread.Salvage, thread.FsyncOnFlush, thread.NoLock, thread.OpenLogger — into the configuration a backend acts on. |
|
examples/approvals
command
Command approvals walks one weft/thread session through the approval flow (ADR 0021): a gated call parks, the process "restarts" — the session is reopened from the JSONL file — a decision arrives signed over the challenge the session minted, and the conversation resumes under it.
|
Command approvals walks one weft/thread session through the approval flow (ADR 0021): a gated call parks, the process "restarts" — the session is reopened from the JSONL file — a decision arrives signed over the challenge the session minted, and the conversation resumes under it. |
|
examples/refund-plan
command
Command refund-plan demonstrates the orchestration ledger on a weft/thread session: a refund-support chat whose policy state lives in the session file as custom entries, so a process that dies mid-flow is replaced by one that resumes exactly where the file says — the last refund_plan entry.
|
Command refund-plan demonstrates the orchestration ledger on a weft/thread session: a refund-support chat whose policy state lives in the session file as custom entries, so a process that dies mid-flow is replaced by one that resumes exactly where the file says — the last refund_plan entry. |
|
examples/session
command
Command session walks one weft/thread session through its whole life: two turns, a label, a branch off the first answer, a fork of the branch, a manual compaction with preview, a close, and a reopen from disk — everything on a JSONL backend, everything offline through a scripted model, every id deterministic.
|
Command session walks one weft/thread session through its whole life: two turns, a label, a branch off the first answer, a fork of the branch, a manual compaction with preview, a close, and a reopen from disk — everything on a JSONL backend, everything offline through a scripted model, every id deterministic. |
|
internal/carry
Package carry builds the context a run continues on when it runs on behalf of another one — a resume over a parked boundary, a deferred steer's follow-up, an async pool child: cancellation and deadline from one context, and, for the keys that context does not carry, the values of the context the work began on.
|
Package carry builds the context a run continues on when it runs on behalf of another one — a resume over a parked boundary, a deferred steer's follow-up, an async pool child: cancellation and deadline from one context, and, for the keys that context does not carry, the values of the context the work began on. |
|
internal/opencfg
Package opencfg holds the resolved form of thread's open options — the one piece of the option vocabulary that thread (which declares the options), its in-tree backends (Memory) and thread/backend (which publishes the resolution to backend authors) all need, kept here so that none of them has to import another to share it.
|
Package opencfg holds the resolved form of thread's open options — the one piece of the option vocabulary that thread (which declares the options), its in-tree backends (Memory) and thread/backend (which publishes the resolution to backend authors) all need, kept here so that none of them has to import another to share it. |
|
internal/rules
Package rules holds the small rules every thread.Storage backend must answer identically: the List limit, the metadata and title filters, the paging cursor, and the line discipline of a session's bytes.
|
Package rules holds the small rules every thread.Storage backend must answer identically: the List limit, the metadata and title filters, the paging cursor, and the line discipline of a session's bytes. |
|
jsonl
Package jsonl is the default durable thread.Storage: one directory, one <id>.jsonl file per session — the header line first, then one entry per line in append order (ADR 0011).
|
Package jsonl is the default durable thread.Storage: one directory, one <id>.jsonl file per session — the header line first, then one entry per line in append order (ADR 0011). |
|
pool
Package pool provides bounded concurrent child runs for thread sessions (ADR 0022): a FIFO semaphore per Pool value, subagent tools whose children run as sessions of their own, linked to the parent session and call, and receipts recording every delegation's journey.
|
Package pool provides bounded concurrent child runs for thread sessions (ADR 0022): a FIFO semaphore per Pool value, subagent tools whose children run as sessions of their own, linked to the parent session and call, and receipts recording every delegation's journey. |
|
sqlite
Package sqlite is the thread's second durable backend: every session in one SQLite file on the CGO-free modernc.org/sqlite driver — the store's choice (store/sqlite, Crush's before it), reused so one dependency serves both modules — with WAL and embedded migrations.
|
Package sqlite is the thread's second durable backend: every session in one SQLite file on the CGO-free modernc.org/sqlite driver — the store's choice (store/sqlite, Crush's before it), reused so one dependency serves both modules — with WAL and embedded migrations. |
|
threadtest
Package threadtest is the shared conformance table for thread.Storage backends — the executable form of ADR 0011 §5's promises.
|
Package threadtest is the shared conformance table for thread.Storage backends — the executable form of ADR 0011 §5's promises. |
|
Package version is weft's one version string: the framework module's release tag, stamped into Studio's api/meta, the devtools panel bundle, the otel resource, the runtime's registration and the binaries' version output.
|
Package version is weft's one version string: the framework module's release tag, stamped into Studio's api/meta, the devtools panel bundle, the otel resource, the runtime's registration and the binaries' version output. |
|
Package wefttest is the framework's import path for the offline test double that github.com/weftgo/weft/core/wefttest implements: a scripted, deterministic Model (Script, Say, ToolCalls, Fail, …), record-and-replay against a real provider, and helpers for events and golden files.
|
Package wefttest is the framework's import path for the offline test double that github.com/weftgo/weft/core/wefttest implements: a scripted, deterministic Model (Script, Say, ToolCalls, Fail, …), record-and-replay against a real provider, and helpers for events and golden files. |
|
conformance
Package conformance is the framework's import path for the adapter conformance suite that github.com/weftgo/weft/core/wefttest/conformance implements: Run drives a Model through the behaviours every provider adapter must share.
|
Package conformance is the framework's import path for the adapter conformance suite that github.com/weftgo/weft/core/wefttest/conformance implements: Run drives a Model through the behaviours every provider adapter must share. |