behavior

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package behavior builds the system prompt.

Behaviour is not "a prompt" — it is a stack of layers with different costs and precision, and the engineering is deciding which layer a rule belongs to:

doctrine      always in the prefix, permanent cost, low precision
tool schema   always in the prefix, high precision at the point of use
user rules    always in the prefix
skill index   one line each in the prefix
tool errors   zero cost until the failure, maximum precision

The operative rule: a rule that can be enforced in code does not belong in a prompt at all. Prompt is for what cannot be structurally enforced, which is why the read-before-edit invariant lives in the tools package and only its one-line summary appears here.

Build is pure, so the prefix is byte-identical between turns.

Spec: docs/specs/architecture/behavior-definition/202608080016-*.

Index

Constants

View Source
const DefaultMaxIndex = 64

DefaultMaxIndex caps how many skills reach the prefix.

The index line is paid for on every turn of every session, so an unbounded index is an unbounded tax: a directory grown to two hundred skills quietly adds two hundred lines to every prompt, and the cost arrives as a slow bill rather than as an error.

View Source
const DoctrineDirName = "doctrine"

DoctrineDirName is the directory searched under the user's config root.

View Source
const MaxWhenToUse = 120

MaxWhenToUse caps the index line. One line is the contract: the index is paid for on every turn, so a skill that describes itself in a paragraph is charging every session for a context most of them never enter.

View Source
const SkillsDirName = "skills"

SkillsDirName is the directory searched under each root.

Variables

This section is empty.

Functions

func Build

func Build(p Prompt, f Formulation) (string, error)

Build renders the system prompt. Pure: same input, byte-identical output.

func LoadDoctrineOverlay

func LoadDoctrineOverlay(dir string, maxBytes int) (DoctrineOverlay, []Notice, error)

LoadDoctrineOverlay reads the overlay from one directory.

The parameter is ONE directory, not a list. The contrast with LoadSkills(dirs []string, ...) is deliberate: a skill comes from two roots, a doctrine overlay comes from one (RN-11). The singular type says that better than a comment could, and the workspace root never becomes an argument in the first place.

func Render

func Render(r Reminder) string

Render wraps a reminder in the marker the base doctrine teaches the model to recognise.

Without the marker the model reads a reminder as the user speaking, and answers it — which is both wrong and unnerving to watch.

func RenderSkill

func RenderSkill(s Skill) string

RenderSkill wraps a loaded body so the model can tell it from the user speaking, exactly as a reminder is wrapped.

Types

type BudgetBand

type BudgetBand int

BudgetBand is how full the context is, as announced to the model.

Mirrors contextengine.Band as a plain integer rather than importing it: the reminder channel is about text, and the arithmetic belongs where the window is measured. Importing would also point this package at one that assembles prompts, which is backwards.

const (
	BudgetNone BudgetBand = iota
	Budget60
	Budget80
	Budget92
)

type Doctrine

type Doctrine struct {
	Identity   string
	ToolPolicy string
	Safety     string
	Style      string
}

Doctrine is the base layer. Safety is isolated from the rest so no configuration path can reach it.

func DefaultDoctrine

func DefaultDoctrine(toolNames []string) Doctrine

DefaultDoctrine is the shipped base layer.

It is deliberately short. Every line here costs tokens on every turn of every session forever, so a rule reaches this layer only when it cannot live anywhere cheaper: in a tool description, in an error message, or as an invariant in code.

func (Doctrine) Apply

func (d Doctrine) Apply(o DoctrineOverlay) Doctrine

Apply returns the doctrine with the overlay applied. Pure.

Safety is not read here and could not be written if it were: the overlay has no such field.

type DoctrineOverlay

type DoctrineOverlay struct {
	Identity  string // replaces
	Style     string // replaces
	ToolsMore string // APPENDS to ToolPolicy; never replaces
}

DoctrineOverlay is what the user's configuration may change in the base layer (RN-11). An empty field leaves the shipped text intact.

Safety is NOT here, and that absence is the guarantee (RN-12): there is no path to close because there is no path. A lock by convention breaks at the first refactor; a lock by type does not compile.

ToolsMore is spelled differently from ToolPolicy for the same reason. No accidental assignment can swap one for the other.

func (DoctrineOverlay) Origins

func (o DoctrineOverlay) Origins() SectionOrigins

Origins reports where each section will have come from once applied.

type Formulation

type Formulation interface {
	// Family names the family, so an assembled prompt can be traced back to
	// the formulation that produced it.
	Family() string
	// Section renders one titled block. An empty title is the leading block
	// that carries no heading.
	Section(title, body string) string
}

Formulation is how a model family prefers a rule to be worded.

RN-8 draws the line and it is worth stating plainly: the RULE is single and lives in this spec; the FORMULATION belongs to the family. Two families do not answer the same sentence the same way — one prefers marked-up structure, another plain prose — and a family that needs to change the *rule* rather than the wording is a sign that the rule is wrong, or that the model is not supportable.

So this interface can only change how a section is delimited. There is no method here that could add, remove or reword a rule, and that is deliberate: the way this abstraction stays honest is by not being able to express the thing it must not do.

