agentkit

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: MIT Imports: 23 Imported by: 0

README

agentkit

Assembly for the nine libraries: one call that turns a product's choices into an agentturn.Config, and the one module in the workspace that imports every other. It owns the composition no single library can own and nothing else.

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", filepath.Join(home, ".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 — and reports it once. Config() is then pure and can be called per run. Close() releases what New opened.

Attach is the one step a Config cannot carry. The recorder has to subscribe to the agent, and the agent does not exist until after Config() — so Attach subscribes it and returns the unsubscribe, which is why the call is doubled: kit.Attach(agent) runs at the defer and subscribes, and the function it returns runs at scope exit and unsubscribes. A session configured but never attached records nothing and says nothing about it, so attach where the agent is built. Without a session it is a no-op, so the line does not need a branch around it.

Every method, and every config Config() returns, is safe to use from any goroutine. The configs are not independent of each other: concurrent runs off one kit share its memory state, which is the right sharing for two runs of one agent and the wrong sharing for two agents. Give each agent its own kit; the Kit doc says exactly what is shared.

The rule

Config() returns a plain agentturn.Config. Every field it sets 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.

That rule is the whole design. It means the easy path and the manual path are the same path, so the kit never becomes load-bearing: a product that outgrows one of its decisions replaces that decision without leaving the kit, and a product that outgrows all of them deletes the import and keeps working. A kit that hid one seam would be a framework, and the stack already has a better answer than a framework.

The test of the rule is mechanical. docs/manual.md names, for every field of agentturn.Config the kit sets, the exported call a product would write instead; TestEveryFieldTheKitSetsIsDocumented fails if the kit sets a field that document does not name, and TestTheManualPathIsTheSamePath writes the manual side out for a full configuration and compares it to the kit's.

What it owns

Three things, each because no library can own it without importing its siblings.

1. Instruction assembly

agentsmd.Render, agentskill.Catalog.Prompt + Usage, and agentmemory.Render + Usage each produce a block of instruction text; each Usage paragraph is joined with its block exactly when the tool it describes is offered. Joining them with "\n\n" is the whole composition, and three problems follow from nobody owning the join: the order is a policy nobody states, the session's instructions_parts record has a shape and no writer, and every layer reports its omissions to nobody.

So the kit builds []Part — which is agentsession.InstructionPart, not a type of its own — in a stated order, under a total budget, and returns both the joined text for Config.Instructions and the parts and the omissions for a recorder. A session the kit opens takes them through session.WithInstructionsParts(kit.PartsFor), so a memory write is recorded as a change to the memory part rather than the whole prompt again. The input guards run over each part before the join, so a part guard.Redact rewrote is recorded as it was sent.

# part id source why here
1 product the product's own prompt the frame everything else refines
2 skills agentskill a catalogue, not an instruction
3 memory agentmemory what the agent knows, before what the repo says
4 agentsmd agentsmd the repository has the last word

Every position is a decision a product reverses with WithOrder, and every one of them is argued in docs/ordering.md rather than asserted in code.

2. Hook composition

agentturn.Config has one field per hook and more than one layer wants each; assigning one twice keeps the second and loses the first with no error and no sign. agentturn's Chain* functions exist because that contest was silent, and the kit is where they get called, in one order:

field order
BeforeModelCall memory re-render, then the guards over each part and over the whole request, then the product's
BeforeToolCall the policy engine, then the product's
OutputGuard the guards, then the product's
ShouldStopAfterTurn the policy's, then the product's
BeforeTurn the skill grants' revoke under WithSkillGrantScope, then the product's
Transform the product's, then compact, with WithOnFold bound to the recorder when there is one

With a session, the engine's verdicts and the guards' are recorded under agentpolicy.VerdictNS, so the record says which rule held a call and not only that the policy did; WithVerdictObserver sees the same verdicts.

3. The tool set

