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 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" )
InstructionKindTurn0 identifies injected turn-zero guidance.
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 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 the already-bound source, 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, 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 from the already-bound source,
// 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) ([]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
// true include it; false omits it.
CommitCoauthor *bool
// 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
// ProjectInstructionHierarchy enables the scope-discovery protocol for a file-backed run.
ProjectInstructionHierarchy bool
}
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(source tool.Workspace, 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 ¶
Expand implements CommandExpander. See the type doc for the grammar and substitution rules.
func (*DirCommandExpander) List ¶
func (e *DirCommandExpander) List(ctx context.Context) ([]Command, error)
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 Shell tool executes against (e.g. "/bin/sh").
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(context.Context, []string, *session.InstructionSnapshot, int) ([]session.Message, []InstructionManifest, error)
// TargetScoped reports whether later target directories can change this assembler's contribution.
TargetScoped() bool
}
InstructionAssembler resolves ordered ephemeral messages from admitted sources.
type InstructionManifest ¶
type InstructionManifest struct {
Kind, Provenance, SourceID, Directory, File string
Partial, Omitted, HasGuidance bool
}
InstructionManifest identifies one emitted message without retaining its body.
func AssembleWithManifest ¶
func AssembleWithManifest(ctx context.Context, a InstructionAssembler, directories []string, state *session.InstructionSnapshot, maxContentBytes int) ([]session.Message, []InstructionManifest, error)
AssembleWithManifest nil-safely delegates to the canonical assembler method.
func DiscoverInstructions ¶
func DiscoverInstructions(ctx context.Context, source tool.WorkspaceReader, directory string) ([]session.Message, []InstructionManifest, error)
DiscoverInstructions selects one source-relative directory. Callers assemble ancestor chains with RootAssembler, which owns retention and byte accounting.
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, _ []string, _ *session.InstructionSnapshot, _ int) ([]session.Message, []InstructionManifest, error)
Assemble renders the capped tier-0 index into one user-role message.
func (MemoryIndexAssembler) TargetScoped ¶ added in v0.16.0
func (MemoryIndexAssembler) TargetScoped() bool
TargetScoped reports that the memory index does not depend on a selected workspace target.
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 preserves contributor order while sharing the one snapshot budget.
func NewMultiAssembler ¶
func NewMultiAssembler(assemblers ...InstructionAssembler) MultiAssembler
NewMultiAssembler combines assemblers while preserving contributor order.
func (MultiAssembler) Assemble ¶
func (m MultiAssembler) Assemble(ctx context.Context, directories []string, state *session.InstructionSnapshot, maxContentBytes int) ([]session.Message, []InstructionManifest, error)
Assemble combines each contributor's messages and manifests within one snapshot budget.
func (MultiAssembler) TargetScoped ¶ added in v0.16.0
func (m MultiAssembler) TargetScoped() bool
TargetScoped reports whether any contributor depends on a selected target.
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 ¶
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 ¶
func (m *MultiExpander) List(ctx context.Context) ([]Command, error)
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 {
Source tool.WorkspaceReader
SourceID string
SourcePrefix string
}
RootAssembler reads only the selected source and its trusted source-relative subtree.
func (RootAssembler) Assemble ¶
func (a RootAssembler) Assemble(ctx context.Context, directories []string, state *session.InstructionSnapshot, maxContentBytes int) ([]session.Message, []InstructionManifest, error)
Assemble resolves applicable project guidance while retaining the shared discovery snapshot.
func (RootAssembler) TargetScoped ¶ added in v0.16.0
func (a RootAssembler) TargetScoped() bool
TargetScoped reports whether the assembler depends on a selected workspace target.
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). 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.
func (RulesAssembler) Assemble ¶
func (a RulesAssembler) Assemble(ctx context.Context, _ []string, _ *session.InstructionSnapshot, _ int) ([]session.Message, []InstructionManifest, error)
Assemble renders the discovered rules into one user-role message.
func (RulesAssembler) TargetScoped ¶ added in v0.16.0
func (RulesAssembler) TargetScoped() bool
TargetScoped reports that rules do not depend on a selected workspace target.
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 ¶
func (a SoulAssembler) Assemble(ctx context.Context, _ []string, _ *session.InstructionSnapshot, _ int) ([]session.Message, []InstructionManifest, error)
Assemble renders the soul body into one user-role message.
func (SoulAssembler) TargetScoped ¶ added in v0.16.0
func (SoulAssembler) TargetScoped() bool
TargetScoped reports that the soul does not depend on a selected workspace target.
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 ¶
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, _ []string, _ *session.InstructionSnapshot, _ int) ([]session.Message, []InstructionManifest, error)
Assemble renders the capped user-model into one user-role message.
func (UserModelAssembler) TargetScoped ¶ added in v0.16.0
func (UserModelAssembler) TargetScoped() bool
TargetScoped reports that the user model does not depend on a selected workspace target.
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).