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
- func ContextWithRecorder(ctx context.Context, rec *session.Recorder) context.Context
- func PartsFrom(kits ...**Kit) ...
- func RecorderFromContext(ctx context.Context) *session.Recorder
- type Conflict
- type Kit
- func (k *Kit) AddMCP(ctx context.Context, command string, opts ...mcpclient.Option) (string, error)
- func (k *Kit) AddMCPTransport(ctx context.Context, t sdk.Transport, opts ...mcpclient.Option) (string, error)
- func (k *Kit) AgentOptions() []agentturn.Option
- 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) GrantScope(ctx context.Context) string
- func (k *Kit) LookupTool(name string) (agenttool.Tool, bool)
- func (k *Kit) LookupToolFor(ctx context.Context, name string) (agenttool.Tool, bool)
- 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(_ context.Context, req openresponses.Request) ([]agentsession.InstructionPart, []agentsession.OmittedPart)
- func (k *Kit) Recorder() *session.Recorder
- func (k *Kit) RegrantSkills(ctx context.Context, sess *agentsession.Session) error
- func (k *Kit) ReloadSkills(ctx context.Context) error
- func (k *Kit) RemoveMCP(label string) error
- 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, budget int, 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 WithFoldObserver(fn func(context.Context, compact.Fold)) 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 WithMCPStderr(w io.Writer) 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 WithMemoryReadScopes(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 WithOptionalSkills(dirs ...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]. The block is // several parts, one per piece [agentmemory.RenderParts] returns, // and PartMemory is both the ID of the first, the block's title, and // the ID [WithOrder] places the whole group by. Every part of the // group has an ID that is PartMemory or starts with "memory/" or // "memory:". PartMemory = "memory" // PartMemoryUsage is the last part of the memory group: // [agentmemory.Usage], the paragraph that tells the model what the // block is and which tool makes which change. PartMemoryUsage = "memory:usage" )
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.
var ErrSkillGrantChanged = errors.New("agentkit: the skill changed since it was read, so its grant was not made again")
ErrSkillGrantChanged is the SkillGrant.Err of a read a restart will not grant again because the skill is not what the model read: the digest of the instructions the catalogue's tool serves for it now, or of its frontmatter, is not the one the read recorded. The report has SkillGrant.Replayed set and SkillGrant.FrontmatterChanged says which digest moved; the session records a verdict saying the same. Nothing is granted: a read of the skill grants by the skill as it is now, and a front that shows the user the policy in force shows that the skill changed since they approved it.
var ErrSkillGrantRecorder = errors.New("agentkit: no recorder writes the session, so its skill grants would be recorded nowhere")
ErrSkillGrantRecorder is the error Kit.RegrantSkills returns, before it binds or grants anything, when the kit records its engine's verdicts and no recorder writes the session it was given: neither the one on ctx, ContextWithRecorder, nor the kit's own. The grants' verdicts and a later revocation would be recorded nowhere, and the next restart would find a session that says nothing about them.
var ErrSkillGrantUnrecorded = errors.New("agentkit: the session records no grant for the read, so it was not made again")
ErrSkillGrantUnrecorded is the SkillGrant.Err of a live read, found by New or Kit.RegrantSkills in a session a kit that records its engine's verdicts is to grant again, for which the session records no verdict of the grant: the engine observes a verdict for every rule a GrantSet grants or refuses, so none means the kit's observer wrote nowhere when the read was made, as it does when the run's context carries no recorder, ContextWithRecorder, and the kit has none. Nothing is granted: a restart grants what the session says the read was granted, and this session says nothing.
Functions ¶
func ContextWithRecorder ¶ added in v0.0.3
ContextWithRecorder returns ctx carrying rec as the recorder of the conversation a run prompted with it belongs to. A front that serves one kit to many conversations, each recorded as a session of its own, prompts each run with it, and everything the kit records for that run goes to rec rather than to the kit's own recorder: the engine's and the guards' verdicts, the memory manifest, a fold, a tool's question and its answer, and a child agent's run. Without it they go to the kit's recorder, or nowhere when the kit has none.
It is what the RecorderFor of agentturn/front/a2a's WithRecorderFor returns, beside pointing the agent's ToolRecorder at rec:
fronta2a.WithRecorderFor(func(ctx context.Context, contextID string, a *agentturn.Agent) (context.Context, func(), error) {
rec, err := openRecorder(ctx, store, contextID) // session.WithInstructionsParts(kit.PartsFor)
if err != nil {
return nil, nil, err
}
cfg := a.Config()
cfg.ToolRecorder = rec.RecordFunc()
if err := a.SetConfig(cfg); err != nil {
return nil, nil, err
}
ctx = session.ContextWithSessionID(ctx, rec.SessionID())
detach, id := rec.Attach(a), rec.SessionID()
return agentkit.ContextWithRecorder(ctx, rec), func() {
detach()
// The store holds an opened session, a lock and the whole
// session in memory, until it is released; the next message
// resumes it.
if r, ok := store.(interface{ Release(string) error }); ok {
_ = r.Release(id)
}
}, nil
})
The engine, the guards' chain and the per-turn hooks are bound once, at New, so a recorder a front sets with SetConfig cannot reach them; the context is the one thing every one of them is handed. An engine the product built, WithEngine, has no observer of the kit's, so its verdicts follow the recorder on the context only if the product's own agentpolicy.WithObserver reads it with RecorderFromContext. A nil rec returns ctx as it is.
A skill grant follows the conversation: it is a rule set the kit's one engine keeps under the grant scope this recorder's session names, Kit.GrantScope, or session.ContextWithSessionID's where there is no recorder, and decides only that conversation's calls. An MCP server's connection, dialed once at New, whose identity every conversation shares, WithMCP, stays the kit's: a front that needs one per conversation or per user gives each its own kit.
func PartsFrom ¶ added in v0.0.2
func PartsFrom(kits ...**Kit) func(context.Context, openresponses.Request) ([]agentsession.InstructionPart, []agentsession.OmittedPart)
PartsFrom returns a parts function for a recorder several kits share: each kit is asked in turn, and the first whose Kit.PartsFor joins to the request answers. It is how one session records a handoff between two agents, or a parent and a child kit built under WithRecorder, with each agent's instructions as its own parts.
It takes the kits' variables, not the kits: the recorder is opened before the kits that record into it are built, so the kits do not exist when this is called. Each variable is read on every request, and one still nil is passed over.
func RecorderFromContext ¶ added in v0.0.3
RecorderFromContext returns the recorder ContextWithRecorder put on ctx, or nil.
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, and each run's memory_save is based on that run's own render. 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. A front serving one kit to many conversations, each its own session, prompts each run under ContextWithRecorder so what the kit records lands in the right one.
Every agent built from a kit with a session is attached to it, Kit.Attach, or prompted under ContextWithRecorder. A run that is neither, an agent attached to nothing or to a recorder of its own with no recorder on its context, is one the kit's session does not record: its renders are not written there, since a record with no run around it would be read as another run's, and its own memory_save, held for approval across a restart, is refused rather than based on a render the path cannot attribute to it. In the process that rendered it, its saves are based on its own render as any run's are.
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.
Handoffs ¶
A handoff between two agents is agentturn.Agent.SetConfig from one kit's config to the other's, over one transcript. To record both agents' runs in one session with each one's instructions as parts, either the host opens the recorder with a parts function that asks both kits, PartsFrom, and builds each kit under WithRecorder:
var triage, billing *agentkit.Kit rec, _, err := session.Start(ctx, store, h, session.WithInstructionsParts(agentkit.PartsFrom(&triage, &billing))) triage, err = agentkit.New(ctx, agentkit.WithRecorder(rec), ...) billing, err = agentkit.New(ctx, agentkit.WithRecorder(rec), ...)
or the receiver's kit opens a session of its own with WithSession on a header whose Base is the sender's leaf, a fork, and the agent is seeded with its Kit.AgentOptions. A kit built under WithRecorder(sender.Recorder()) alone records the receiver's instructions as one string, since that recorder's parts function is the sender's.
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.WithOptionalSkills(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) AddMCP ¶ added in v0.0.5
AddMCP starts an MCP server from a command line and offers its tools from the next turn on, as WithMCP does at New: the same stderr handling, elicitation when WithToolElicitor is set, and the label "mcp:#<n> <program>", numbered after the servers already given. It returns that label, which WithToolFilter, WithToolWrap, a Conflict and Kit.Tools use, and which Kit.RemoveMCP takes.
It is for a server the product connects once a session is under way: a command that adds one, a sign-in that makes one reachable. A turn already running keeps the tools it was offered.
The dial runs off the kit's lock, since it may wait on that sign-in, so Kit.Tools, Kit.RemoveMCP and Kit.Close answer meanwhile, and two servers added at once are dialed at once. Each takes its number before it dials, so the two are numbered apart. A server that is refused, or whose dial fails, gives its number back unless another was numbered meanwhile, so the labels are dense unless servers are added at once, when a refusal leaves a gap. The clash check runs when the dial ends, against what the kit offers then.
A tool whose name is taken by one the kit offers now is an error, as at New, and the server is closed: this is when the product can still rename it, with mcpclient.WithPrefix. A server added after Kit.Close, or to a kit built with no tool source, whose config has no ToolProvider to carry it, is an error too, and so is one whose dial Close ended: Close cancels a dial in flight, and a server that connected as the kit closed is closed again.
By hand it is a ToolProvider over a list of remotes the product appends to under a lock; see docs/manual.md.
func (*Kit) AddMCPTransport ¶ added in v0.0.5
func (k *Kit) AddMCPTransport(ctx context.Context, t sdk.Transport, opts ...mcpclient.Option) (string, error)
AddMCPTransport connects to an MCP server over t and offers its tools from the next turn on, as WithMCPTransport does at New. It is Kit.AddMCP for a transport the product built, an OAuth-protected server's among them.
func (*Kit) AgentOptions ¶ added in v0.0.2
AgentOptions are the options that seed an agent with the session at its leaf, session.AgentOptions read with the recorder's session.Recorder.ReadOptions: the transcript Kit.Transcript returns, the model each reasoning item on it came from, so a request to another model leaves the earlier model's reasoning out as a live agent does, and the calls pending there, so an approval after a restart runs a call again only when its tool says it may. The skill grants the task had are the engine's, and New made them again, under WithSkillGrants, from the session's records. It is nil for no session and under WithRecorder, whose owner seeds the agent, so a caller need not branch:
agent := agentturn.New(kit.Config(), kit.AgentOptions()...)
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: the one New discovered, or the last Kit.ReloadSkills did.
func (*Kit) Close ¶
Close releases what New opened, joining the errors: every MCP client, those Kit.AddMCP connected and those New dialed, closed together and each ending the calls in flight to its server with mcpclient.ErrClosed, as agenttool v0.0.15's close does, so Close returns within a few seconds and not when the calls finish. The errors are joined in reverse order of connection. A dial Kit.AddMCP has in flight is cancelled, and that AddMCP returns an error. 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, and Kit.Tools answers while it closes.
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. The kit has no run-end hook, so a front also calls agentpolicy.Engine.Forget with the run's ID for a run that ended with no pending call, as that method's doc asks: a nested call the hook deferred and the elicitor answered stays remembered otherwise.
func (*Kit) GrantScope ¶ added in v0.0.7
GrantScope names the grant scope the kit decides a run on ctx under: the conversation the run belongs to, which is where a skill read in it is granted, WithSkillGrants, and the only conversation whose calls the grant decides. It is the key agentpolicy.ContextWithGrantScope puts on the context of every call the kit's engine decides, and a product that calls the engine itself on a conversation's behalf, agentpolicy.Engine.Answers on a Resume, agentpolicy.Engine.GrantsFor to show the grants in force, or agentpolicy.Engine.RevokeScope when the conversation ends, puts it on the context it calls with:
ctx = agentpolicy.ContextWithGrantScope(ctx, kit.GrantScope(ctx))
A scope on ctx already, which a front puts there with agentpolicy.ContextWithGrantScope to name a conversation or a child agent of its own, is the scope. Otherwise it is the session of the recorder on the context, ContextWithRecorder; else the session the context names, session.ContextWithSessionID, as a front that records each conversation itself puts there, unless the kit records into a recorder another owns, WithRecorder, every session of which is one conversation, or the session is the kit's own; else the kit's own session. A run recorded nowhere that names no session belongs to one conversation, the kit's, whose scope is not empty so that its grants are not unscoped ones, which decide every conversation's calls.
The run of a child agent WithChildAgent offers has a scope of its own, under its conversation's, so it is not decided by what its parent read.
func (*Kit) LookupTool ¶ added in v0.0.2
LookupTool returns the tool of the given name in the union the kit offers outside any run, before the policy's filter: its own configuration, as of New and of the last Kit.Config tools resolved with a context that names no run, which is where a server Kit.AddMCP connected shows. A run's own list is Kit.LookupToolFor's, which is what the engine the kit builds reads, and what a product passes agentpolicy.WithToolsFor when it builds its own; LookupTool has the signature of agentpolicy.WithTools, for a product whose runs all see one list, and it is the fallback for a context with no run or one the kit has no union for.
It is safe on a nil kit, which has no tools.
func (*Kit) LookupToolFor ¶ added in v0.0.7
LookupToolFor returns the tool of the given name in the union the provider last offered the run whose context this is, before the policy's filter: the tool the loop runs for a call of that name in that run. It has the signature agentpolicy.WithToolsFor takes, and the engine WithPolicy builds is given it, so the batch hold reads a sibling's confinement, before the loop hands the engine that sibling's call, from the list its own run was offered, and agentpolicy.Engine.Answers finds the tool of a call cut off in a seeded transcript. A product that builds its own engine for WithEngine passes it the same way:
var kit *agentkit.Kit
engine, err := agentpolicy.Build(p, m, agentpolicy.WithToolsFor(func(ctx context.Context, name string) (agenttool.Tool, bool) {
return kit.LookupToolFor(ctx, name)
}))
kit, err = agentkit.New(ctx, agentkit.WithEngine(engine), ...)
A context outside any run, or of a run the kit has no union for, the kit keeps the last 1024 runs', reads what Kit.LookupTool answers, the kit's own configuration, and never another run's list. It is safe on a nil kit, which has no tools.
func (*Kit) MemoryManifest ¶
func (k *Kit) MemoryManifest() agentmemory.Manifest
MemoryManifest is what the last render put in the memory block, of whichever run rendered last, and the hash a product compares to record the render only when it moved. The kit records it itself when a session is configured. memory_save is based on its own run's render, and on this only for a call made outside any run.
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(_ context.Context, 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, one built from a render other than the last, or another agent's. 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(ctx context.Context, req openresponses.Request) ([]agentsession.InstructionPart, []agentsession.OmittedPart) {
return kit.PartsFor(ctx, req)
}))
kit, err = agentkit.New(ctx, agentkit.WithRecorder(rec), ...)
The recorder asks for every session it writes, a child run's among them, and a request whose instructions are not this kit's gets nil, so the child keeps the string unless PartsFrom names its kit too. 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. It is safe on a nil kit, which composes nothing.
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) RegrantSkills ¶ added in v0.0.4
RegrantSkills grants again, under WithSkillGrants, what the skill reads on sess's path granted and nothing revoked, as New does for a session it resumes, under the grant scope of sess's conversation, Kit.GrantScope, or under the scope ctx carries when the front names its conversations' scopes itself with agentpolicy.ContextWithGrantScope; the session does not record the scope, so such a front passes the one its runs use. It is for a front that resumes a conversation itself, with session.Resume under ContextWithRecorder, so a call held before a restart is approved with the tools its task had. ctx carries that conversation's recorder, ContextWithRecorder, which is where the verdicts the regrant records go and where a later revocation of the grants is written; the kit's own recorder serves when it writes sess. When the kit records its engine's verdicts, that is under WithPolicy, and neither writes sess, it returns ErrSkillGrantRecorder before it grants anything: the grants' verdicts and a later revocation would be recorded nowhere, and the next restart would find a session that says nothing about them. Under WithEngine the kit records no verdict, and a ctx without a recorder grants with none for the revocation. The grants are made silently and reported with SkillGrant.Replayed set; a read it will not grant again is reported too, with ErrSkillGrantChanged when the skill changed since the read and ErrSkillGrantUnrecorded when sess holds the read and no verdict of its grant, as a session a front recorded without ContextWithRecorder does. It does nothing without skill grants.
func (*Kit) ReloadSkills ¶ added in v0.0.6
ReloadSkills discovers the skills again, over the directories and sources New was given, and puts the catalogue it finds in place of the one in force: a skill written since, by the agent or anyone else, is listed and served, and one removed is not. It is for an agent that writes its own skills and uses one in the conversation it wrote it in.
The skill tool serves the new catalogue from the next call, in every run of every config the kit returned. The skills part of the instructions is the new catalogue's, and Kit.Omitted its omissions, from the next request of a kit that renders its instructions each request, under WithMemory or WithGuards; otherwise from the next run of an agent given the new Kit.Config, with SetConfig between runs, since a config is a value and the loop sends its instructions as they are. Either way the recorder writes the new part as a config delta, and the provider's prefix cache misses from that part on.
Grants already made stand, by the rules they were made with. A read after the reload grants by the new catalogue's rules, and a restart's replay passes over a read whose skill has changed since, reporting it with ErrSkillGrantChanged: the digest the replay compares covers the body the catalogue's tool serves, the skill's file list and each file's size, and the frontmatter, so an edit the reload picked up with no read after it, or a file the skill wrote into its own directory, ends the grant at the next restart. A read refused because the skill file is gone, renamed or no longer parses is reported with agentskill.ErrSkillChanged, and this is the call that answers it.
It returns an error, and changes nothing, when discovery fails, when New found no skill source and so offers no skill tool, and when the new catalogue would take the instructions past WithInstructionBudget with the parts the kit cannot bound. It does nothing without skills.
func (*Kit) RemoveMCP ¶ added in v0.0.5
RemoveMCP closes a server Kit.AddMCP or Kit.AddMCPTransport connected, by the label it returned, and stops offering its tools from the next turn on. A label that names no added server, one New dialed among them, is an error.
The server leaves the kit's lists at once and is closed after, off the lock, so Kit.Tools and the other MCP methods answer while RemoveMCP closes it. The close ends every call in flight to the server with mcpclient.ErrClosed and returns within a few seconds, as agenttool v0.0.15's does; it does not wait for the calls to finish. With WithToolElicitor set it also waits, for that long at most, for the requests telling the server its open questions were cancelled.
func (*Kit) RevokeSkillGrants ¶
RevokeSkillGrants revokes every grant a skill's read made through WithSkillGrants in the conversation of ctx, Kit.GrantScope, and those of the child agents run under it, 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, and the end of a conversation is also when the kit may forget what it kept of its grants; WithSkillGrantScope revokes when each message arrives. Another conversation's grants stay. 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 Kit.AgentOptions seeds an agent with it.
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. A server Kit.AddMCP connected is, with the tools it listed when it was added, until Kit.RemoveMCP removes it.
func (*Kit) Transcript ¶
func (k *Kit) Transcript() agentturn.Transcript
Transcript is the conversation in force at the session's leaf, session.Transcript: its context's items with the ones the filter kept from the model put back where the loop held them, which is what an agent continuing it must be seeded with. It is empty for a new session WithSession started, the prefix up to the header's Base for a fork WithSession started, and empty for no session at all or under WithRecorder. Kit.AgentOptions carries it with the calls pending at the leaf, which an agent resuming a session needs as well.
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, the part ID an entry would have had,
// [agentmemory.PartID], 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. Decisions fold deny over ask over allow, as agentturn.ChainBeforeToolCall describes.
With WithPolicy the hooks are folded into the engine's own decision through agentpolicy.WithHooks, so a hook that asks about a call holds the call's siblings with it, and a hook that blocks a call the policy asked about leaves nothing held. The engine reads a call's siblings to decide whether to hold it, so a hook may be called for a call before the loop hands it that call, and more than once: it must decide a call the same way each time it is asked, reading the call from its agentturn.ToolCallInfo and not from the context, which then carries the call being decided rather than the sibling. A hook that asks a person is asked before the batch runs. Without a policy, or with WithEngine, they are chained after the engine's hook.
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 the parent's run has a recorder, the kit's or one a front put on its context with ContextWithRecorder, the child's run is recorded into it, live and linked to the parent's, because the kit binds childagent.WithObserver to that recorder, and the child runs under its 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. Under WithSkillGrants the run context also carries a grant scope of the child's own, under its conversation's, agentpolicy.ContextWithGrantScope: the child's session ID, or the call's ID and a number without a recording. A skill its parent read grants the child's calls nothing, and one the child reads is the child's alone, ended with its conversation's next message; see WithSkillGrants. Without skill grants the kit puts no scope there, and a scope the product put on the host's context is the child's. 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, and a run context of the product's that does not give the child a grant scope leaves it under its parent's.
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. Summary requests name the agent's model, compact.WithModel ahead of opts.
The kit records the fold through a compact.WithOnFold of its own, ahead of opts, whether or not there is a session, since the recorder may arrive on a run's context, ContextWithRecorder, after New. compact.WithOnFold adds a callback, so one in opts is called too, after the fold is recorded. WithFoldObserver is the kit's own way to hear of a fold, with or without a session.
The summary request is sent under the agent's reasoning, the WithReasoning or WithRequest one, through a compact.WithRequest of the kit's ahead of opts: a thinking model left at its server's default reasons through the summary's cap and answers no text, which fails every fold. So for an agent at effort low or above the summary is asked at that effort, and a thinking model spends part of the summary's cap, half the budget, reasoning before it writes: the fold succeeds, later and thinner, and nothing reports it. A product whose agent thinks passes its own, which replaces the kit's since compact.WithRequest is one function:
agentkit.WithCompaction(budget, compact.WithRequest(func(r *openresponses.Request) {
r.Reasoning = openresponses.ReasoningConfig{Effort: openresponses.ReasoningEffortNone}
}))
or the lowest effort its provider accepts. The kit does not pick a lower effort itself because it does not know that floor: several reasoning models refuse none, and a fold that succeeds thinner today would then fail every time. Under WithCompactionModel the kit passes no reasoning at all, since the product chose that model knowing it and a configuration meant for the agent's model may be refused by, or wasted on, another; a compact.WithRequest in opts sets it there too.
With a session New resumed, the last fold that failed on its path, session.CompactOptions, is passed after opts, so a restart does not ask again for a summary that failed before it. Under WithRecorder or ContextWithRecorder the kit does not know the session when it builds the fold, and the product passes session.CompactOptions in opts.
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. The summary request carries no reasoning unless a compact.WithRequest in WithCompaction's options sets one; see there.
func WithCompactor ¶
WithCompactor folds through a compactor the product built, a provider's compaction endpoint, when the transcript grows past budget tokens. It is WithCompaction with the fold made elsewhere, and takes the same budget and the same default: every request is sent under the agent's model name, compact.WithModel ahead of opts, so a compact.WithModel in opts still wins. The fold is recorded, and WithFoldObserver told, as under WithCompaction, and a compact.WithOnFold in opts is called after the recording as there.
A compactor and a local summary are two ways to fold, so New refuses WithCompactor beside WithCompaction or WithCompactionModel.
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 options, so its verdicts are recorded only if the product's own observer records them, it reads siblings' tools only if the product passed agentpolicy.WithToolsFor with Kit.LookupToolFor, and the WithBeforeToolCall hooks are chained after it rather than folded into it; a product that wants them held with their siblings passes them to the engine with agentpolicy.WithHooks instead.
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 WithFoldObserver ¶ added in v0.0.2
WithFoldObserver is told of each fold compaction makes, after the recorder has written it when a session is recorded. It hears a fold that failed as well as one that folded: since agentturn v0.0.13 a summary no smaller than what it folds, one cut short, or one with no text fails the fold quietly, with compact.Fold Err set and no Summary, and the transcript is sent whole. A front that says the model has forgotten a detail says it only for a fold whose Err is nil. It has no effect without WithCompaction or WithCompactor. A later call replaces an earlier one.
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.
New runs the same per-part pass over its own render, so agentturn.Config.Instructions is the text the guards leave, and the config entry a recorder settles at a run's start, before any hook has run, carries nothing a guard kept from the model. A guard that refuses a part there is an error from New. That pass has no observer, since there is no run to record under; the first turn's pass reports the same verdicts.
A refusal names the part, "instructions/<id>", in New's error and in a turn's, so a line saved to memory and the same line in AGENTS.md are told apart.
The chain's observer records each verdict that blocked or gave a reason into the run's recorder, when there is one, 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.
The server's stderr goes to WithMCPStderr, and nowhere without it; either way the kit keeps its last two kilobytes, and an error from New connecting to the server ends with them, since a server that fails at start says why there. The label a Conflict, WithToolFilter and Kit.Tools give its tools is "mcp:#<n> <program>", the command's first word: its arguments are where a credential is put, and the label is logged.
With WithToolElicitor set, the client is dialed with mcpclient.WithElicitation ahead of opts, so a question the server asks mid-call reaches that elicitor under the call that asked. Every server may then ask the user, a URL to visit among them; a product that trusts one server less passes that server an ElicitationHandler through mcpclient.WithClientOptions, which takes precedence.
The error's stderr tail is the server's own words and may carry what the server printed, a token in a failed request among them; a product that logs errors from New logs it.
The server is dialed once, at New, and every run the kit serves calls it over that one connection, so whatever identity the connection carries is the kit's, not a conversation's or a user's. A server authorized by OAuth acts as whoever authorized it for every conversation: the token belongs to the connection, and the server refuses another user's token on it. A front serving several users with such a server gives each user a kit of their own, and keeps each user's grant with mcpclient.StoreTokens; see WithMCPTransport. Kit.AddMCP connects a server after New, the same way.
func WithMCPStderr ¶ added in v0.0.2
WithMCPStderr sends the stderr of every server WithMCP starts to w, os.Stderr for a command-line product, a log for one with a screen of its own. The servers' writes are serialised, so w need not be safe for concurrent use. A write to w that fails is dropped rather than stopping the server. A later call replaces an earlier one.
func WithMCPTransport ¶
WithMCPTransport connects to an MCP server over the given transport. Its label is "mcp:#<n>" and what the transport reaches, without the places credentials go: a *mcp.CommandTransport's program, an HTTP transport's scheme and host, or the transport's type. The command's stderr is the product's to set, since it built the command. WithToolElicitor binds elicitation as for WithMCP. The connection's identity is the kit's, as for WithMCP: a transport carrying one user's credentials serves every conversation the kit serves as that user.
A server behind OAuth takes a *mcp.StreamableClientTransport whose OAuthHandler is the SDK's authorization-code handler. Its grant lives in memory unless mcpclient.StoreTokens keeps it in a mcpclient.TokenStore, so a restart does not send the user to consent again. The store is keyed by the endpoint and a subject the host names, which is the user the kit is built for:
cfg := &auth.AuthorizationCodeHandlerConfig{ /* client, redirect, fetcher */ }
key := mcpclient.TokenKey{Endpoint: endpoint, Subject: userID}
if err := mcpclient.StoreTokens(ctx, cfg, store, key, logSaveError); err != nil {
return err
}
h, err := auth.NewAuthorizationCodeHandler(cfg)
kit, err := agentkit.New(ctx,
agentkit.WithMCPTransport(&mcp.StreamableClientTransport{Endpoint: endpoint, OAuthHandler: h}),
...)
The handler, the fetcher and the store are the product's: the kit dials the transport it is given and adds nothing to its authorization.
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 group of parts, 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 as PartMemoryUsage, since the tools are offered whenever the block is.
A render WithInstructionBudget leaves no room for drops the block and the paragraph, and that request is not offered memory_save, memory_patch or memory_forget: the model would be writing over entries it was never shown, with no word on the tools. A write the model makes anyway in that run is refused, and so is one a policy held and a person approved, which runs in a Resume: the render kept for the call says the block was dropped, and after a restart the manifest recorded at the call does, where it shows no entry and lists one the block held among the omitted. memory_search stays offered, and Kit.Omitted lists every entry the drop left out. The block, and the writes, come back on the first render that fits.
The block is recorded as one part per piece agentmemory.RenderParts returns, the title, each scope's heading, each entry and the summary line, so a write to one entry is recorded as that entry's part and the summary rather than the whole block.
memory_save is given agentmemory.WithRendered with the render its own run's model was shown, ahead of WithMemoryTools: a save is based on the entry the block showed the model, so a write another session made after the render is reported by agentmemory.LostUpdates and the model is told, rather than silently discarded. The render is looked up by agentturn.RunIDFromContext, so concurrent runs off one kit each save over their own. A call held for approval keeps its run's render, kept by a BeforeToolCall hook of the kit's ahead of whatever holds it, for the Resume that runs it; after a restart, or once the kit has dropped the run, a call is based on the manifest in force on its session's path where the model made it. A call in a run for which the kit has none of these is refused, and the model told to look at the entry again, rather than based on another run's render. A WithMemoryTools option of the same kind replaces the kit's.
With a session, each render that differs from the manifest in force on the session's path is recorded under agentmemory.ManifestNS, as agentmemory.Manifest.RecordSince the one in force: whole in a session that holds none, and a delta otherwise, in the session New opened, one on a run's context and a child's alike.
func WithMemoryReadScopes ¶ added in v0.0.2
func WithMemoryReadScopes(scopes ...agentmemory.Scope) Option
WithMemoryReadScopes renders scopes the model may read and not write, such as project rules the product keeps in memory for the model to follow and never edit. A scope WithMemory also names is rendered in its place there and read-only; one it does not is rendered after its scopes. The memory tools are built over the writable scopes alone, with agentmemory.WithReadScopes naming these, so memory_search reaches what the block omitted from them and the writers refuse them.
At least one scope must stay writable, since agentmemory's tools cannot be built over none; New fails otherwise. It has no effect without WithMemory. Several calls accumulate.
func WithMemoryRender ¶
func WithMemoryRender(opts ...agentmemory.RenderOption) Option
WithMemoryRender passes options to agentmemory.RenderParts, 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, after the kit's own agentmemory.WithRendered and, under WithMemoryReadScopes, agentmemory.WithReadScopes. A read scope given here that WithMemory also names is an error from New; name it in WithMemoryReadScopes instead.
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 WithOptionalSkills ¶ added in v0.0.3
WithOptionalSkills is WithSkills for a directory the user may not have made, such as ~/.dex/skills: one that does not exist is passed over, and one that exists and cannot be read is still an error from New. The directories take their place among WithSkills' in the order the options were given, since that order decides which of two skills of one name shadows the other.
Keep WithSkills for the directory the product configures, where a misspelling that silently offers no skills is the failure to refuse. A directory passed over is not reported, since it held nothing to leave out; a product that shows it stats the path itself. When every skill directory was optional and none is there, and no WithSkillSources were given, there is no catalogue: no skills part and no skill tool, as if skills were not configured.
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 memory/user memory:summary memory:usage 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.
The kit gives the engine three options ahead of opts:
- agentpolicy.WithToolsFor with Kit.LookupToolFor, the union the decision's own run was offered at its last turn, so a call ahead of a confined command in one batch is not held for a sibling the engine could not see, and runs whose tool lists differ each read their own. The union does not exist when the engine is built, so a product cannot hand it in; an agentpolicy.WithTools or WithToolsFor in opts replaces the kit's.
- agentpolicy.WithObserver: it records each verdict into the run's recorder, the one ContextWithRecorder put on its context or the kit's own, 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.
- agentpolicy.WithHooks with the WithBeforeToolCall hooks, which the engine folds into its decision before the batch hold.
func WithReasoning ¶
func WithReasoning(r openresponses.ReasoningConfig) Option
WithReasoning sets the reasoning effort and summary of every request, the summary request a WithCompaction fold sends among them.
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, and Kit.AgentOptions seeds an agent with it: session.AgentOptions, the transcript with the items the filter kept from the model put back, which Context().Items leaves out, the model each reasoning item came from, so a request to another model leaves the earlier one's out, and the calls pending at the leaf. The recorder takes the kit's parts, and every agent built from the kit is attached to it or prompted under ContextWithRecorder, as under WithSession. Under WithSkillGrants, the grants the session's skill reads made are granted again, under the session's conversation or, when the front scopes its conversations itself, the scope New's context carries, which is the one its runs pass agentpolicy.ContextWithGrantScope, since the session does not record it; and under WithCompaction the fold backs off from the last fold that failed on the session's path.
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.
Every agent built from the kit is attached to it, or prompted under ContextWithRecorder with a recorder of its own. A run that is neither is one the session does not record, and the kit writes none of its memory renders there: a record with no run around it would be read as another run's. Such a run's memory_save, held for approval and approved after a restart, is refused, since no session holds the render it was composed from; in the process that rendered it, it is based on that render as any run's is.
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, a skill whose allowed-tools would not parse, a read the catalogue's tool refused because the skill file is gone, renamed or no longer parses, with Err wrapping agentskill.ErrSkillChanged, and, with SkillGrant.Replayed set, what a restart granted again and the read it will not grant again, with Err wrapping ErrSkillGrantChanged when the skill changed since the read and ErrSkillGrantUnrecorded when the session records no verdict of the read's grant. A grant widens what the agent may do, so a front that shows the user the policy in force wants to see it happen; and a front reloads the skills, Kit.ReloadSkills, on a report wrapping agentskill.ErrSkillChanged or with SkillGrant.FrontmatterChanged set, since the model is told to discover the skills again and cannot.
func WithSkillGrantScope ¶
func WithSkillGrantScope() Option
WithSkillGrantScope revokes every grant a skill's read made when a message from the user arrives, as Claude Code clears allowed-tools when the next message arrives: a later request that wants the tools reads the skill again. The message may start a run, follow the run's answer through agentturn.Agent.FollowUp, be steered in between turns, come with a developer note after it, or arrive in another agent's run, as it does when the kit's agent handed the conversation to another kit's and is handed it back; each revokes. A run that Resume starts after an approval, or that Continue starts, is the same task going on and keeps them, after a restart too, since the scope's revocations are in the session's journal.
It is a agentturn.Config.BeforeTurn hook, ahead of the product's own. Each grant is bound to the user message in force when the read made it: the count of user messages in the turn's transcript and a digest of the last one, its role and the JSON of its content. The hook calls Kit.RevokeSkillGrants on every turn whose transcript holds more user messages than a grant's count, or whose last user message is not the one it digested, and on every turn whose transcript's tail, back to the last item the model or a tool produced, holds a user message, which catches a grant made in no run and so bound to nothing. A grant New or Kit.RegrantSkills grants again is bound to the message the transcript the agent is seeded with ends under, session.Transcript, the items Kit.AgentOptions gives agentturn.New, so the restart's first turn keeps it. A compaction ends nothing by itself, in the run, whose turns keep the message the fold summarised, or across a restart, where the fold's summary stands in for it in the seeded transcript and the grant is bound to that. A steer delivered before an output is one more user message, so it revokes on the turn that follows, which the tail test alone left to agentturn.TurnStartInfo.
Under the scope the model is told, in a developer note at the turn that ended a grant, which skills' tools ended and that a read restores them; and a call that only an ended grant allowed is refused with a reason naming the skill and the tool that reads it again, until the skill is read again. The engine would have deferred the call to a reviewer, whose refusal says nothing of the skill. The refusal is the first hook the kit folds into the engine it builds under WithPolicy, agentpolicy.WithHooks, ahead of the product's WithBeforeToolCall hooks, so the engine's fold takes the Block, a sibling in the same batch is decided beside it rather than held for a question nobody is asked, and the engine records the decision as the call's verdict under agentpolicy.VerdictNS; it is not reported through WithSkillGrantReport. There is no refusal under WithEngine. A call's subjects are the tool's agentpolicy.Subjects split under WithPolicy's matchers, and each must be covered by an ended rule, bare or with a specifier the tool's matcher matches; an ended grant of a skill the catalogue no longer lists, after Kit.ReloadSkills, covers nothing.
Whether the engine would have allowed the call is agentpolicy.Engine.Would's answer, which reads the policy, the grants still in force, the call's confinement and the hooks folded into the engine as the decision does, under the call's grant scope. The kit refuses when the engine would ask, which is what the grant answered. A call it would allow needs no grant, and one it denies is denied by a rule a grant never beats, so both are the engine's. The product's WithBeforeToolCall hooks are among those folded, and are called once more for the question, so a hook must decide a call the same way however often it is asked. With such hooks, or agentpolicy options that may add some, the kit refuses only when an ask rule is behind the verdict, since a hook's question would be asked whatever a grant did and the verdict does not say whose it is.
Only the sources the kit granted are revoked; a product's own agentpolicy.Engine.GrantSet calls are left alone. A message ends the grants of its own conversation and of the child agents run under it, and no other conversation's. Two agents sharing a conversation 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 listed name, agentskill.Skill.ListedName, with the skill's location as its path, which is untrusted and therefore contributes its deny and ask rules alone. A source function should key its name on ListedName too: a root "deploy" and a qualified "apps/web:deploy" share Name, and two skills given one source name share one grant.
A grant lasts until something revokes it, and a restart does not: with a session New opened, New replays the session's journal and grants again each skill read no recorded revocation ended, so an approval after a restart goes on under the grants the task had. A restart may narrow a grant and never widens one: what is granted again is the rules the journal says the read was granted, as the engine recorded them after agentpolicy.WithAliases expanded them, when the read recorded the digest of the frontmatter it was granted from and the skill has it still; a skill whose instructions or frontmatter changed since the read is not granted at all; under WithEngine, where the kit records no verdict, it is the catalogue's rules as they stand. The replay records no verdict, since the ones it repeats are on the path, and is reported with SkillGrant.Replayed set. A front that resumes a conversation itself calls Kit.RegrantSkills.
A grant belongs to the conversation that read the skill. The engine keeps it under that conversation's grant scope, Kit.GrantScope, the session the run records into, agentpolicy.ContextWithGrantScope, and decides only the calls made under it, so one kit serves any number of conversations and a skill read in one grants nothing in another. A child agent WithChildAgent offers has a scope of its own, under its conversation's, which the kit makes and ends, with agentpolicy.Engine.RevokeScope, with the conversation's. Nothing ends a conversation for the kit, so a front that does calls Kit.RevokeSkillGrants with the conversation's context, which is also when the kit forgets what it kept of its grants; the engine holds the sets of every conversation not so ended and consults them for every decision, and WithSkillGrantScope ends a conversation's at its next message. The scope comes from the context, so a call with a bare one revokes the kit's no-session scope and no conversation's. The verdicts and reports of every conversation reach the one engine and the one WithSkillGrantReport function, SkillGrant.Scope saying whose. WithSkillGrantScope revokes every skill's grant when the user's next message arrives, 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.
A source function that trusts a skill by its name or its directory trusts whatever is written there next, an agent's own skills included. To trust what a person approved, set Trusted only when agentskill.Skill.FrontmatterSHA256, the digest of the frontmatter the rules are parsed from, is one they approved, and, to approve the text too, the SHA-256 of agentskill.Skill.Instructions. The digest of the instructions alone does not cover allowed-tools. The default source names the frontmatter's digest as its Hash, so every verdict about a grant says what it was built from; a source function does the same by setting Hash.
A grant lives in the engine, so New over a resumed session and Kit.RegrantSkills grant again what the session's reads left in force, and only for a skill that is what the model read: the digest of the instructions the catalogue's tool served, which agentskill ends with the skill's file list and each file's size, and the digest of its frontmatter. So a skill that writes into its own directory, a draft changelog or a log, is not granted again after a restart, nor is one whose allowed-tools were edited or whose body Kit.ReloadSkills picked up with no read after; the front hears each through WithSkillGrantReport, with ErrSkillGrantChanged, and a read of the skill grants by the skill as it is now.
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 recorder, the kit's or the one on the run's context, the kit puts that 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.
An MCP client offers the server elicitation only when dialed with mcpclient.WithElicitation, so with this option set the kit dials every server WithMCP and WithMCPTransport name with it, ahead of their own options.
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', those Kit.AddMCP connected among them, 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 recorder, the kit's or one a front put on the run's context with ContextWithRecorder, 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, by the name the catalogue lists
// it under, [agentskill.Skill.ListedName]: "deploy" for a root
// skill and "apps/web:deploy" for a qualified one that shares its
// name, so the two are two grants and two reports.
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 nothing was granted: because the skill's
// allowed-tools would not parse; because the catalogue's tool
// refused the read, the skill file gone, renamed or unparseable
// since discovery, [agentskill.ErrSkillChanged], a report a front
// reloads on with [Kit.ReloadSkills]; or, with Replayed set, because
// a restart would not grant a read again: the skill changed since
// the read, [ErrSkillGrantChanged], the session records no verdict
// of the read's grant, [ErrSkillGrantUnrecorded], or none of the
// rules the session recorded for the read is among the skill's now.
Err error
// Scope is the grant scope the read was made in, the conversation
// whose calls the grant decides, [Kit.GrantScope]. A kit that serves
// many conversations reports every one's reads to one function, and
// this says whose.
Scope string
// Replayed is true for a grant made again from a session's records,
// at [New] or by [Kit.RegrantSkills], rather than for a read the
// model made just now.
Replayed bool
// FrontmatterChanged is the read's [agentskill.Read]
// FrontmatterChanged: the skill file on disk has other frontmatter
// than the catalogue was built from, so the grant is the rules as
// loaded, and [Kit.ReloadSkills] brings in the new ones.
FrontmatterChanged bool
}
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, and once per live read New or Kit.RegrantSkills granted again, with Replayed set.
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.