A run's tools come from the product, agentskill.Catalog.Tool(), agentmemory.Tools, mcpclient, and agentturn/tools/agent for a child. Five sources, one namespace, and nothing below the kit detects a collision: two tools with one name reach the model as two entries and the loop dispatches whichever the set returns first.

The kit unions them in a stable order — product tools, then child agents and other deferred sources, then skills, then memory, then MCP, then any provider the product gave — refuses a duplicate present at New with a Conflict naming both sources, drops the later of one that appears at turn time and tells WithToolConflict, and puts agentpolicy's ToolProvider in front of the result so a tool a bare deny rule removes is never offered. Every source carries a label, so a conflict says which two to fix.

WithToolFilter is the other thing a union can do that its parts cannot: it sees each tool with the label of the source that produced it, which is how a product takes five tools from a server offering forty. It runs before the duplicate check, so dropping one of two tools claiming a name resolves the collision rather than reporting it. WithToolWrap is the same seam for replacing a tool, a replay or a logger over every tool, inside the kit's own wrapper, and Kit.Tools() lists each tool with its label once, which is how a product names the libraries' tools to a policy whose default asks. The kit does not allow them itself: whether a library's tool runs unasked is a decision.

What it also exposes

A front needs things a Config cannot carry: kit.Engine() for Deferred and Release, kit.Recorder() and kit.SessionID(), kit.Catalog(), kit.Tools(), and kit.MemoryManifest() for the hash that says whether the render moved.

A front that asks a person about a held call says so when it answers, or the record cannot tell the approval from one a script gave:

answers, err := kit.Engine().Release(ctx, end,
	agentturn.Approve(callID).WithBy(agentpolicy.ByHuman))
if err != nil {
	return err
}
end, err = agent.Resume(ctx, answers...)

A host that wants its own run's memory writes to name its session puts the ID on the context it prompts with, agentmemory.WithSession(ctx, kit.SessionID()); the kit cannot, since that context is the host's. A child agent's run is the kit's, and WithChildAgent does it there.

A kit built where a recorder already exists, under an evaluation runner or inside a parent's WithDeferredTools, takes it with WithRecorder(rec): everything the kit binds to a session it binds to that recorder, and it opens and attaches nothing.

WithSkillGrants is the one place a skill's allowed-tools meets a policy: reading a skill grants its rules to the engine through agentpolicy.Engine.GrantSet. It is off by default and, when on, attributes the grant to an untrusted source unless the caller's own source function says otherwise, so a skill widens what the agent may do only when the product has said it trusts the tree the skill came from. WithSkillGrantScope ends each grant when a new message starts a run, as Claude Code clears allowed-tools at the next message, and kit.RevokeSkillGrants(ctx) ends them when a front says; a skill read again is granted again.

Composing with other agents

agentturn's composition story is a model, a tool, a peer and an in-process agent. The kit has options for three of them and, by design, none for a peer — because a peer does not need one. Config() returns a plain agentturn.Config, so a remote peer is an ordinary tool through WithTools, and serving the agent as a peer is fronta2a.New(kit.Config()). If a peer needed an option, the kit would have a seam.

WithChildAgent is the in-process one, and it exists for a different reason than a seam: it binds the child's observer and run context to the session recorder, which only the kit has, because only the kit opened the session, so the child's run is recorded into its own linked session and what its tools write, a memory among them, names that session.

docs/composition.md has both directions, and the child-agent case where the recorder the kit made has to reach the child. The a2a code is the nested module examples/a2a, compiled by CI: the a2a packages pull in a gRPC stack that would take a build from 64 packages to 201, so they stay out of the root and out of every consumer that never speaks a2a.

What it is not

  • Not a UI, a server, a launcher or a deployment tool. agenttui is the terminal client and it does not import this module.
  • Not a new tool type, model type, store type or hook type. Everything it hands to the loop came from a library's exported constructor.
  • Not orchestration. The composition story stays agentturn's: a model, a tool, a peer, an in-process agent.
  • Not a place for defaults that hide a decision. An option that changes behaviour is argued in docs/ or it does not exist.

