Documentation
¶
Overview ¶
Package prompt is the domain context for two-layer system-prompt assembly: a cache-stable prefix plus a volatile suffix, placed so the LLM adapter can put a prompt-cache breakpoint between them. WP1 freezes only the Layered type and its render method; discovery behaviour (the <env> block, AGENTS.md/CLAUDE.md) lands in WP5.
Allowed imports (ARCHITECTURE.md §3): the standard library only (and other domain packages). It MUST NOT import adapter, agent, api, os, the OpenAI SDK, or any third-party library.
Index ¶
- Constants
- Variables
- func DefaultRole() string
- func DefaultTone() string
- func DiscoverInstructions(ctx context.Context, ws tool.Workspace) ([]session.Message, error)
- func EnvBlock(env Env) string
- func IsInjectedTurn0Fragment(text string) bool
- func RulesHeader() string
- func ValidCommandName(name string) bool
- type Builder
- type Command
- type CommandExpander
- type CommandLister
- type CommandPostExpansionSource
- type CommandSource
- type Config
- type DirCommandExpander
- type Env
- type InstructionAssembler
- type InstructionManifest
- type Layered
- type MemoryIndexAssembler
- type MemoryIndexSource
- type MultiAssembler
- type MultiExpander
- type NoopExpander
- type OperatorProfileConfig
- type OperatorProfileSource
- type RootAssembler
- type Rule
- type RuleOrigin
- type RulesAssembler
- type RulesSource
- type SoulAssembler
- type SoulSource
- type SourceExpander
- type UserModelAssembler
- type UserModelSource
Constants ¶
const ( InstructionKindTurn0 = "instruction" InstructionProvenanceProject = "project" InstructionProvenanceSoul = "soul" InstructionProvenanceMemory = "memory" InstructionProvenanceRules = "rules" InstructionProvenanceUserModel = "user_model" InstructionProvenanceCustom = "custom" InstructionProvenanceUnknown = "unknown" )
Instruction manifest kind and provenance tokens form a closed vocabulary.
const ( // DefaultOperatorProfileMaxEntries caps full facts included on each request. DefaultOperatorProfileMaxEntries = 32 // DefaultOperatorProfileMaxBytes caps the complete rendered profile block. DefaultOperatorProfileMaxBytes = 8 * 1024 // DefaultOperatorProfileMaxRunes independently caps decoded prompt size. DefaultOperatorProfileMaxRunes = 4 * 1024 )
const MaxCommandDescriptionRunes = 80
MaxCommandDescriptionRunes caps a command description (in RUNES) so a long frontmatter line or body sentence cannot blow out a palette row. It is the exported single source of the cap every command surface shares: the DirCommandExpander's derived descriptions (maxDescriptionLen aliases it) and a remote CommandSource client's defensive re-cap.
const MaxRuleBytes = 20 * 1024
MaxRuleBytes caps a single rule body. The body is the EXPENSIVE field: it is always-in-context turn-0 context (every run prepends it), summed across ALL injected rules, so an unbounded one would inflate every prompt. It therefore stays CONSERVATIVE — 20 KiB mirrors the soul body cap (internal/adapter/soul/store.go). It is the ONE canonical cap every source shares: the filesystem frontmatter parser truncates to it on discovery, and a remote driver re-truncates wire data to it defensively (the conformance suite asserts every listed rule respects it). Changing this value is a ONE-TIME prompt-cache-prefix invalidation (the truncation point moves).
Variables ¶
var DefaultCommandDirs = []string{".mecatl/commands", ".claude/commands"}
DefaultCommandDirs are the workspace-relative, PROJECT-TIER directories DirCommandExpander searches, in order, for a command's <name>.md file. ".mecatl/commands/" is the native location; ".claude/commands/" is accepted for familiarity. The first directory that contains a matching file wins.
This is the CANONICAL project-tier command dir set: it is the single source of truth for (a) the expander's default search path here, (b) composition's untrusted-workspace command gate (internal/app/build.go), and (c) the workspace-trust identity anchor (internal/adapter/workspacetrust/anchor.go), so the gate's admission surface and the anchor's drift surface can never silently diverge. Treat it as read-only; copy before mutating.
Functions ¶
func DefaultRole ¶
func DefaultRole() string
DefaultRole returns the built-in role-framing line Build uses when Config.Role is empty. It is exported so the composition layer can compose an agent-def body onto the SAME default framing the prompt uses, rather than carrying a private verbatim copy that could silently diverge if the default is reworded.
func DefaultTone ¶
func DefaultTone() string
DefaultTone returns the built-in tone/style block Build uses when Config.Tone is empty. It is exported so the composition layer composes its own posture/model deltas (e.g. the per-model agencyDelta) onto the SAME default tone the prompt uses, rather than carrying a private verbatim copy that could silently diverge if the default is reworded.
func DiscoverInstructions ¶
DiscoverInstructions looks for project-instruction files at the workspace root via the Workspace FS port (never os) and returns their content as user-role messages. Per doc 08 #5, project instructions ride in a USER message, never the system role, so they do not receive the elevated trust of the system prompt. The content is prefixed with a provenance marker so the model knows where the instructions came from.
Precedence (see instructionFiles): AGENTS.md wins. If AGENTS.md is present, exactly one message (for AGENTS.md) is returned and CLAUDE.md is ignored. If AGENTS.md is absent, CLAUDE.md is used as a fallback. If neither exists, or a file is empty/whitespace-only, no messages are returned and no error is reported. A genuine read error (other than "not found") is returned.
func EnvBlock ¶
EnvBlock renders env as an <env>...</env> block with a stable, deterministic key order. The same Env always produces byte-identical output. Empty fields are still emitted (with empty values) so the block's shape is constant across turns, which keeps diffing and testing simple.
func IsInjectedTurn0Fragment ¶
IsInjectedTurn0Fragment reports whether text is the body of a harness-injected turn-0 context fragment — a project-instructions (AGENTS.md/CLAUDE.md), rules, soul, memory-index, or user-model message — rather than a genuine user instruction.
The five turn-0 InstructionAssemblers record their output as RoleUser messages (so they ride after the cache-stable system prefix, fenced as untrusted DATA), which makes them indistinguishable from a real first prompt by role alone. A consumer that must anchor on "the user's genuine first instruction" — the compaction first-user pin, the resume re-injection guard — calls this to skip them. It recognises each fragment by the header its renderer prepends (the constants in this file are the shared source of truth), so a header reword is reflected here automatically; there is no duplicated literal prose to drift.
It does NOT recognise compaction summaries — those are a separate class of harness-authored RoleUser message owned by the agent package's isSynthesisedSummary predicate; a "genuine user turn" test composes both.
func RulesHeader ¶
func RulesHeader() string
RulesHeader returns the shared header string the RulesAssembler prepends to the rendered rules fragment. It is exported so tests can assert against the exact header the assembler emits, without carrying a private verbatim copy that could silently diverge if the header is reworded.
func ValidCommandName ¶
ValidCommandName reports whether name is a valid command invocation name: non-empty and made solely of the invocation grammar's name runes (letters, digits, '-', '_', '.'). It is the ONE shared validator: parseCommand accepts exactly this set, so a name that fails here can never be invoked as "/<name>" — consumers (a palette, a remote-source client) drop violators.
Types ¶
type Builder ¶
Builder assembles a Layered system prompt from a Config. prompt.Build is the default implementation; a host that embeds the engine for a non-coding agent can supply its own to fully own the system prompt (role, tone, safety, tool inventory) with no coding-agent defaults. The result's StablePrefix MUST be byte-stable across turns for a given Config so the LLM adapter can cache it (gauntlet #6); only the VolatileSuffix may vary per turn (e.g. from cfg.Env).
type Command ¶
type Command struct {
// Name is the command's invocation name (without the leading "/").
Name string
// Description is a short one-line summary derived from the command file: its
// frontmatter `description:` field when present, else the first non-blank body
// line, trimmed and length-capped. May be empty when neither yields text.
Description string
}
Command is a discovered slash command's listing metadata: its invocation name (the "<name>" of "/<name>") and a short, human-facing description for a palette or help surface. It carries NO body — discovery is intentionally cheap and metadata-only; expansion (which reads the body) is a separate concern.
type CommandExpander ¶
type CommandExpander interface {
// Expand inspects input. When input is a command invocation it loads the
// matching template from ws, substitutes its placeholders, and returns the
// rendered body with expanded=true. When input is not a command, or the named
// command does not exist, it returns input unchanged with expanded=false.
//
// A non-nil error is returned only for a genuine read fault discovering or
// reading a command file (not for "not a command" or "unknown command", which
// are normal, non-error outcomes that must not abort the run).
Expand(ctx context.Context, ws tool.Workspace, input string) (string, bool, error)
}
CommandExpander rewrites a raw user input into the prompt the model sees. If the input is a command invocation (e.g. "/review foo.go"), it expands the matching template; otherwise it returns the input unchanged (expanded=false).
It is the seam that makes slash commands / templated prompts pluggable: the agent loop consumes this interface in recordPrompt instead of using the raw user text directly, so a richer expander can be wired at the composition root without touching the loop. The default implementation, NoopExpander, returns the input unchanged so behaviour is identical when no commands are configured.
type CommandLister ¶
type CommandLister interface {
// List returns the available commands discovered through ws, de-duplicated by
// name (first occurrence wins, matching expansion precedence) and sorted by
// name. It returns a nil/empty slice when no commands are available. A non-nil
// error is reserved for a genuine read fault enumerating the command dirs; a
// dir that simply does not exist is not an error (it yields no commands).
List(ctx context.Context, ws tool.Workspace) ([]Command, error)
}
CommandLister is the discovery counterpart to CommandExpander: it enumerates the available commands (name + short description) WITHOUT expanding any. It is the seam a palette/help UI consumes to offer completion, kept separate from CommandExpander so an expander that cannot enumerate (e.g. a pure prompt source) need not implement it. List is read-only and cheap.
type CommandPostExpansionSource ¶
type CommandPostExpansionSource interface {
CommandBodyWithPost(ctx context.Context, name string) (body, post string, found bool, err error)
}
CommandPostExpansionSource optionally supplies activation-derived text that SourceExpander prepends after it has stripped frontmatter and substituted placeholders in the command body. It preserves CommandSource's raw-template contract while keeping non-template activation metadata out of substitution.
type CommandSource ¶
type CommandSource interface {
// ListCommands returns the available commands' metadata only (no bodies),
// de-duplicated by name and name-sorted. The set MAY change between calls
// (live semantics). A non-nil error is a genuine backend fault.
ListCommands(ctx context.Context) ([]Command, error)
// CommandBody returns the RAW template for name: optional YAML frontmatter
// is permitted and is stripped by the expander (never by the source), so
// the body round-trips verbatim. found=false reports an unknown name — a
// NORMAL outcome (the input passes through unchanged), never an error.
CommandBody(ctx context.Context, name string) (body string, found bool, err error)
}
CommandSource is the consumer-local port a non-filesystem command backend implements (the SoulSource precedent: defined here in prompt, next to its consumer). It carries NO path/dir/root concept — where a command template lives is the implementation's private business.
Lifecycle: LIVE semantics — the source is consulted on EVERY Expand/List call (no snapshot), matching the DirCommandExpander's reads-current-files discipline, so a palette promise of "the CURRENT commands" holds for remote backends too.
type Config ¶
type Config struct {
// Role is the role-framing line ("You are ..."). When empty a built-in
// default is used.
Role string
// Tone is the tone/style guidance block. When empty a built-in default is
// used.
Tone string
// Safety is the refusal/safety rules block. When empty a built-in default is
// used.
Safety string
// Tools is the tool catalog the model can call. Their names and a one-line
// purpose (derived from the first line of each ToolSpec.Description) are
// rendered into the stable prefix as the tool inventory.
Tools []tool.ToolSpec
// Env is the per-turn environment rendered into the volatile suffix. It MUST
// NOT influence the stable prefix.
Env Env
// OperatorProfile carries full durable user facts for this turn. Build renders
// it only in the volatile suffix; changing it never changes StablePrefix.
OperatorProfile OperatorProfileConfig
}
Config drives Build. It carries everything needed to assemble the two-layer system prompt. The fields split cleanly into cache-stable inputs (Role, Tone, Safety, Tools) that shape the StablePrefix, and the volatile Env that shapes the VolatileSuffix. Changing only Env must never alter the StablePrefix — that is the prompt-cache invariant (gauntlet #6).
type DirCommandExpander ¶
type DirCommandExpander struct {
// contains filtered or unexported fields
}
DirCommandExpander discovers command templates as <name>.md files under one or more workspace-relative directories (default ".mecatl/commands/" and ".claude/commands/"), read through the tool.Workspace FS port (never os, so the type stays infra-free / domain-pure).
Invocation grammar: an input is a command iff, after trimming leading spaces, it begins with "/" followed by a non-empty command name made of letters, digits, '-', '_', or '.'. The remainder (after the name) is split on whitespace into positional arguments.
Template substitution, applied to the command body:
- "$ARGUMENTS" → all arguments joined by a single space (empty if none).
- "$1", "$2", … → the corresponding positional argument (1-based); a reference past the end of the argument list expands to the empty string.
- Any other "$"-prefixed token is left intact, so bodies may contain literal shell-style variables the model is meant to see.
Frontmatter: an optional leading YAML frontmatter block (delimited by a "---" line at the very start and a closing "---" line) is stripped before substitution, so only the body template is returned. The frontmatter is metadata (e.g. a description) and never reaches the model.
Unknown command (no matching file in any directory) or non-command input returns the original input unchanged with expanded=false and no error, so a mistyped or unconfigured command never aborts the run.
func NewDirCommandExpander ¶
func NewDirCommandExpander(dirs ...string) *DirCommandExpander
NewDirCommandExpander constructs a DirCommandExpander. Each dir is a workspace-relative directory searched in order for "<name>.md"; a blank dir is ignored. When no non-blank dir is given it falls back to the defaults (".mecatl/commands/" then ".claude/commands/").
func (*DirCommandExpander) Expand ¶
func (e *DirCommandExpander) Expand(ctx context.Context, ws tool.Workspace, input string) (string, bool, error)
Expand implements CommandExpander. See the type doc for the grammar and substitution rules.
func (*DirCommandExpander) List ¶
List implements CommandLister. It scans each configured directory in order for "<name>.md" files (via the workspace Glob port, so it stays infra-free), reads each to derive a short description, and returns the commands de-duplicated by name (first directory wins, matching Expand's precedence) and sorted by name.
Description source, in order of preference: a frontmatter `description:` field when the file opens with a YAML frontmatter block; otherwise the first non-blank body line. Either way the result is trimmed and capped to maxDescriptionLen runes. A file that yields neither gets an empty description.
It is read-only and fail-soft on a missing directory (Glob over a dir with no files yields nothing). A file that cannot be read is skipped (logged via no channel here — it simply contributes nothing) rather than aborting the scan; a Glob fault on a dir IS returned, since that is a genuine enumeration failure.
type Env ¶
type Env struct {
// Cwd is the session working directory.
Cwd string
// OS is the host operating system / platform string (e.g. "linux").
OS string
// Model is the model identifier in use for the session.
Model string
// Date is the current date, pre-formatted by the caller (e.g. "2026-05-29").
// It is a string, not a time.Time, so the prompt package imports no time.
Date string
// Mode is the session permission mode (e.g. "default", "plan",
// "acceptEdits").
Mode string
// Shell is the shell the Bash tool executes against (e.g. "/bin/bash").
Shell string
// GitStatus is a start-of-session git snapshot (branch + short status +
// recent commits). It may be multi-line and is rendered as a dedicated
// sub-block; an empty value emits no sub-block.
GitStatus string
}
Env is the per-turn environment that renders into the volatile suffix of the system prompt. Every field is injected by the composition root; the prompt package never touches os or time so the domain stays infra-free and the render stays deterministic and testable. Because these values change between turns (date, possibly cwd/mode) they live in the volatile suffix, never the cache-stable prefix.
type InstructionAssembler ¶
type InstructionAssembler interface {
// Assemble returns the instruction messages for ws, in the order they should
// be recorded. A nil/empty slice means "no project instructions"; an error is
// returned only for a genuine read fault (not a missing file).
Assemble(ctx context.Context, ws tool.Workspace) ([]session.Message, error)
}
InstructionAssembler resolves the ordered set of project-instruction messages for a workspace, returned as user-role messages to be recorded once at the start of a run.
It is the seam that makes scoped context assembly (pattern 2) pluggable: the loop consumes this interface instead of calling a discovery function directly, so a richer adapter (parent-directory walk, user/managed scopes, @imports) can be wired at the composition root without touching the loop. The default implementation, RootAssembler, reproduces the v1 root-only behaviour exactly.
type InstructionManifest ¶
InstructionManifest identifies the provenance of one message returned by an InstructionAssembler without retaining or duplicating its body. Kind and Provenance are closed tokens consumed by the request-manifest builder.
func AssembleWithManifest ¶
func AssembleWithManifest(ctx context.Context, ws tool.Workspace, a InstructionAssembler) ([]session.Message, []InstructionManifest, error)
AssembleWithManifest runs an assembler exactly once and returns one metadata row per resulting message. Built-in assemblers retain their known provenance; custom assemblers remain compatible and are honestly labelled custom/unknown.
type Layered ¶
type Layered struct {
// StablePrefix is the cache-stable portion of the system prompt.
StablePrefix string
// VolatileSuffix is the per-turn portion appended after the prefix.
VolatileSuffix string
}
Layered is the two-layer system prompt. The StablePrefix is byte-stable across turns for a given configuration (role, tone, tool inventory, safety) so the LLM adapter can cache it; the VolatileSuffix holds per-turn material (e.g. the <env> block) that must not invalidate the cached prefix. The adapter places a prompt-cache breakpoint at the boundary between the two.
func Build ¶
Build assembles a Layered system prompt from cfg. The StablePrefix holds the role framing, tone/style guidance, safety rules, and the tool inventory — everything that is byte-identical across turns for a given Config, so the LLM adapter can prompt-cache it. The VolatileSuffix holds the per-turn <env> block rendered from cfg.Env. By construction the StablePrefix never references the date, cwd, model, or any other volatile value (cache invariant, gauntlet #6).
type MemoryIndexAssembler ¶
type MemoryIndexAssembler struct {
// Src is the tier-0 index source; nil makes the assembler a no-op (memory
// disabled).
Src MemoryIndexSource
// MaxEntries caps the rendered entry count; 0 uses the default.
MaxEntries int
// MaxBytes caps the rendered body size; 0 uses the default.
MaxBytes int
}
MemoryIndexAssembler renders the tier-0 memory index as a single user-role message recorded once at turn 0 (via the InstructionAssembler seam). It rides AFTER the cache-stable system prefix, so it never touches prompt.Build's StablePrefix — the prompt-cache invariant (gauntlet #6) is untouched.
It FAILS SOFT: a nil source, or a source returning an error, yields no message and no error — memory is best-effort context, not correctness, so a memory fault must never abort a run.
func (MemoryIndexAssembler) Assemble ¶
func (a MemoryIndexAssembler) Assemble(ctx context.Context, _ tool.Workspace) ([]session.Message, error)
Assemble renders the capped tier-0 index into one user-role message. The workspace is unused (memory is project-scoped at the adapter, not workspace files). It fails soft on a nil source or a source error.
type MemoryIndexSource ¶
type MemoryIndexSource interface {
// Index returns the tier-0 entries (key + description + updated-at, value
// omitted) for the current project. A nil slice means "nothing stored".
Index(ctx context.Context) ([]tool.MemoryEntry, error)
}
MemoryIndexSource is a CONSUMER-DEFINED port: the prompt package declares the tiny seam it needs (a tier-0 memory index) and the memory adapter satisfies it structurally, so prompt never imports the memory adapter. It is satisfied by *memory.Store, bound at the composition root (internal/app). Its single method mirrors tool.MemoryStore.Index, which prompt already has access to via the tool package it imports — so no new package edge is introduced.
type MultiAssembler ¶
type MultiAssembler struct {
Assemblers []InstructionAssembler
}
MultiAssembler composes several InstructionAssemblers, concatenating their messages in order. It lets the composition root layer turn-0 context — e.g. RootAssembler (AGENTS.md/CLAUDE.md) THEN MemoryIndexAssembler (the tier-0 memory index) — so the conversation opens with project instructions followed by the memory index, all as user-role messages recorded once at turn 0 (and so, by construction, AFTER the cache-stable system prefix — never in StablePrefix).
A nil child is skipped. The first child to return an error aborts (so a genuine read fault still surfaces); children that fail soft (return nil, nil) simply contribute nothing.
func NewMultiAssembler ¶
func NewMultiAssembler(assemblers ...InstructionAssembler) MultiAssembler
NewMultiAssembler builds a MultiAssembler from the given children (nil children are tolerated and skipped at Assemble time).
type MultiExpander ¶
type MultiExpander struct {
// contains filtered or unexported fields
}
MultiExpander composes an ORDERED list of CommandExpanders into one with a first-that-expands-wins rule. It is the seam that lets several expansion sources (e.g. file-backed slash commands plus MCP prompts) be layered without the agent loop knowing about any of them. It is pure domain composition: it imports no infrastructure and only consumes the CommandExpander interface, mirroring skills.MultiSource's earlier-wins semantics.
PRECEDENCE: the expanders are tried in slice order; the FIRST one that returns expanded=true wins and its rendered output is returned immediately. So an earlier expander SHADOWS a later one on a name collision (order callers highest-precedence-first). When no expander matches, the original input is returned unchanged with expanded=false. A non-nil error from any expander is a genuine read fault and is returned immediately (it stops the chain), matching the single-expander contract that errors are reserved for real I/O faults, not "not a command".
func NewMultiExpander ¶
func NewMultiExpander(expanders ...CommandExpander) *MultiExpander
NewMultiExpander builds a MultiExpander over the given ordered expanders (highest precedence first). nil entries are dropped so callers can assemble the slice conditionally without nil checks.
func (*MultiExpander) Expand ¶
func (m *MultiExpander) Expand(ctx context.Context, ws tool.Workspace, input string) (string, bool, error)
Expand tries each composed expander in order and returns the first expansion (expanded=true). If none expands, it returns the original input unchanged with expanded=false. An error from any expander stops the chain and is returned.
func (*MultiExpander) List ¶
List aggregates the lists of every composed expander that ALSO implements CommandLister, applying the SAME first-wins precedence as Expand: the expanders are walked in slice order and the first occurrence of a name wins, so an earlier (higher-precedence) source shadows a later one on a name collision. An expander that does not implement CommandLister contributes nothing (it cannot enumerate). The merged result is de-duplicated by name and sorted. A read fault from any child stops the walk and is returned.
type NoopExpander ¶
type NoopExpander struct{}
NoopExpander is the default CommandExpander. It performs no expansion and returns every input unchanged (expanded=false). The zero value is ready to use; it is the default in agent.Deps so command expansion is OFF unless a DirCommandExpander (or another adapter) is explicitly wired in.
type OperatorProfileConfig ¶
type OperatorProfileConfig struct {
Entries []tool.MemoryEntry
MaxEntries int
MaxBytes int
MaxRunes int
}
OperatorProfileConfig is the volatile operator-profile input to Builder.
type OperatorProfileSource ¶
type OperatorProfileSource interface {
List(ctx context.Context, prefix string) ([]tool.MemoryEntry, error)
}
OperatorProfileSource supplies current durable user facts through the existing MemoryStore List shape. Lifecycle metadata is deliberately not required.
type RootAssembler ¶
type RootAssembler struct{}
RootAssembler is the default InstructionAssembler. It resolves project instructions from the workspace root only (AGENTS.md winning, CLAUDE.md as fallback) by delegating to DiscoverInstructions, so it is byte-for-byte identical to the v1 behaviour. The zero value is ready to use.
type Rule ¶
type Rule struct {
Name string // logical id from the filename stem (e.g. "testing"); non-empty
Body string // the markdown body, frontmatter stripped, byte-capped by the source
Paths []string // glob patterns from `paths:` frontmatter; nil/empty = always applies
Origin RuleOrigin // admission tier (observability only; a tier label, never a location)
}
Rule is one project/user rule: a markdown body plus the OPTIONAL path globs that scope it. Pure value object — no behaviour, NO file/path/dir/root concept (the source's private business, matching SoulSource/CommandSource). A rule with an empty Paths slice is UNCONDITIONAL (always applies); a non-empty Paths slice scopes it to the listed globs, stated to the model as a condition the model applies itself (eager v1 — see ADR 0081). Paths rides the port from day one so a future lazy, path-triggered activation is additive, not a breaking change.
type RuleOrigin ¶
type RuleOrigin string
RuleOrigin classifies the ADMISSION TIER a rule entered through (project / user / driver). A tier label, NEVER a location — the SkillOrigin/AgentOrigin discipline. It is the THIRD parallel closed label set: extraction of a shared Origin type was evaluated here and DEFERRED — the sets are not identical (this set has no "explicit" tier; rules carry no operator-flag lane), so a shared type would force a superset one seam must never mint. Recorded in agentsource.go's NOTE.
const ( RuleOriginProject RuleOrigin = "project" // workspace-tier (trust-gated at construction) RuleOriginUser RuleOrigin = "user" // user-tier (never trust-gated) RuleOriginDriver RuleOrigin = "driver" // operator-configured remote driver )
The CLOSED admission-tier label set — implementations must never mint a new label (a consumer that does not recognise one normalizes to Driver, mirroring the SkillOrigin/AgentOrigin contract).
type RulesAssembler ¶
type RulesAssembler struct {
// Src is the rules source; nil makes the assembler a no-op (rules disabled).
Src RulesSource
// MaxBytes caps the total rendered body size (header + all fenced rules);
// 0 uses the default (defaultMaxRulesBytes).
MaxBytes int
// MaxCount caps how many rules are rendered; 0 uses the default
// (defaultMaxRulesCount).
MaxCount int
}
RulesAssembler renders all discovered rules as a single user-role message recorded once at turn 0 (via the InstructionAssembler seam). It rides AFTER the cache-stable system prefix, so it never touches prompt.Build's StablePrefix — the prompt-cache invariant (gauntlet #6) is untouched. Each rule is fenced in a <rule name="...">...</rule> block, with an "Applies when:" condition rendered from the rule's Paths. Rules are injected in the order ListRules returns them (name-sorted at the source).
It FAILS SOFT: a nil source, a source error, or an empty rule set yields no message and no error — rules are best-effort project guidance, not correctness, so a fault must never abort a run.
type RulesSource ¶
type RulesSource interface {
// ListRules returns the discovered rules, name-sorted and de-duplicated. A
// nil/empty slice means "no rules"; an error is returned only for a genuine
// read fault (not a missing source), and the assembler fails soft on it
// (never aborts a run).
ListRules(ctx context.Context) ([]Rule, error)
}
RulesSource is a CONSUMER-DEFINED port: prompt declares the tiny seam it needs (the ordered set of rules) and the rules adapter satisfies it structurally, so prompt never imports the rules adapter. It is satisfied by *rulesfs.FSSource (or a future remote driver), bound at the composition root. Mirrors SoulSource/CommandSource.
Lifecycle: SNAPSHOT-semantics — ListRules is stable for the life of the source (the harness resolves once at build; there is no watch seam, matching the build-once trust-gate invariant).
type SoulAssembler ¶
type SoulAssembler struct {
// Src is the soul source; nil makes the assembler a no-op (soul disabled).
Src SoulSource
}
SoulAssembler renders the user-scoped persona/"soul" as a single user-role message recorded once at turn 0 (via the InstructionAssembler seam). It rides AFTER the cache-stable system prefix, so it never touches prompt.Build's StablePrefix — the prompt-cache invariant (gauntlet #6) is untouched. By the chosen ordering (issue #14) it sits BEFORE the memory index: identity ("who you are") precedes saved facts ("what you know").
It FAILS SOFT: a nil source, a source error, or an empty body yields no message and no error — the persona is best-effort context, not correctness, so a soul fault must never abort a run.
func (SoulAssembler) Assemble ¶
Assemble renders the soul body into one user-role message. The workspace is unused: the soul is user-scoped (~/.config/mecatl/soul.md), resolved by the adapter against the process environment, NOT against the session workspace root. It fails soft on a nil source, a source error, or an empty body.
type SoulSource ¶
type SoulSource interface {
// Load returns the soul body (already validated/sanitised by the adapter), or
// "" when there is no usable soul. It must fail soft: a missing/empty/oversized/
// flagged/unreadable soul yields ("", nil), never an error that aborts a run.
Load(ctx context.Context) (string, error)
}
SoulSource is a CONSUMER-DEFINED port: the prompt package declares the tiny seam it needs (a user-scoped persona/"soul" fragment) and the soul adapter satisfies it structurally, so prompt never imports the soul adapter. It is satisfied by *soul.Store, bound at the composition root (internal/app). It mirrors MemoryIndexSource — a minimal interface declared here, implemented by an adapter, wired in the composition layer.
The body is OPAQUE text (no schema, no entry list). A "" return means "no usable soul" — the assembler then contributes nothing. All load discipline (path resolution, byte cap, injection scan, fail-soft) lives in the ADAPTER; the domain only fences and wraps whatever clean body it is handed.
type SourceExpander ¶
type SourceExpander struct {
// contains filtered or unexported fields
}
SourceExpander adapts a CommandSource to the CommandExpander/CommandLister seams the agent loop and the palette consume, reusing the SAME grammar (parseCommand), frontmatter stripping, and placeholder substitution as DirCommandExpander — so a template expands byte-identically whichever backend serves it. The Workspace argument is ignored (mirrors SoulAssembler: the source is not workspace-rooted).
func NewSourceExpander ¶
func NewSourceExpander(src CommandSource) *SourceExpander
NewSourceExpander builds a SourceExpander over src.
func (*SourceExpander) Expand ¶
func (e *SourceExpander) Expand(ctx context.Context, _ tool.Workspace, input string) (string, bool, error)
Expand implements CommandExpander: a non-command input passes through unchanged; an unknown command name passes through unchanged (found=false is normal); a backend fault is returned as an error; otherwise the body is frontmatter-stripped and placeholder-substituted exactly like a file-backed command.
type UserModelAssembler ¶
type UserModelAssembler struct {
// Src is the user-model source; nil makes the assembler a no-op (disabled).
Src UserModelSource
// MaxEntries caps the rendered entry count; 0 uses the default.
MaxEntries int
// MaxBytes caps the rendered body size; 0 uses the default.
MaxBytes int
}
UserModelAssembler renders the saved user-model as a single user-role message recorded once at turn 0 (via the InstructionAssembler seam). It rides AFTER the cache-stable system prefix, so it never touches prompt.Build's StablePrefix — the prompt-cache invariant (gauntlet #6) is untouched. By the chosen ordering (issue #14) it sits LAST: identity (soul) precedes saved project facts (memory index) which precede the operator model (this).
It FAILS SOFT: a nil source, a source error, or an empty set yields no message and no error — the user model is best-effort context, not correctness, so a fault must never abort a run.
func (UserModelAssembler) Assemble ¶
func (a UserModelAssembler) Assemble(ctx context.Context, _ tool.Workspace) ([]session.Message, error)
Assemble renders the capped user-model into one user-role message. The workspace is unused (the user model is user-scoped at the adapter, cross-project, not workspace files). It fails soft on a nil source, a source error, or an empty set.
type UserModelSource ¶
type UserModelSource interface {
// Index returns the user-model entries (key + description + updated-at, value
// omitted) for the current operator. A nil slice means "nothing stored".
Index(ctx context.Context) ([]tool.MemoryEntry, error)
}
UserModelSource is a CONSUMER-DEFINED port: the prompt package declares the tiny seam it needs (a user-model fragment — durable FACTS about the operator) and the memory adapter satisfies it structurally, so prompt never imports the memory adapter. It is satisfied by *memory.Store (a SECOND, user-scoped store instance), bound at the composition root (internal/app). Its single method mirrors MemoryIndexSource.Index / tool.MemoryStore.Index, which prompt already has access to via the tool package it imports — so no new package edge is introduced.
The entries it returns are tier-0-shaped (key + description + updated-at, value omitted), like the memory index — but scoped to the user-model namespace and rendered LAST in the turn-0 seam (after the project memory index), so identity (soul) → saved project facts (memory index) → who-the-operator-is (user model).