Help us shape the future of Go! Take the 2026 Go Developer Survey

agentsmd

package module
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 10 Imported by: 1

README

agentsmd

The AGENTS.md convention for Go agents: the instruction files that apply at a path, found from its directory up to a root, one per directory, nearest last, and rendered into the text a product puts in its instructions. The nearest file is the last one written and wins. The module imports the standard library alone.

res, err := agentsmd.Chain(cwd, agentsmd.Options{
	Names:  []string{"AGENTS.override.md", "AGENTS.md"},
	Root:   repoRoot,
	Extra:  []string{filepath.Join(home, ".dax", "AGENTS.md")},
	Budget: 32 << 10,
})
if err != nil {
	return err
}
for _, o := range res.Omitted {
	log.Printf("%s (%d bytes): %s", o.Path, o.Size, o.Reason) // shadowed or over budget
}
cfg.Instructions = agentsmd.Render(res.Files)

Chain returns Extra first, then walks from the path's directory to Root, takes in each directory the first of Names that the directory lists under exactly that spelling, and returns those files farthest first and nearest last, so that later text refines earlier text. Extra comes first because a file outside the tree, such as the user's own in their home directory, is the farthest of the lot: Codex reads ~/.codex/AGENTS.md before the repository's files for that reason, and the repository's nearest file still has the last word. Budget caps the total and is spent in that same order: the first file that would exceed it ends the chain, nothing is cut short, and running out is not an error. RenderedBudget caps Render's output the same way, wrapper and paths included, for a product that bounds its prompt rather than the files. Budget and MaxBytes count the bytes a file reads, not the size its stat reported, so an FS that expands a file as it reads it is held to them too. Every file found and left out, shadowed by a preferred name or over budget, is in Result.Omitted with its size and its path, so a product can tell the user and a session can record what the model was not given as well as what it was.

Render wraps the files as pi renders <project_context>: one <project_instructions path="..."> per file, in order. The result is one instructions part, identified by agentsmd.PartID: a product that records what the model was given in parts rather than as one string hands it the whole rendering under that id, and names a file it considered and left out by that file's Omitted.Path.

The walk reads the OS file system unless Options.FS is set. A product whose project lives somewhere it has no OS path for, such as a container or a remote workspace, passes that project as an fs.FS:

res, err := agentsmd.Chain("services/api", agentsmd.Options{
	FS:    workspace.FS(), // "." is the project's root
	Root:  ".",
	Extra: []string{filepath.Join(home, ".dax", "AGENTS.md")},
})

The path, Root and the chain's directories are then names in the FS (fs.ValidPath, "." for its root), directories are listed with fs.ReadDir, files are stat'd with fs.Stat and read through Open, and File.Path and Omitted.Path are the names in the FS, which the product maps to what it shows before Render. RenderedBudget counts the paths as Chain returns them, so a product that rewrites them changes the rendered size and bounds the rewritten rendering itself. The walk only asks the FS for names below "." and resolves no links itself, so an FS that confines, such as os.Root.FS(), keeps its confinement: a link it refuses is Chain's error, not a missing file. Extra stays OS paths, since the user's own file is not part of the project.

The package does not expand imports inside files, know any file name specially, or fetch anything.

Beyond the convention

agents.md says the file is named AGENTS.md and that the one nearest the edited file wins. It does not say how several files combine, how large they may be, how their text enters a prompt, or whether one file imports another. This library decides:

  • Name. A directory's file is the first of Names (AGENTS.md by default) equal byte for byte to a name the directory lists, on every host: agents.md is not AGENTS.md, on macOS either.
  • Combining. One file per directory from Root down to the path's directory, farthest first and nearest last, so the nearest is the last text the model reads; Extra comes before them all.
  • Size. Nothing is cut short. A file over MaxBytes is an error; the first file over Budget (the files) or RenderedBudget (the rendering) ends the chain without error. All three count the bytes a file reads, not the size its stat reported.
  • Paths in the budget. RenderedBudget counts each path as Chain returns it; rewriting File.Path before Render changes the size.
  • Rendering. <project_context>\n, then per file <project_instructions path="P">\n, the content, \n if the content does not end in one, and </project_instructions>\n; then </project_context>, with no newline. P has &, <, > and " escaped as &amp;, &lt;, &gt; and &quot;. No files render as nothing. These bytes are the agentsmd part a session records and hashes; testdata/golden/render.txt is an example.
  • Imports. None: an @path line is text, for a product to expand.