func FormulationFor

func FormulationFor(family string) Formulation

FormulationFor picks the wording for a family.

The choice lives here rather than in the provider package on purpose. If each family carried its own wording, the wording would drift into the rule — a family would grow a sentence, then a caveat, then an exception, and two models would end up behaving differently while the spec said one thing. Here the whole surface is a delimiter.

An unknown family gets markdown. It is the safe default: a prompt that reads slightly less well is recoverable, and refusing to assemble one is not.

type Instruction

type Instruction struct {
	Source InstructionSource
	Scope  string
	// Locked marks an instruction the administrator set, which is what
	// SourceLocked ranking above everything else is FOR. Without the field the
	// precedence table had a top row nothing could ever occupy.
	Locked bool
	Text   string
}

Instruction is one block of user or administrator guidance.

type InstructionSource

type InstructionSource string

InstructionSource ranks where an instruction came from.

const (
	SourceLocked    InstructionSource = "locked"
	SourceDirectory InstructionSource = "directory"
	SourceProject   InstructionSource = "project"
	SourceUser      InstructionSource = "user"
	// SourceLearned is what the agent noted for itself, read back from the
	// workspace. It exists to be OUTRANKED: see the authority table.
	SourceLearned InstructionSource = "learned"
)

type Notice

type Notice struct {
	Path   string
	Reason string
}

Notice is something the loader refused to do silently.

func SafetyClaims

func SafetyClaims(in []Instruction) []Notice

SafetyClaims reports the places an instruction tries to loosen safety.

The instruction is NOT modified and NOT dropped: the rest of it is legitimate and discarding a whole file over one sentence is the silent-filter failure this project refuses everywhere else. What is returned is what to say out loud.

func (Notice) String

func (n Notice) String() string

type Origin

type Origin string

Origin says where a section of the assembled prompt came from.

It exists for the audit: an invisible replacement would be worse than the immutability it replaces, because the only way a user has today of knowing what reached the model is to read it.

const (
	OriginBuiltin  Origin = "builtin"
	OriginReplaced Origin = "replaced"
	OriginAppended Origin = "appended"
)

type OutOfChainInstruction

type OutOfChainInstruction struct {
	Path string
	Text string
}

OutOfChainInstruction is an instruction file found in a directory the session touched, outside the chain frozen at session creation.

type Prompt

type Prompt struct {
	Doctrine     Doctrine
	Tools        []string
	Instructions []Instruction
	SkillIndex   []SkillIndexEntry
	// Repo is where the work is happening, frozen at session creation. Nil for
	// a directory that is not a repository, which is ordinary and silent.
	Repo *Repo
}

Prompt is everything the prefix is built from.

type Reminder

type Reminder struct {
	Kind ReminderKind
	Text string
}

Reminder is one appended note.

Appended, never prefixed. That is the whole point of the channel: it steers the model mid-turn without touching the prefix, so the cache survives. A reminder in the prefix would cost the entire cached prompt every time anything changed on disk.

func Emit

func Emit(s SessionState) []Reminder

Emit is PURE: the same state always produces the same reminders, in the same order. That is what makes a replayed history byte-identical to a live one.

type ReminderKind

type ReminderKind string

ReminderKind identifies a reminder. The text is constant per kind, which is what keeps the history reproducible.

const (
	ReminderFileChanged             ReminderKind = "file_changed"
	ReminderApprovalDenied          ReminderKind = "approval_denied"
	ReminderCompacted               ReminderKind = "compacted"
	ReminderToolsParallel           ReminderKind = "tools_parallel"
	ReminderInstructionOutOfChain   ReminderKind = "instruction_out_of_chain"
	ReminderContextBudget           ReminderKind = "context_budget"
	ReminderUnmetCriteria           ReminderKind = "unmet_criteria"
	ReminderVerificationUnavailable ReminderKind = "verification_unavailable"
	ReminderUnplannedChange         ReminderKind = "unplanned_change"
	ReminderWorthRemembering        ReminderKind = "worth_remembering"
	ReminderProtectedTouched        ReminderKind = "protected_touched"
	ReminderInterrupted             ReminderKind = "interrupted"
)

type Repo

type Repo struct {
	// Branch is empty when there is none to name — a detached head, or a
	// repository with no commit yet.
	Branch string
	// MainBranch is what work is normally cut from and merged back into. It is
	// what "am I on the right branch" is answered against.
	MainBranch string
	// Detached marks a head that is not on a branch. Separate from an empty
	// Branch because "no branch" and "we did not find out" are different, and
	// only one of them is worth telling the model.
	Detached bool
	// Clean is stated rather than derived from an empty Status: "nothing
	// changed" and "we did not look" read the same when both are blank.
	Clean bool
	// Status is porcelain output, already bounded by whoever read it.
	Status string
	// Commits are the most recent, newest first, one line each.
	Commits []string
	// Truncated marks a status that did not fit. Nothing in this codebase cuts
	// output without saying so.
	Truncated bool
}

