Documentation
¶
Overview ¶
Package agentkit assembles the nine libraries into an agentturn.Config.
It owns three compositions no single library can own without importing its siblings: the order of the instruction parts, the order of the hooks that contest one agentturn.Config field, and the union of the tool sets. Everything else it hands the loop came from a library's exported constructor.
The rule the package holds itself to is that Kit.Config returns a plain agentturn.Config whose every field a product could have set by hand, with the same values, by calling the same exported functions. There are no private seams, no wrapper types a caller cannot construct, and no behaviour that exists only when the kit assembled it. For each field the kit sets, docs/manual.md names the call a product would write instead.
kit, err := agentkit.New(ctx,
agentkit.WithModel(model, "gpt-5"),
agentkit.WithInstructions("Be brief."),
agentkit.WithAgentsMD(cwd, agentsmd.Options{Root: repoRoot}),
agentkit.WithSkills(".dex/skills"),
agentkit.WithMemory(store, "user", "project"),
agentkit.WithPolicy(policy, matchers),
agentkit.WithTools(read, write, edit, bash),
agentkit.WithMCP("some-server --stdio"),
agentkit.WithSession(sessions, agentsession.Header{CWD: cwd}),
agentkit.WithCompaction(60_000),
)
if err != nil {
return err
}
defer kit.Close()
for _, o := range kit.Omitted() {
log.Printf("not given to the model: %s (%s)", o.What, o.Reason)
}
agent := agentturn.New(kit.Config())
defer kit.Attach(agent)()
New does the work that can fail: discovery, validation, opening the session, dialing MCP. Kit.Config is then pure and may be called per run. Kit.Close releases what New opened.
Kit.Attach is the one step a config cannot carry: it subscribes the recorder to the agent and returns the unsubscribe, so the doubled call above subscribes now and unsubscribes when the scope ends. A session configured but never attached records nothing, and nothing reports that, so attach where the agent is built. It is a no-op without a session, so the line is the same either way.
Index ¶
- Constants
- Variables
- type Conflict
- type Kit
- func (k *Kit) Attach(a *agentturn.Agent) func()
- func (k *Kit) Catalog() *agentskill.Catalog
- func (k *Kit) Close() error
- func (k *Kit) Config() agentturn.Config
- func (k *Kit) Engine() *agentpolicy.Engine
- func (k *Kit) MemoryManifest() agentmemory.Manifest
- func (k *Kit) Omitted() []Omission
- func (k *Kit) OmittedParts() []agentsession.OmittedPart
- func (k *Kit) Parts() []Part
- func (k *Kit) PartsFor(req openresponses.Request) ([]agentsession.InstructionPart, []agentsession.OmittedPart)
- func (k *Kit) Recorder() *session.Recorder
- func (k *Kit) RevokeSkillGrants(ctx context.Context) int
- func (k *Kit) Session() *agentsession.Session
- func (k *Kit) SessionID() string
- func (k *Kit) Tools() []ToolOrigin
- func (k *Kit) Transcript() agentturn.Transcript
- type Omission
- type Option
- func WithAfterToolCall(...) Option
- func WithAgentsMD(path string, opts agentsmd.Options) Option
- func WithBeforeModelCall(fn func(context.Context, *openresponses.Request) error) Option
- func WithBeforeToolCall(...) Option
- func WithBeforeTurn(fn func(context.Context, agentturn.TurnStartInfo) (openresponses.Items, error)) Option
- func WithChildAgent(cfg agentturn.Config, opts ...childagent.Option) Option
- func WithCompaction(budget int, opts ...compact.Option) Option
- func WithCompactionModel(m openresponses.Streamer) Option
- func WithCompactor(c compact.Compactor, opts ...compact.Option) Option
- func WithDeferredTools(fn func(*Kit) []agenttool.Tool) Option
- func WithEngine(e *agentpolicy.Engine) Option
- func WithFilter(fn func(agentturn.Transcript) agentturn.Transcript) Option
- func WithGuards(gs ...guard.Guard) Option
- func WithInstructionBudget(n int64) Option
- func WithInstructions(text string) Option
- func WithMCP(command string, opts ...mcpclient.Option) Option
- func WithMCPTransport(t sdk.Transport, opts ...mcpclient.Option) Option
- func WithMaxParallelTools(n int) Option
- func WithMaxTurns(n int) Option
- func WithMemory(store agentmemory.Store, scopes ...agentmemory.Scope) Option
- func WithMemoryRender(opts ...agentmemory.RenderOption) Option
- func WithMemoryTools(opts ...agentmemory.ToolOption) Option
- func WithModel(m agentturn.Model, name string) Option
- func WithName(name, description string) Option
- func WithOrder(ids ...string) Option
- func WithOutputGuard(fn func(context.Context, agentturn.OutputInfo) (*openresponses.Message, error)) Option
- func WithPolicy(p agentpolicy.Policy, matchers map[string]agentpolicy.ToolMatcher, ...) Option
- func WithReasoning(r openresponses.ReasoningConfig) Option
- func WithRecorder(rec *session.Recorder) Option
- func WithRequest(req openresponses.Request) Option
- func WithRequestExtra(extra map[string]any) Option
- func WithResumedSession(store agentsession.Store, id string, opts ...session.Option) Option
- func WithRetry(r agentturn.Retry) Option
- func WithSession(store agentsession.Store, h agentsession.Header, opts ...session.Option) Option
- func WithShouldStopAfterTurn(fn func(context.Context, agentturn.TurnInfo) (bool, error)) Option
- func WithSkillGrantReport(fn func(SkillGrant)) Option
- func WithSkillGrantScope() Option
- func WithSkillGrants(source func(*agentskill.Skill) agentpolicy.Source) Option
- func WithSkillSources(sources ...agentskill.Source) Option
- func WithSkillTool(opts ...agentskill.ToolOption) Option
- func WithSkills(dirs ...string) Option
- func WithText(t openresponses.TextConfig) Option
- func WithToolConflict(fn func(Conflict)) Option
- func WithToolElicitor(by string, fn agenttool.Elicitor) Option
- func WithToolExecution(mode agentturn.ExecutionMode) Option
- func WithToolFilter(fn func(source string, t agenttool.Tool) bool) Option
- func WithToolProvider(fn func(context.Context) []agenttool.Tool) Option
- func WithToolWrap(fn func(source string, t agenttool.Tool) agenttool.Tool) Option
- func WithTools(ts ...agenttool.Tool) Option
- func WithTransform(fn func(context.Context, agentturn.Transcript) (agentturn.Transcript, error)) Option
- func WithVerdictObserver(fn func(context.Context, agentpolicy.Verdict)) Option
- func WithoutSkillTool() Option
- type Part
- type SkillGrant
- type ToolOrigin
Examples ¶
Constants ¶
const ( // PartProduct is the product's own prompt, from [WithInstructions]. PartProduct = "product" // PartSkills is the skill catalogue, from [WithSkills]. PartSkills = "skills" // PartMemory is the memory block, from [WithMemory]. PartMemory = "memory" )
The identifiers of the parts the kit assembles. A part's id is stable across a session, so a config delta can name a part it does not repeat. The AGENTS.md part's id is agentsmd.PartID, which that module owns.
const ( SourceProduct = "product" SourceSkills = "agentskill" SourceMemory = "agentmemory" SourceAgentsMD = "agentsmd" )
The Source of each part: the library that produced the text, in the harness's own terms, as agentsession.InstructionPart asks for.
const Separator = agentsession.PartSeparator
Separator joins the parts, and is agentsession.PartSeparator: the session format's own, so the joined text of Kit.Parts is the instructions the model was sent and the request hash covers.
Variables ¶
var DefaultOrder = []string{PartProduct, PartSkills, PartMemory, agentsmd.PartID}
DefaultOrder is the order the parts are joined in, unless WithOrder says otherwise. The argument for each position is in docs/ordering.md; the short form is that later text overrides earlier, so the most specific text goes last.
Functions ¶
This section is empty.
Types ¶
type Conflict ¶
type Conflict struct {
// Name is the name both tools claim.
Name string
// Kept is where the tool the model is offered came from.
Kept string
// Dropped is where the tool that is not offered came from.
Dropped string
}
Conflict is two tools claiming one name. The kit unions four tool sets into one namespace and nothing below it detects a collision: two tools with one name reach the model as two entries and the loop dispatches whichever the set returns first.
type Kit ¶
type Kit struct {
// contains filtered or unexported fields
}
Kit is an assembled configuration and the things a front needs that an agentturn.Config cannot carry. It is built by New, which does everything that can fail, and read by Kit.Config, which is pure and may be called once per run.
Concurrency ¶
Every method is safe to call from any goroutine, and so is every config Kit.Config returns. The configs are not independent of each other: they share the kit's memory state, because they share the hook that renders it. Two runs off one kit each render memory for themselves and each build their own instructions, so neither sees a prompt the other assembled; what they share is Kit.Parts and Kit.MemoryManifest, which describe the render that happened last, and the record of the manifest, which is written once per distinct render because one session records both runs.
That is the right sharing for concurrent runs of one agent and the wrong sharing for two agents, which want two prompts, two manifests and usually two sessions. Give each agent its own kit.
func New ¶
New assembles the kit. It does the work that can fail — discovering skills, reading the AGENTS.md chain, rendering memory, building the policy engine, starting or resuming the session, dialing every MCP server — and reports it once. A kit that dialed some servers before one failed closes the ones it opened before returning the error, so a failed New leaks nothing.
Example ¶
A whole coding agent: a prompt, the repository's AGENTS.md chain, a skill catalogue, memory, a policy, some tools, an MCP server, a recorded session and compaction. Everything that can fail fails in New, so there is one error to handle.
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"github.com/ChristopherDavenport/agentkit"
"github.com/ChristopherDavenport/agentmemory"
"github.com/ChristopherDavenport/agentpolicy"
"github.com/ChristopherDavenport/agentsession"
"github.com/ChristopherDavenport/agentsmd"
"github.com/ChristopherDavenport/agentturn"
"github.com/ChristopherDavenport/openresponses"
)
func main() {
ctx := context.Background()
cwd, _ := os.Getwd()
home, _ := os.UserHomeDir()
model := &openresponses.ClientAdapter{Client: openresponses.NewClient(
"https://api.example.com/v1",
openresponses.WithAPIKey(os.Getenv("API_KEY")),
)}
memory := agentmemory.NewMemStore()
sessions := agentsession.NewMemoryStore()
kit, err := agentkit.New(ctx,
agentkit.WithModel(model, "gpt-5"),
agentkit.WithInstructions("Be brief."),
agentkit.WithAgentsMD(cwd, agentsmd.Options{Root: cwd}),
agentkit.WithSkills(filepath.Join(home, ".dex", "skills")),
agentkit.WithMemory(memory, "user", "project"),
agentkit.WithPolicy(agentpolicy.Suggest(agentpolicy.Tools{
Read: []string{"read"},
Edit: []string{"edit"},
Execute: []string{"bash"},
}), map[string]agentpolicy.ToolMatcher{
"bash": {Match: agentpolicy.PrefixMatcher("command")},
}),
agentkit.WithMCP("some-server --stdio"),
agentkit.WithSession(sessions, agentsession.Header{CWD: cwd}),
agentkit.WithCompaction(60_000),
)
if err != nil {
log.Fatal(err)
}
defer kit.Close()
// Everything the layers left out of the prompt, in one list.
for _, o := range kit.Omitted() {
fmt.Printf("not given to the model: %s (%s)\n", o.What, o.Reason)
}
agent := agentturn.New(kit.Config())
defer kit.Attach(agent)()
end, err := agent.Prompt(ctx, openresponses.UserText("what does this repo do?"))
if err != nil {
log.Fatal(err)
}
fmt.Println(end.Reason)
}
Output:
func (*Kit) Attach ¶
Attach subscribes the recorder to the agent and returns the unsubscribe. It returns a no-op when there is no session, so a caller need not branch on one, and under WithRecorder, whose recorder its owner attaches: a second subscription would write every event twice.
func (*Kit) Catalog ¶
func (k *Kit) Catalog() *agentskill.Catalog
Catalog is the skill catalogue, or nil when no skills were configured.
func (*Kit) Close ¶
Close releases what New opened, in reverse order, joining the errors: the MCP clients. The stores a caller passed in — the memory store, the session store — stay the caller's to sync, release and close, since the kit did not open them.
A config handed out before Close keeps working and stops offering the closed servers' tools, so a run that outlives the kit is offered what it can still reach. Close is safe to call twice.
func (*Kit) Config ¶
Config is the assembled configuration. It is a plain agentturn.Config: every field a product could have set by hand, with the same values, by calling the same exported functions. It is pure and may be called once per run.
func (*Kit) Engine ¶
func (k *Kit) Engine() *agentpolicy.Engine
Engine is the policy engine, or nil when no policy was configured. A front reads agentpolicy.Engine.Deferred and calls agentpolicy.Engine.Release through it.
func (*Kit) MemoryManifest ¶
func (k *Kit) MemoryManifest() agentmemory.Manifest
MemoryManifest is what the last render put in the memory block, and the hash a product compares to record the render only when it moved. The kit records it itself when a session is configured.
func (*Kit) Omitted ¶
Omitted is everything the layers considered for the instructions and left out: the files the AGENTS.md budget dropped and the names a preferred file shadowed, the memory entries the last render did not fit, the skills that would not load and the ones the catalogue will not offer. One list, because a product wants one and a session's instructions_omitted is one.
func (*Kit) OmittedParts ¶
func (k *Kit) OmittedParts() []agentsession.OmittedPart
OmittedParts is Kit.Omitted in the shape the session format records.
func (*Kit) Parts ¶
Parts are the instructions as named parts, in the order they were joined, as the last request was sent them: after the memory block was re-rendered and after the input guards ran over each part, so a part guard.Redact rewrote holds the rewritten text. Their texts joined with Separator are the instructions that request carried, and before the first request they are agentturn.Config.Instructions, so they may be handed to agentsession.ConfigFromRequestParts as they are.
A guard that rewrites the joined instructions in its pass over the whole request, or a product hook after the kit's, leaves the parts describing the text before it, and Kit.PartsFor then says so.
Example ¶
The kit's instruction parts are the shape a session records, so they go straight into a config entry: a change to one layer is then recorded as a change to one part rather than as a new copy of the whole prompt.
kit, err := agentkit.New(context.Background(),
agentkit.WithModel(stubModel{}, "gpt-5"),
agentkit.WithInstructions("Be brief."),
)
if err != nil {
log.Fatal(err)
}
defer kit.Close()
for _, p := range kit.Parts() {
fmt.Printf("%s (%s): %s\n", p.ID, p.Source, p.Text)
}
fmt.Println(agentsession.JoinInstructions(kit.Parts()) == kit.Config().Instructions)
Output: product (product): Be brief. true
func (*Kit) PartsFor ¶
func (k *Kit) PartsFor(req openresponses.Request) ([]agentsession.InstructionPart, []agentsession.OmittedPart)
PartsFor returns the parts req's instructions are composed of and what the layers left out, or nil and nil when Kit.Parts do not join to req.Instructions: a request a hook after the kit's rewrote, or one built from a render other than the last. Its signature is the one session.WithInstructionsParts takes, and a session the kit opens is given it, so the recorder's config entries carry instructions_parts and instructions_omitted. A recorder opened elsewhere, the one WithRecorder is given, takes it the same way:
var kit *agentkit.Kit
rec, _, err := session.Start(ctx, store, h,
session.WithInstructionsParts(func(req openresponses.Request) ([]agentsession.InstructionPart, []agentsession.OmittedPart) {
return kit.PartsFor(req)
}))
kit, err = agentkit.New(ctx, agentkit.WithRecorder(rec), ...)
Concurrent runs off one kit share the last render, so a request from the other run's render gets nil and is recorded as a string, which is what the recorder does with parts that do not join.
func (*Kit) Recorder ¶
Recorder is the session recorder, or nil when no session was configured: the one the kit opened, or the one WithRecorder gave it. Attach one the kit opened to the agent with session.Recorder.Attach, or use Kit.Attach.
func (*Kit) RevokeSkillGrants ¶
RevokeSkillGrants revokes every grant a skill's read made through WithSkillGrants and returns the number of rules the engine removed. A front calls it when a grant should end, the end of a run or of a conversation; WithSkillGrantScope calls it when each run starts. A skill read again is granted again. It is zero and does nothing without skill grants.
func (*Kit) Session ¶
func (k *Kit) Session() *agentsession.Session
Session is the session New started or resumed, or nil, as it is under WithRecorder. After a resume its Context().Items is what the agent should be seeded with.
func (*Kit) Tools ¶
func (k *Kit) Tools() []ToolOrigin
Tools lists the tools in the union at New, after WithToolFilter and WithToolWrap and before the policy's filter, each with the source it came from. It is how a product names the tools a library made, the skill tool, the memory tools, an MCP server's, a child's, to a policy whose default asks, since the engine is built before the union exists:
var allow []agentpolicy.Rule
for _, t := range kit.Tools() {
if t.Source != "WithTools" {
allow = append(allow, agentpolicy.Rule{Tool: t.Name, Source: libraries})
}
}
The kit does not allow them itself: whether a library's tool runs unasked is the product's decision. An MCP server's list and a provider's are fetched each turn, so a tool they add after New is not here.
func (*Kit) Transcript ¶
func (k *Kit) Transcript() agentturn.Transcript
Transcript is the conversation in force at the session's leaf, and what an agent continuing it must be seeded with. It is empty for a session WithSession started and for no session at all, so a caller need not branch on which:
agent := agentturn.New(kit.Config(), agentturn.WithTranscript(kit.Transcript()))
type Omission ¶
type Omission struct {
// Part is the id of the part the omission belongs to.
Part string
// Source is the library that reported it.
Source string
// What names the thing left out in its layer's own stable key: an
// absolute path for a file, "scope/name" for a memory entry, a
// location for a skill.
What string
// Reason is the layer's own word for why.
Reason string
// Size is the bytes the thing would have added, zero when the
// layer does not say.
Size int64
// By names what took its place, for an omission that is a
// shadowing rather than a bound.
By string
}
Omission is one thing a layer considered for the instructions and left out. The layers each report their own; the kit returns them as one list, because a product wants one list and a session wants one instructions_omitted.
func (Omission) OmittedPart ¶
func (o Omission) OmittedPart() agentsession.OmittedPart
OmittedPart is the omission as the session format records it.
type Option ¶
type Option func(*settings)
Option configures New. An option records a choice; every choice is applied in New, in the order the package documents rather than the order the options were given, so which option comes before which never changes the result.
Repeating one option is a different matter, and the option says which it does. Most replace: a second WithModel or WithInstructions wins. The ones that accumulate keep the order they were given, and for the tool sources that order is load-bearing — WithSkills decides which of two skills of one name shadows the other, and WithTools, WithMCP and WithToolProvider decide which side of a Conflict is kept.
func WithAfterToolCall ¶
func WithAfterToolCall(fn func(context.Context, agentturn.ToolResultInfo) (*agentturn.ToolOverride, error)) Option
WithAfterToolCall sets the hook that may replace a tool's result. The kit contests nothing here, so it is the product's field alone and setting it twice keeps the second.
func WithAgentsMD ¶
WithAgentsMD reads the AGENTS.md chain that applies at path and renders it as the agentsmd.PartID part. opts are agentsmd.Options verbatim: the names to look for, the root the walk stops after, the explicit Extra files, and the per-file and total byte bounds.
A budget set here is the layer's own and applies whatever WithInstructionBudget says. When both are set the smaller of the two binds; see docs/ordering.md.
func WithBeforeModelCall ¶
WithBeforeModelCall adds a hook on the built request. It runs after the kit's own: after memory has re-rendered the instructions and after the guards have seen them.
func WithBeforeToolCall ¶
func WithBeforeToolCall(fn func(context.Context, agentturn.ToolCallInfo) (*agentturn.ToolDecision, error)) Option
WithBeforeToolCall adds a policy on each tool call, after the engine's. Decisions fold deny over ask over allow, as agentturn.ChainBeforeToolCall describes.
func WithBeforeTurn ¶
func WithBeforeTurn(fn func(context.Context, agentturn.TurnStartInfo) (openresponses.Items, error)) Option
WithBeforeTurn adds a hook to the start of each turn. Several are joined with agentturn.ChainBeforeTurn, in the order given.
func WithChildAgent ¶
func WithChildAgent(cfg agentturn.Config, opts ...childagent.Option) Option
WithChildAgent offers cfg as a tool, through agentturn/tools/agent, so the model can delegate a piece of work to an agent of its own. The tool is named agentturn.Config.Name unless childagent.WithToolName says otherwise.
When a session is configured the child's run is recorded into it, live and linked to the parent's, because the kit binds childagent.WithObserver to the recorder, and the child runs under the recorder's session.Recorder.ChildContext through childagent.WithRunContext, so what the child's tools attribute to a session names the child's. With WithMemory as well, the same context carries the child's session ID under agentmemory.WithSession, which is the key the memory journal reads: a memory the child saves names the child's session. Those bindings are the reason this option exists rather than the child going in through WithTools: the recorder does not exist until New has opened the session, so a product doing this by hand reaches for WithDeferredTools and a nil check. The caller's own options are applied after the kit's, so passing childagent.WithObserver or childagent.WithRunContext here still wins.
The kit cannot do the same for the parent's own run, whose context is the host's: a host that wants the parent's memory writes to name its session prompts with agentmemory.WithSession(ctx, kit.SessionID()).
The child is an ordinary agenttool.Tool in every other way: it joins the set where WithTools puts it, the policy filters it, and a name it shares with another tool is a Conflict.
Example ¶
An in-process child agent is a tool like any other. WithChildAgent offers one and, when a session is configured, records the child's own run into it: the observer that does the linking is the recorder the kit made, which is why the kit is the one that can bind it.
kit, err := agentkit.New(context.Background(),
agentkit.WithModel(stubModel{}, "gpt-5"),
agentkit.WithInstructions("Be brief."),
agentkit.WithChildAgent(agentturn.Config{
Name: "explore",
Description: "Delegate a read-only investigation to a sub-agent.",
Model: stubModel{},
ModelName: "gpt-5",
Instructions: "You are a read-only explorer. End with a written answer.",
MaxTurns: 10,
}),
)
if err != nil {
log.Fatal(err)
}
defer kit.Close()
for _, t := range kit.Config().ResolveTools(context.Background()) {
fmt.Println(t.Name())
}
Output: explore
func WithCompaction ¶
WithCompaction folds the transcript with the model the kit was given when it grows past budget tokens. When a session is recorded, the fold is recorded with it.
func WithCompactionModel ¶
func WithCompactionModel(m openresponses.Streamer) Option
WithCompactionModel folds with a model other than the agent's, which is how a cheap model summarises for an expensive one.
func WithCompactor ¶
WithCompactor folds with a compactor the product built.
func WithDeferredTools ¶
WithDeferredTools adds tools that cannot be built until the kit has built the rest of itself: fn is called once, inside New, after the session is open and the policy engine exists, and its tools join the set where WithTools puts them.
The case it was written for, a child agent whose own runs are recorded, is WithChildAgent now; this is the general form, for a tool that wants the policy engine, the skill catalogue or the session itself. Without it such a tool has to be built lazily inside a per-turn provider, which is a cache and a nil check standing in for an ordering the kit already knows.
The Kit it is given is not yet finished: Kit.Config is not built. Kit.Recorder, Kit.Session, Kit.Engine and Kit.Catalog are.
Example ¶
WithDeferredTools is the general form, for any tool that has to be built after New has opened the session and built the engine. Here a tool reports the session it is running in, which does not exist until New has opened it.
kit, err := agentkit.New(context.Background(),
agentkit.WithModel(stubModel{}, "gpt-5"),
agentkit.WithSession(agentsession.NewMemoryStore(), agentsession.Header{CWD: "/tmp"}),
agentkit.WithDeferredTools(func(k *agentkit.Kit) []agenttool.Tool {
id := k.SessionID()
return []agenttool.Tool{agenttool.New("session_id",
"Report the session this conversation is recorded in.",
func(context.Context, agenttool.NoArgs) (string, error) {
return id, nil
})}
}),
)
if err != nil {
log.Fatal(err)
}
defer kit.Close()
for _, t := range kit.Config().ResolveTools(context.Background()) {
fmt.Println(t.Name())
}
Output: session_id
func WithEngine ¶
func WithEngine(e *agentpolicy.Engine) Option
WithEngine uses an engine the product built. It is WithPolicy for a product that needs the engine before the kit exists, and the two are mutually exclusive. The kit cannot give an engine it did not build an observer, so its verdicts are recorded only if the product's own observer records them.
func WithFilter ¶
func WithFilter(fn func(agentturn.Transcript) agentturn.Transcript) Option
WithFilter sets the filter that drops app-only items before each model call. nil, the default, means agentturn.DefaultFilter.
func WithGuards ¶
WithGuards adds output and input guards. Each one runs on the request before it is sent, on each assistant message as it completes, and on the turn, through a guard.Chain's BeforeModelCall, OutputGuard and ShouldStopAfterTurn.
Before the request is checked whole, each instructions part is checked on its own, as a guard.Input carrying that part's text and no items, so a guard that rewrites instructions, guard.Redact for one, rewrites the part the text is in and Kit.Parts holds what was sent. The whole request, its items and the joined instructions, is then checked as before, which is where a guard that measures the whole, guard.Limit, has its say. A guard therefore sees each part's text twice, once alone and once joined.
The chain's observer records each verdict that blocked or gave a reason when a session is configured, and hands every verdict to WithVerdictObserver.
WithInstructionBudget measures the parts before the guards run, so a guard that makes a part longer, a redaction whose placeholder is longer than the secret, can send instructions over the budget.
func WithInstructionBudget ¶
WithInstructionBudget bounds the joined instructions to n bytes. The parts that can be bounded are bounded, the rest are measured, and New fails when the parts that cannot be bounded already exceed n rather than sending a prompt the caller asked not to send. Zero, the default, leaves each layer its own bound. How the budget is spent is argued in docs/ordering.md.
func WithInstructions ¶
WithInstructions sets the product's own prompt: the first instructions part, PartProduct, the frame every other part refines.
func WithMCP ¶
WithMCP connects to an MCP server run as a subprocess: command is split on whitespace into the program and its arguments. A server that needs an environment, a working directory or an argument with a space in it is dialed with WithMCPTransport instead.
func WithMCPTransport ¶
WithMCPTransport connects to an MCP server over the given transport.
func WithMaxParallelTools ¶
WithMaxParallelTools bounds a parallel batch to n calls at a time. Zero, the default, means agenttool.DefaultMaxParallel.
func WithMaxTurns ¶
WithMaxTurns stops a run after n turns. Zero means no limit.
func WithMemory ¶
func WithMemory(store agentmemory.Store, scopes ...agentmemory.Scope) Option
WithMemory renders the memory block for the given scopes, in order, as the PartMemory part, offers the memory tools, and re-renders the block before every model call so the model sees the freshest state. agentmemory.Usage, the paragraph that tells the model what the block is and which tool makes which change, follows the block in the same part, since the tools are offered whenever the block is.
func WithMemoryRender ¶
func WithMemoryRender(opts ...agentmemory.RenderOption) Option
WithMemoryRender passes options to agentmemory.Render, both for the first render and for the per-turn one.
func WithMemoryTools ¶
func WithMemoryTools(opts ...agentmemory.ToolOption) Option
WithMemoryTools passes options to agentmemory.Tools.
func WithModel ¶
WithModel sets the model and the model name of every request. It is the one required option: New refuses a kit without a model, as the loop refuses a config without one.
func WithName ¶
WithName sets agentturn.Config.Name and Description: how the agent names itself when it is composed as a tool, an MCP server or an agent card.
func WithOrder ¶
WithOrder replaces the order the parts are joined in. ids are part identifiers — PartProduct, PartSkills, PartMemory, agentsmd.PartID — and every configured part must appear exactly once, or New fails. The default order and the argument for it are in docs/ordering.md.
Example ¶
A product that outgrows one of the kit's decisions replaces that decision without leaving the kit. Here the order is the product's, and everything else stays the kit's.
kit, err := agentkit.New(context.Background(),
agentkit.WithModel(stubModel{}, "gpt-5"),
agentkit.WithInstructions("Be brief."),
agentkit.WithMemory(agentmemory.NewMemStore(), "user"),
agentkit.WithOrder(agentkit.PartMemory, agentkit.PartProduct),
)
if err != nil {
log.Fatal(err)
}
defer kit.Close()
for _, p := range kit.Parts() {
fmt.Println(p.ID)
}
Output: memory product
func WithOutputGuard ¶
func WithOutputGuard(fn func(context.Context, agentturn.OutputInfo) (*openresponses.Message, error)) Option
WithOutputGuard adds a guard on each assistant message, after the guards WithGuards added. Each sees what the one before it left.
func WithPolicy ¶
func WithPolicy(p agentpolicy.Policy, matchers map[string]agentpolicy.ToolMatcher, opts ...agentpolicy.Option) Option
WithPolicy builds a policy engine from p and the matchers, and wires its tool filter and its BeforeToolCall hook. Use Kit.Engine to reach the engine a front needs for Deferred and Release.
When a session is configured, or WithVerdictObserver is, the kit gives the engine an observer through agentpolicy.WithObserver, ahead of opts: it records each verdict and hands it to WithVerdictObserver. The engine keeps every observer it is given, so an agentpolicy.WithObserver in opts runs beside the kit's, after it, and the recording stays either way.
func WithReasoning ¶
func WithReasoning(r openresponses.ReasoningConfig) Option
WithReasoning sets the reasoning effort and summary of every request.
func WithRecorder ¶
WithRecorder records into a recorder the product opened, for a kit built where the recorder already exists: under an evaluation runner's configuration function, which hands it the recorder that writes the task, or inside a parent's WithDeferredTools. Everything the kit binds to a session it binds to rec: agentturn.Config.ToolRecorder, the fold, a child agent, the memory manifest and the verdicts.
The kit opens nothing and attaches nothing: Kit.Attach is a no-op, since the recorder's owner attaches it, and Kit.Session and Kit.Transcript are empty, since the owner seeded the agent. The recorder's options were the owner's to choose, so the kit cannot give it its parts; an owner that wants them passes session.WithInstructionsParts with a function that calls Kit.PartsFor on the kit it is about to build. It is mutually exclusive with WithSession and WithResumedSession, and a nil rec is no recorder.
func WithRequest ¶
func WithRequest(req openresponses.Request) Option
WithRequest sets the base request every call is built from: tool_choice, parallel_tool_calls, max_output_tokens, temperature and the rest. The kit owns none of its members and copies it through.
func WithRequestExtra ¶
WithRequestExtra sets members the request type does not name, merged over WithRequest's own Extra on every call: a vendor's field, a preview flag, anything the endpoint takes and openresponses does not model.
func WithResumedSession ¶
WithResumedSession continues the session with the given ID at its leaf, through session.Resume. Kit.Session then holds the session, whose Context().Items is what the agent should be seeded with. The recorder takes the kit's parts as under WithSession.
func WithRetry ¶
WithRetry sends a failed model call again, under the policy r. The zero agentturn.Retry, which is the default, retries nothing; the loop's own agentturn.DefaultRetryable and agentturn.DefaultBackoff apply to the members r leaves nil. Only an attempt that has not committed is retried, which is agentturn's rule and not the kit's.
func WithSession ¶
func WithSession(store agentsession.Store, h agentsession.Header, opts ...session.Option) Option
WithSession starts a session from h and records the run into it. The recorder subscribes through Kit.Attach; the store stays the caller's to sync, release and close.
The recorder is opened with session.WithInstructionsParts bound to Kit.PartsFor, ahead of opts, so its config entries carry the instructions as Kit.Parts and what the layers left out as instructions_omitted. A session.WithInstructionsParts in opts replaces the kit's.
func WithShouldStopAfterTurn ¶
WithShouldStopAfterTurn adds a reason to end a run after a turn, after the policy's. The chain stops at the first hook that stops the run, so the error on RunEnd says which one fired.
func WithSkillGrantReport ¶
func WithSkillGrantReport(fn func(SkillGrant)) Option
WithSkillGrantReport is told what the engine did with each skill's allowed-tools: what it granted, what it refused and why, and a skill whose allowed-tools would not parse. A grant widens what the agent may do, so a front that shows the user the policy in force wants to see it happen.
func WithSkillGrantScope ¶
func WithSkillGrantScope() Option
WithSkillGrantScope revokes every grant a skill's read made when a new user message starts a run, as Claude Code clears allowed-tools when the next message arrives: a later request that wants the tools reads the skill again. A run that Resume starts after an approval, or that Continue starts, is the same task going on and keeps them. It is a agentturn.Config.BeforeTurn hook that calls Kit.RevokeSkillGrants on turn 1 when the transcript ends with a user message, ahead of the product's own BeforeTurn.
Only the sources the kit granted are revoked; a product's own agentpolicy.Engine.GrantSet calls are left alone. Every run on the engine revokes them, so two agents sharing one engine share one scope. It has no effect without WithSkillGrants.
func WithSkillGrants ¶
func WithSkillGrants(source func(*agentskill.Skill) agentpolicy.Source) Option
WithSkillGrants grants a skill's allowed-tools to the policy engine each time the model reads that skill. source builds the agentpolicy.Source the grant is attributed to; nil means a source named "agentskill:" and the skill's name, with the skill's location as its path, which is untrusted and therefore contributes its deny and ask rules alone.
A grant lasts until something revokes it. WithSkillGrantScope revokes every skill's grant when a new message starts a run, which is the lifetime Claude Code gives allowed-tools; without it a grant lasts the life of the engine unless the product calls Kit.RevokeSkillGrants. A skill read again is granted again, and reported again: a repeated agentpolicy.Engine.GrantSet under one source name replaces the set, so a read after a revoke puts the grant back. Two skills the source function gives one name share one grant, and the later read replaces the earlier's rules.
Untrusted is the default because a skill is a file someone else wrote. A product that trusts the tree its skills came from returns a Source with Trusted set, and then a skill's allowed-tools widens what the agent may do the moment the model reads it. That is the whole point of allowed-tools and it is also a privilege escalation, so the kit will not assume it.
It has no effect without a policy engine, and none without skills, so a product may add it unconditionally and the two behind flags. WithoutSkillTool is the one combination New refuses: a grant happens when the model reads a skill, which it does through the catalogue's tool, so withholding that tool withholds every grant.
func WithSkillSources ¶
func WithSkillSources(sources ...agentskill.Source) Option
WithSkillSources adds skill sources that are not local directories: an embed.FS, a zip, an adapter over a remote store. They are discovered after the directories WithSkills named, so a directory shadows a source given here by the same name.
func WithSkillTool ¶
func WithSkillTool(opts ...agentskill.ToolOption) Option
WithSkillTool offers the catalogue's tool with the given options. It is offered by default when any skill source is configured; this is how a caller changes its options without changing that.
func WithSkills ¶
WithSkills discovers skills in the given directories, in order, and renders the catalogue as the PartSkills part. A directory that does not exist is an error from New, as it is from agentskill.Dir: a misspelled skill path silently offering no skills is the failure this refuses.
Unless WithoutSkillTool says otherwise, the catalogue's tool is offered and agentskill.Catalog.Usage is appended to the part, which is the pairing agentskill asks for: the usage paragraph tells the model to reach the listed skills through a tool, so it is written only when that tool is there.
func WithText ¶
func WithText(t openresponses.TextConfig) Option
WithText sets the output format and verbosity of every request.
func WithToolConflict ¶
WithToolConflict is called when two tools claim one name at turn time, which is the only point at which a remote's tool list can collide with a name it did not collide with when New checked. The later tool is dropped, the earlier is kept, and fn is told. A conflict present at New is an error from New instead.
func WithToolElicitor ¶
WithToolElicitor sets the elicitor a tool's question to the user reaches mid-call, an MCP server's elicitation among them, as agentturn.Config.ToolElicitor. With a session configured the kit sets the recorder's session.Recorder.Elicitor around fn, so each question and its answer are written under the call that asked; by names who answers, in the session format's words, such as agentpolicy.ByHuman. Without the option the field stays nil and an elicitor on the prompt's context applies.
func WithToolExecution ¶
func WithToolExecution(mode agentturn.ExecutionMode) Option
WithToolExecution selects parallel batches, the default, or sequential ones. A tool that declares itself agenttool.Sequential runs alone whatever this says.
func WithToolFilter ¶
WithToolFilter drops tools from the union before the model is offered them: fn is called once per tool per turn, with the label of the source that produced it — "WithTools", "WithSkills", "WithMemory", "mcp:#1 some-server" — and keeps the tool when it returns true.
It is how a product takes five tools from a server that offers forty. The union is the one place every source meets, and the label is knowledge only the kit has, which is why the filter is here and not in the libraries the tools came from. A policy is the other way to withhold a tool and a better one where it fits: a deny rule is recorded, explicable and the same rule that stops the call.
It runs before the duplicate check, so filtering one of two tools that claim a name resolves the conflict rather than reporting it.
func WithToolProvider ¶
WithToolProvider supplies tools from a source the kit does not know about, called once per turn. Its tools come last, after the MCP servers', and a name it repeats is a Conflict resolved the same way. Several providers are each their own source, in the order given, so a collision between two of them says which is which.
func WithToolWrap ¶
WithToolWrap replaces tools in the union with what fn returns: fn is given each tool with the label of its source, the same label WithToolFilter is given, and returns the tool to offer in its place, or the tool itself to leave it alone. It is how a middleware reaches every tool, a replay, a timer or a logger, from inside the kit rather than around the provider.
It runs inside the kit's own wrapper, so the skill tool's grant is made on what fn's tool returned: a wrapper whose result carries the catalogue's agentskill.Read in Details still grants, and one that returns a result without it, a replay that decodes a recording into its own type, does not. The filter sees the tool its source produced, not fn's. A fixed source's tools, those from WithTools, WithChildAgent, WithDeferredTools, WithSkills and WithMemory, are wrapped once, in New, before the filter is asked about them, so fn is called for a tool the filter then drops; an MCP server's and a WithToolProvider's are wrapped each turn, after the filter, since their lists are fetched each turn. A wrapper that keeps a tool's properties uses agenttool.Wrap. The duplicate check, Kit.Tools and the policy read the name of the tool fn returned, and a nil return drops the tool.
func WithTools ¶
WithTools adds the product's own tools. They come first in the tool set, so a listing is reproducible.
func WithTransform ¶
func WithTransform(fn func(context.Context, agentturn.Transcript) (agentturn.Transcript, error)) Option
WithTransform sets the product's transcript transform. With compaction as well, the two are joined with agentturn.ChainTransform, the product's first, so the fold is over what the product shaped. Setting it twice keeps the second.
func WithVerdictObserver ¶
func WithVerdictObserver(fn func(context.Context, agentpolicy.Verdict)) Option
WithVerdictObserver is told every verdict the policy engine the kit built and the guards reach, after the kit has recorded it. It is the product's observer, for a front that shows the policy at work; the kit binds the recording itself. It is the one way to see the guards' verdicts, since the kit builds their chain. The engine's verdicts also reach an agentpolicy.WithObserver passed to WithPolicy, so a product that passes both is told each engine verdict twice.
With a session configured the kit writes each of the engine's verdicts, and each guard verdict that blocked or gave a reason, through session.Recorder.Annotate under agentpolicy.VerdictNS, in the shape agentpolicy.Verdict.Record gives. A guard's bare allow is not written, as a hook's allow with no reason is not: a guard runs on every part and every message, and recording its silence would outweigh the run.
func WithoutSkillTool ¶
func WithoutSkillTool() Option
WithoutSkillTool renders the catalogue into the prompt without offering the tool that reads a skill. The usage paragraph is then not appended either, since it describes a tool the model does not have. A product that serves skill bodies its own way wants this.
type Part ¶
type Part = agentsession.InstructionPart
Part is one named block of the instructions: its stable id, its text and the layer that produced it. It is agentsession.InstructionPart itself, which is the shape a session's instructions_parts wants, so a product hands Kit.Parts straight to agentsession.ConfigFromRequestParts.
type SkillGrant ¶
type SkillGrant struct {
// Skill is the skill that was read.
Skill string
// Location is the SKILL.md behind the name.
Location string
// Granted are the rules the engine took.
Granted []agentpolicy.Rule
// Refused are the rules it would not take, each with the engine's
// reason.
Refused []agentpolicy.Refusal
// Err is set when the skill's allowed-tools would not parse, in
// which case nothing was granted.
Err error
}
SkillGrant is what happened when a skill the model read asked for tools. It is reported through the function WithSkillGrantReport was given, once per read.
type ToolOrigin ¶
ToolOrigin is one tool in the union and the source it came from: the label WithToolFilter, WithToolWrap and a Conflict use, such as "WithTools", "WithSkills", "WithMemory", "WithChildAgent #1 explore" or "mcp:#1 some-server". Kit.Tools lists them.