projectctx

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package projectctx discovers project-level instruction files — AGENTS.md and friends — from a checkout on disk, and renders them into a section an application can fold into an agentloop run's system prompt.

This is a layered capability, not core loop behaviour: agentloop works fine without it, nothing in the core imports it, and an application opts in by calling Load and passing the rendered result as RunRequest.Context:

docs, err := projectctx.Load(cwd)  // err is advisory; docs may be usable
result, err := loop.Run(ctx, agentloop.RunRequest{
    SessionID: id,
    Message:   msg,
    Context:   projectctx.Render(docs),
})

Render returns "" for no docs and the loop skips an empty Context, so the call is safe to make unconditionally.

Retrieval mode

Render inlines every doc in full, which is a STANDING cost: these files ride in the prompt of every turn of every run, so a large AGENTS.md is paid for on each round-trip whether or not the turn touches anything it covers. Against a small context window that is often the single largest avoidable line item.

RenderCatalog + Capabilities is the alternative: the prompt carries each file's opening section, and the model pulls the rest with projectGet(name) when it needs it.

docs, err := projectctx.Load(cwd)
caps = append(caps, projectctx.Capabilities(docs)...) // adds projectGet/projectList
result, err := loop.Run(ctx, agentloop.RunRequest{
    SessionID: id,
    Message:   msg,
    Context:   projectctx.RenderCatalog(docs),
})

The two are alternatives, not a pair: RenderCatalog points the model at projectGet, so the capabilities must be wired alongside it.

"Project" here means a checkout on disk — a directory tree with a repository root. It is unrelated to agentloop.Scope.ProjectID, which is a tenant boundary; a single Scope may span many checkouts and a checkout knows nothing about scopes.

Index

Constants

View Source
const DefaultFileName = "AGENTS.md"

DefaultFileName is the instruction file looked for when Loader.FileName is empty. AGENTS.md is the emerging cross-tool convention for agent-readable project guidance.

View Source
const DefaultInlineBytes = 4 * 1024

DefaultInlineBytes caps the part of a doc that RenderCatalog puts in the prompt when Loader.InlineBytes is zero.

Sized so a typical AGENTS.md — a page or two of conventions — rides along whole and nothing changes for it, while the outliers that actually justify retrieval get excerpted. Raising it trades context for fewer projectGet round-trips; lowering it does the reverse.

View Source
const DefaultMaxBytes = 64 * 1024

DefaultMaxBytes caps a single instruction file's body when Loader.MaxBytes is zero. Past the cap the head is kept and the rest replaced by a marker.

Under Render this is what lands in every prompt, so an oversized file is a standing cost rather than a one-off. Under RenderCatalog it caps only what projectGet can return, and DefaultInlineBytes is the figure that governs the prompt.

View Source
const PackName = "project_instructions"

PackName is the sandbox pack (and agentloop Capability) the retrieval mode installs. One pack for all discovered docs rather than one each: project instructions are a single authority the model consults, and "some files retrievable, others not" is not a state worth being able to configure.

Variables

This section is empty.

Functions

func Ancestors

func Ancestors(cwd string) []string

Ancestors returns cwd and each parent up to and including the repository root — the first ancestor containing a .git entry — or the filesystem root when there is no repository. The slice is ordered cwd-first.

func Capabilities

func Capabilities(docs []Doc) []agentloop.Capability

Capabilities wraps Packs as agentloop Capabilities, ready to append to the slice a DefaultSandboxBuilder gets.

func Packs

func Packs(docs []Doc) []sandbox.Pack

Packs returns the sandbox pack backing RenderCatalog: it installs projectGet(name) and projectList(), with every doc's FULL body held server-side and reachable by name.

Returns nil for no docs, so an application can append the result unconditionally — a project with no AGENTS.md gets neither the primitives nor the declarations that document them.

func Render

func Render(docs []Doc) string

Render composes docs into the prompt section an application passes as agentloop.RunRequest.Context, each file under its own heading. It returns "" for no docs — and the loop skips an empty Context — so callers can apply it unconditionally.

The heading level matches the sections agentloop's own system prompt uses, so project instructions read as one more section of it rather than as a document pasted onto the end.