Development

make check    # gofmt, tidy, vet, deps, staticcheck, govulncheck, race tests

The fixture tree is testdata/tree; the golden render is testdata/golden/render.txt, regenerated with go test . -update.

Documentation

Overview

Package agentsmd finds and renders the instruction files of the AGENTS.md convention: the files that apply at a path are the ones in its directory and every ancestor, one per directory, and the nearest takes precedence.

Chain collects them farthest first and nearest last, so that when a product includes every file, later text refines earlier text, and says which files it found and left out, so a product can record or show exactly what the model was given and what it was not. Explicit paths, such as the user's own file outside the tree, come before the chain, since the nearest file is the one that wins. The walk reads the OS file system, or an fs.FS a product supplies in Options.FS when the project is somewhere else, such as a container or a remote workspace. Render wraps the files the way pi renders project context, one project_instructions element per file inside project_context. The package imports the standard library alone.

Index

Constants

View Source
const DefaultMaxBytes = 1 << 20

DefaultMaxBytes is the size above which a file is an error.

View Source
const PartID = "agentsmd"

PartID is the stable identifier of the one instructions part Render produces, for a product that records what the model was given as parts rather than as one string. The text of the part changes whenever the working directory moves or a file is edited; the identifier does not, so a reader can tell that this part moved and the others did not. A file considered and left out is named by its own Omitted.Path.

Variables

View Source
var DefaultNames = []string{"AGENTS.md"}

DefaultNames is the file name Chain looks for when Options.Names is empty.

View Source
var ErrTooLarge = errors.New("agentsmd: file exceeds size limit")

ErrTooLarge is wrapped in the error of a file over MaxBytes.

Functions

func Render

func Render(files []File) string

Render wraps the files as pi does: one project_instructions element per file, with its path as the attribute, inside project_context, in order, so the nearest file is last and refines the rest. No files render as the empty string.

The result is one instructions part, whose identifier is PartID: a product that hands its instructions to a session in parts hands it this whole string under that id, not one part per file.

Types

type File

type File struct {
	// Path is absolute, or, for a file of the chain read through
	// [Options.FS], its name in that FS. A product that shows the
	// file somewhere else maps the name to what it shows, such as a
	// workspace's root joined with it, before [Render]; that changes
	// the rendered size, which [Options.RenderedBudget] counts with the
	// path as Chain returned it.
	Path string
	// Content is the file's bytes as a string.
	Content string
}

File is one instruction file, read verbatim.

type Omitted

type Omitted struct {
	// Path is absolute, or the name in [Options.FS] as for
	// [File.Path], and is the omitted file's stable key.
	Path string
	// Size is the file's size in bytes: the bytes read, for a file
	// Chain read and left out over budget, and otherwise the size the
	// file system reported.
	Size int64
	// Reason says why the file is not in [Result.Files].
	Reason Reason
	// By is the path of the file that took this one's place, for
	// [Shadowed]; "" otherwise.
	By string
}

Omitted is one file Chain found and left out. A product recording what the model was given as parts, beside the part Render produces, names an omitted file by its Path, which is the stable key for the file across runs.

type Options