Repo is where the agent is working, as it stood when the session opened.

Data, never a reader: Build is pure and must stay pure, so nothing here runs a command. Whoever creates the session takes the snapshot and passes it in, the same way the instruction chain is frozen and handed over.

It is deliberately not a tool. `bash` already runs git, and a tool the model has to remember to call is a fact it uses when it happens to think of it. The branch it is on is not that kind of fact: every rule about where work belongs depends on it, and a rule that needs a lookup first is a rule followed by accident.

type SectionOrigins

type SectionOrigins struct {
	Identity   Origin
	ToolPolicy Origin
	Safety     Origin
	Style      Origin
}

SectionOrigins is the provenance of all four sections.

type SessionState

type SessionState struct {
	// ChangedFiles are paths read this session whose content on disk no longer
	// matches what the model was shown.
	ChangedFiles []string
	// DeniedTools are tool names whose boundary crossing the user refused.
	DeniedTools []string
	// Compacted reports that history was summarised since the last reminder.
	Compacted bool
	// ParallelBatch is the size of the batch that just ran. Only the fact that
	// it exceeds one reaches the text — the number itself would vary between
	// otherwise identical runs.
	ParallelBatch int
	// OutOfChain are instruction files discovered after the chain was frozen.
	OutOfChain []OutOfChainInstruction
	// UnmetCriteria are the names of the done criteria still not met. Set only
	// when the turn is being asked to continue because of them.
	UnmetCriteria []string
	// ProtectedTouched are paths that ARE the measurement and were written this
	// turn. Surfaced, never counted as progress in silence.
	ProtectedTouched []string
	// UnplannedChange reports that work has spread across several files with
	// no plan recorded.
	//
	// A boolean and not a count: the count would vary between otherwise
	// identical runs and reach the model through the text, which is what RN-7
	// of the context engine forbids and why the budget texts are constants too.
	UnplannedChange bool

	// WorthRemembering reports that the turn hit the same wall twice: the same
	// tool, the same failure, the same path. Once is a mistake; twice is the
	// repository teaching something nobody wrote down.
	//
	// It exists because measurement said so. Four scenarios, four designs, and
	// the model never once called `remember` on its own — so the prompt asking
	// for it was not enough, and a fifth sentence in the same place would be the
	// third time that failed. A reminder is the other layer: nothing until the
	// situation exists, and delivered at the moment it is being missed.
	WorthRemembering bool
	// VerificationUnavailable reports that files changed and no criterion was
	// able to run at all.
	//
	// Different from UnmetCriteria, which means a check ran and said no. Here
	// nothing ran, so there is no failure to fix and nothing to try again —
	// only something to admit.
	VerificationUnavailable bool
	// BudgetCrossed is the occupancy band to announce, set only on the turn the
	// band is crossed upward. BudgetNone announces nothing.
	//
	// The caller decides whether a crossing happened, because that needs the
	// previous band, and Emit is a function of this state alone.
	BudgetCrossed BudgetBand
}

SessionState is everything Emit is a function of. Nothing else is read: no clock, no filesystem, no globals.

type Skill

type Skill struct {
	Name      string
	WhenToUse string
	Triggers  []string
	Body      string
	Path      string
}

Skill is a body of guidance that only matters in a specific context.

The index line goes in the prefix; the body is appended when a trigger fires. Loading every body into the prefix is the fastest route to a system prompt of tens of thousands of tokens, paid on every turn of every session, with the model's attention spread across all of it (RN-7).

func LoadSkills

func LoadSkills(dirs []string, maxBytes int) ([]Skill, error)

LoadSkills reads skills from a set of directories, later directories winning on a name collision.

A skill is either `<dir>/<name>.md` or `<dir>/<name>/SKILL.md`. The second form exists so a skill can carry files beside it.

func Match

func Match(task string, skills []Skill) []Skill

Match selects the skills whose triggers fire for a task. Deterministic: the same text always loads the same bodies, which is what keeps a replayed session identical to the live one.

An explicit `triggers` list is matched as a phrase. Without one, the when-to-use line is matched on its own significant words, and two distinct hits are required — a single common word would load a skill into a task that merely mentioned it in passing.

func ParseSkill

func ParseSkill(text, path string) (Skill, error)

ParseSkill reads frontmatter and body. Pure over its input.

type SkillIndexEntry

type SkillIndexEntry struct {
	Name      string
	WhenToUse string
}

SkillIndexEntry is the one line a skill contributes to the prefix. The body is loaded on demand: putting every skill body in the prefix is the fastest route to a prompt of tens of thousands of tokens paid on every turn.

func Index

func Index(skills []Skill) []SkillIndexEntry

Index renders the prefix entries, one line each.

func IndexCapped

func IndexCapped(skills []Skill, max int) []SkillIndexEntry

IndexCapped is Index with an explicit ceiling.

Over the cap it keeps the first n by name and says how many it left out. It does not truncate in silence: a skill missing from the index is one the model never learns exists, and discovering that from behaviour costs far more than reading one line.

Jump to

Keyboard shortcuts

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