func RenderCatalog

func RenderCatalog(docs []Doc) string

RenderCatalog composes docs into the prompt section for the retrieval mode: each file's opening section inline, and a pointer to projectGet(name) for whatever was left out.

It is the alternative to Render, not a companion to it — the two produce the same heading and would duplicate each other. Wire Capabilities alongside it, or the pointers it writes name a function the sandbox does not have.

Returns "" for no docs, like Render, so the call is safe to make unconditionally.

func ResolveWithin

func ResolveWithin(root, path string) (string, error)

ResolveWithin returns path resolved against root, following symlinks far enough to prove the final target stays under it. The path need not exist: the nearest existing parent is resolved and the missing suffix appended to that real parent, so a not-yet-created file is still checked against where it *would* land.

func Root

func Root(cwd string) string

Root returns the repository root containing cwd, or cwd's absolute path when no repository is found.

Types

type Doc

type Doc struct {
	// Path is where it was read from, as an absolute path. For the
	// application's own logging and diagnostics.
	Path string

	// Name is how the file is identified to the model: its path
	// relative to the repository root, or the bare file name for a
	// global doc. Render uses this rather than Path so the host's
	// directory layout stays out of the prompt.
	Name string

	// Content is the trimmed file body, truncated at Loader.MaxBytes.
	// This is what Render inlines, and what projectGet returns.
	Content string

	// Inline is the bounded opening section of Content — what
	// RenderCatalog puts in the prompt, cut at a line boundary and
	// capped at Loader.InlineBytes. Equal to Content when the whole
	// file fits.
	//
	// Empty on a Doc built by hand rather than by Load, in which case
	// InlineText falls back to Content: a caller who assembled its own
	// docs has already decided how big they are.
	Inline string
}

Doc is one discovered instruction file.

func Load

func Load(cwd string) ([]Doc, error)

Load discovers instruction files for a session rooted at cwd with the default Loader. See Loader.Load.

func (Doc) Complete

func (d Doc) Complete() bool

Complete reports whether the prompt carries the whole file, i.e. whether there is anything left for projectGet to add.

func (Doc) InlineText

func (d Doc) InlineText() string

InlineText is the part of the doc that belongs in the prompt.

type Loader

type Loader struct {
	// FileName is the instruction file to look for. Empty uses
	// DefaultFileName.
	FileName string

	// GlobalDir is an optional directory holding a user-global
	// FileName, loaded ahead of (and so overridden by) the repository's
	// own files.
	//
	// Empty means no global file is read at all. That is deliberate: a
	// library reaching into a user's home directory by default would
	// surprise the applications embedding it, so a CLI that wants the
	// convention opts in explicitly — GlobalDir:
	// filepath.Join(home, ".myagent").
	GlobalDir string

	// MaxBytes caps each file's body. Zero uses DefaultMaxBytes;
	// negative disables truncation.
	MaxBytes int

	// InlineBytes caps how much of each file RenderCatalog puts in the
	// prompt. Zero uses DefaultInlineBytes; negative inlines the whole
	// body, which makes RenderCatalog equivalent to Render.
	//
	// Ignored by Render, which inlines everything by definition.
	InlineBytes int
}

Loader discovers instruction files. The zero value is usable and looks for AGENTS.md files from the repository root down to the working directory.

func (Loader) Load

func (l Loader) Load(cwd string) ([]Doc, error)

Load discovers instruction files for a session rooted at cwd, returned in INCREASING priority — earlier is more general, later more specific, so a caller that renders them in order lets the closest file have the last word:

  • GlobalDir/FileName, when GlobalDir is set
  • FileName from the repository root down to cwd, outermost first

The upward walk stops at the repository root, or at cwd when there is no repository. Repository files must resolve within that root even after symlinks, and are read through an os.Root so a target swapped between the check and the read still cannot escape. Missing and empty files are skipped silently.

The error is advisory: any file that loaded cleanly is still returned alongside it, because one unreadable AGENTS.md in a subdirectory is a reason to warn, not a reason to deny the run its remaining project context. Callers that want strictness can treat a non-nil error as fatal themselves.

Jump to

Keyboard shortcuts

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