type Options struct {
	// FS, when set, is the file system the walk reads: the path given
	// to Chain, Root and every directory of the chain are names in it,
	// valid by [fs.ValidPath] ("." is its root), directories are
	// listed with [fs.ReadDir], and files are stat'd with [fs.Stat]
	// and read through Open. The walk never leaves FS: it only asks
	// FS for names it builds below "." and does not resolve links
	// itself, so an FS that confines, such as
	// [os.Root.FS], keeps its confinement, and an error it returns,
	// such as a link refused for leaving it, is Chain's error. Only an
	// [fs.ErrNotExist] means no file. Nil means the OS file system.
	//
	// Extra are OS paths either way: the user's own file in their home
	// directory is not part of the project.
	//
	// The bounds, MaxBytes and Budget, apply to the bytes a file
	// reads, which an FS that changes a file's text, such as one that
	// expands imports, may make larger than its Stat says.
	FS fs.FS
	// Names are the file names looked for in each directory, in order
	// of preference: the first one found is that directory's file and
	// the rest are not read, so a product can let AGENTS.override.md
	// shadow AGENTS.md, or CLAUDE.md stand in where AGENTS.md is
	// absent, as Codex reads them. Empty means [DefaultNames].
	//
	// A name is found when it equals, byte for byte, the name of an
	// entry the directory lists, whatever the host file system's case
	// rules, so agents.md is not AGENTS.md on macOS either. Each name
	// is one path element; any other is an error.
	Names []string
	// Root is the directory the walk stops after. Empty means the
	// file system root, which with FS is ".". A path outside Root is
	// an error.
	Root string
	// Extra are explicit OS paths included before the chain, in order,
	// such as the user's own file in their home directory, which the
	// repository's files then refine. Missing ones are skipped.
	//
	// They come first because the convention is that the nearest file
	// wins, and a file that is not in the tree at all is the farthest
	// of the lot: Codex reads ~/.codex/AGENTS.md first for that
	// reason. They are also the first charge on Budget, as they are
	// there.
	Extra []string
	// MaxBytes bounds one file; zero means [DefaultMaxBytes]. A file
	// that reads larger is an error, because a truncated instruction
	// file would silently change what the model is told. Chain reads
	// no more than one byte past it to find that out.
	MaxBytes int64
	// Budget bounds the total bytes of every file returned. The first
	// file that would take the total over it, and every file after
	// it, is left out and reported in [Result.Omitted], and Chain
	// returns what fits without error, so a large file deep in a tree
	// degrades the prompt rather than failing the run. No file is cut
	// short. Zero means no budget.
	//
	// The bytes charged are the bytes read. The size the file system
	// reports decides only whether a file is read: one whose reported
	// size does not fit is left out unread, and one that reads more
	// than fits is left out with the size it read.
	Budget int64
	// RenderedBudget bounds the bytes of [Render]'s output rather than
	// of the files, so a product bounding its prompt sets the number
	// the model pays: the files, the project_context element around
	// them and one project_instructions element per file, with its
	// path. It is spent as Budget is, in the same order, on the bytes
	// read, and the first file that would take the rendering over it
	// ends the chain. Zero means no rendered budget; Budget, when also
	// set, still bounds the files, and the first file over either ends
	// the chain.
	//
	// The paths counted are the ones Chain returns. A product that
	// rewrites [File.Path] before Render, such as to show a name in
	// Options.FS as a workspace path, changes the rendered size, and
	// must bound the rewritten rendering itself.
	RenderedBudget int64
}

Options configure Chain.

type Reason

type Reason int

Reason is why Chain left a file out.

const (
	// OverBudget: the file, or one before it, would have taken the
	// total over Options.Budget, or the rendering over
	// Options.RenderedBudget, and the chain ends at the first such
	// file.
	OverBudget Reason = iota
	// Shadowed: an earlier name in Options.Names exists in the same
	// directory and stands for it.
	Shadowed
)

func (Reason) String

func (r Reason) String() string

String returns "over budget" or "shadowed".

type Result

type Result struct {
	// Files are the instruction files to include: Options.Extra first,
	// then the chain farthest first and nearest last, so the nearest
	// file is the last text the model reads and wins.
	Files []File
	// Omitted are the files Chain found and did not include, in the
	// order it met them.
	Omitted []Omitted
}

Result is what Chain found: the files that apply, in order, and the files it saw and left out, so a product can record or show exactly what the model was given and what it was not.

func Chain

func Chain(path string, opts Options) (Result, error)

Chain returns the instruction files that apply at path: opts.Extra in order, then, in path's directory and each of its ancestors up to opts.Root, the first of opts.Names that the directory lists, farthest first and nearest last, within opts.Budget and opts.RenderedBudget. A file found and not included, because a preferred name shadows it or a budget is spent, is in Result.Omitted. A file is read only when its reported size fits the budgets, and the bounds apply to the bytes it reads: more than opts.MaxBytes is an error, and more than a budget has left omits it. A directory that cannot be listed is an error. path may be a directory, an existing file, or a file about to be created, whose directory is then the start. With opts.FS set, path is a name in it and the chain's files are named as they are in it.

Jump to

Keyboard shortcuts

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