Modules

library version
openresponses v0.0.12
agenttool, agenttool/mcpclient v0.0.9
agentturn, agentturn/session v0.0.10
agentsession v0.0.9
agentsmd v0.0.2
agentskill v0.0.6
agentmemory v0.0.5
agentpolicy v0.0.6

Every sibling is required at a released version with no replace, and make no-replace enforces it: the kit is the module that proves the released libraries compose, so it has to build against the versions a consumer would fetch. TestTheREADMEVersionTableIsGoMod holds the table above to go.mod, so a bump that forgets the table fails.

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

Examples

Constants

View Source
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.

View Source
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.

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

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.

func (Conflict) Error

func (c Conflict) Error() string

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

func New(ctx context.Context, opts ...Option) (*Kit, error)

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)
}

func (*Kit) Attach

func (k *Kit) Attach(a *agentturn.Agent) func()

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

func (k *Kit) Close() error

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

func (k *Kit) Config() agentturn.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

func (k *Kit) Omitted() []Omission

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

func (k *Kit) Parts() []Part

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

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

func (k *Kit) Recorder() *session.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

func (k *Kit) RevokeSkillGrants(ctx context.Context) int

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) SessionID

func (k *Kit) SessionID() string

SessionID is the ID of the session, or "" when there is none.

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.

func (Omission) String

func (o Omission) String() string

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

func WithAgentsMD(path string, opts agentsmd.Options) Option

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

func WithBeforeModelCall(fn func(context.Context, *openresponses.Request) error) Option

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

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

func WithCompaction(budget int, opts ...compact.Option) Option

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

func WithCompactor(c compact.Compactor, opts ...compact.Option) Option

WithCompactor folds with a compactor the product built.

func WithDeferredTools

func WithDeferredTools(fn func(*Kit) []agenttool.Tool) Option

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

func WithGuards(gs ...guard.Guard) Option

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

func WithInstructionBudget(n int64) Option

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

func WithInstructions(text string) Option

WithInstructions sets the product's own prompt: the first instructions part, PartProduct, the frame every other part refines.

func WithMCP

func WithMCP(command string, opts ...mcpclient.Option) Option

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

func WithMCPTransport(t sdk.Transport, opts ...mcpclient.Option) Option

WithMCPTransport connects to an MCP server over the given transport.

func WithMaxParallelTools

func WithMaxParallelTools(n int) Option

WithMaxParallelTools bounds a parallel batch to n calls at a time. Zero, the default, means agenttool.DefaultMaxParallel.

func WithMaxTurns

func WithMaxTurns(n int) Option

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

func WithModel(m agentturn.Model, name string) Option

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

func WithName(name, description string) Option

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

func WithOrder(ids ...string) Option

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

func WithRecorder(rec *session.Recorder) Option

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

func WithRequestExtra(extra map[string]any) Option

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

func WithResumedSession(store agentsession.Store, id string, opts ...session.Option) Option

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

func WithRetry(r agentturn.Retry) Option

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

func WithShouldStopAfterTurn(fn func(context.Context, agentturn.TurnInfo) (bool, error)) Option

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

func WithSkills(dirs ...string) Option

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

func WithToolConflict(fn func(Conflict)) Option

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

func WithToolElicitor(by string, fn agenttool.Elicitor) Option

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

func WithToolFilter(fn func(source string, t agenttool.Tool) bool) Option

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

func WithToolProvider(fn func(context.Context) []agenttool.Tool) Option

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

func WithToolWrap(fn func(source string, t agenttool.Tool) agenttool.Tool) Option

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

func WithTools(ts ...agenttool.Tool) Option

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

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

type ToolOrigin struct {
	Name   string
	Source string
}

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.

Jump to

Keyboard shortcuts

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