Documentation
¶
Overview ¶
Package prompt models a system prompt as an ordered set of addressable sections rather than one opaque string. A host assembles sections from a persona, its skills and traits, then a tenant overlay can patch individual sections without being able to touch the ones the host pinned.
Index ¶
Constants ¶
const ( OrderRole = 1000 OrderPersona = 2000 OrderSkill = 3000 OrderKnowledge = 4000 OrderTrait = 5000 )
Order bands place each producer's sections in the assembled prompt. The values reproduce the order the prompt has always had: the agent's own instructions, then the persona's identity, then one section per skill, then retrieved knowledge, then one section per trait injection.
They are spaced a thousand apart for two reasons. A host can slot a section of its own between any two bands by picking a value in the gap, and a producer that emits several sections can take consecutive orders from its band without ever colliding with the next one.
Variables ¶
This section is empty.
Functions ¶
func ApplyOverlay ¶
ApplyOverlay applies patches to sections and returns the resulting section set. It copies sections rather than mutating the input, so a caller's slice is unaffected by the patches applied here.
A Locked section accepts an append and nothing else. The rule is written that way round on purpose. Enumerating the modes to refuse leaves every value nobody thought of permitted, and PatchMode is a bare string persisted as JSON, so "Replace" or a typo would sail past a refusal list and overwrite the body a host pinned.
A patch this package cannot apply is declined wherever it lands, locked section or not, and it never creates a section either. Reading an unrecognized mode as a replace would make every future mode name a destructive operation on today's build, which is the wrong way for that mistake to fail.
A declined patch does not fail the run: assembly proceeds with the section as the host wrote it. The declined patches come back in the second value so the caller can surface them, since a patch that looks applied and silently is not has been a recurring source of confusion in prompt configuration.
An unknown ID adds a new section, placed after every section already present so that a patch naming an ID nothing emitted cannot outrank a Locked one. Several creations in one call keep the order the patches were given in.
func Assemble ¶
Assemble joins sections into a single prompt string, sorted by Order and then by ID for stability when two sections share an order. Sections are joined with a blank line between them. A section's Title is prefixed to its Body when the Title is non-empty; an empty Title produces no prefix and no stray blank line, which is the case every section without a host-authored Title hits.
Types ¶
type ListFilter ¶
ListFilter controls overlay listing. AgentID narrows to one agent; Exact narrows the scope match to rows stored at precisely that depth instead of everything beneath it.
type Overlay ¶
type Overlay struct {
cortex.Entity
ID id.OverlayID `json:"id"`
AgentID id.AgentID `json:"agent_id"`
// Scope is stamped from the caller's context on create and is
// immutable afterwards. It is what decides which runs this overlay
// applies to, so letting an update move it would silently retarget
// an overlay a tenant already approved.
Scope cortex.Scope `json:"scope"`
// Patches reshape the agent's assembled sections. Order matters:
// they apply in slice order, so two patches against the same section
// compose the way a reader of the list would expect.
Patches []Patch `json:"patches,omitempty"`
// ToolsAdded and ToolsRemoved adjust the agent's tool list for runs
// in this scope. They are two lists rather than one replacement list
// because a host that only wants to withdraw one tool should not
// have to restate the agent's whole surface, and would silently
// re-grant anything the agent gained later if it did.
//
// Resolution order, since the two lists can disagree: within ONE
// overlay the removals apply after the additions, so a tool named in
// both ends up withdrawn. An overlay that both grants and withdraws
// the same tool is stating a restriction, and reading it the other
// way would hand out a tool the same document asked to take away.
//
// ACROSS overlays the narrower scope applies last, so it can
// withdraw what a broader scope granted and can re-grant what a
// broader scope withdrew. This is the same "narrowest wins" rule the
// patches follow. A workspace-level removal is therefore not a floor
// a project overlay cannot lift: making it one would leave removal
// as the only part of an overlay that inherited downward instead of
// being overridden, and a host wanting a tool gone everywhere takes
// it off the agent rather than off one scope.
//
// Re-granting takes NAMING the tool. A narrower overlay that adds
// something unrelated leaves every withdrawal above it in place: an
// overlay only moves the tools it mentions, so no addition can lift
// a removal by accident, and no tool nobody named can appear.
//
// That "narrower may re-grant" policy is right while overlays are
// written by the host at scopes the host controls, which is the only
// way they can be written today. It would be the wrong default if
// sub-scope overlays were ever delegated to the tenants living in
// those scopes, since a tenant could then restore a tool the
// platform withdrew above it. Delegation would need broader
// removals to become floors, and that is a policy change, not a bug
// fix.
ToolsAdded []string `json:"tools_added,omitempty"`
ToolsRemoved []string `json:"tools_removed,omitempty"`
// Model overrides the agent's model when non-empty. A per-run
// override still beats it: a caller naming a model for one call
// means it, where an overlay is a standing preference.
Model string `json:"model,omitempty"`
// Temperature and MaxTokens are pointers so that "leave the agent's
// value alone" is distinguishable from "set this to zero". A plain
// float64 would make an unset temperature read as a request for 0,
// which is a real and very different setting.
//
// Both are honored whenever the pointer is set, zero included, which
// is the whole reason they are pointers. Like Model, a per-run
// override outranks them.
Temperature *float64 `json:"temperature,omitempty"`
MaxTokens *int `json:"max_tokens,omitempty"`
}
Overlay is one host's per-scope customization of one agent. It carries the patches that reshape the agent's prompt sections, plus the handful of run settings a host is allowed to move without editing the agent itself.
An overlay never replaces the agent it points at. The agent stays the single definition every scope shares, and the overlay is the delta a tenant applies on top of it, so removing the overlay restores the agent's own behavior exactly.
One overlay per agent per scope, enforced by a unique index on (agent_id, scope_canon). A host that wants two different sets of patches for the same agent expresses that as two scopes, not two overlays, because "which of these applies" has no answer otherwise.
type Patch ¶
type Patch struct {
// ID names the target section. An ID with no matching section becomes
// a new section rather than an error, since a host patching a section
// a persona did not emit is expressing intent, not making a mistake.
// A section created that way is placed after every section already
// present, so an overlay cannot use an unrecognized ID to land text
// ahead of a Locked one. Position stays a host decision: put the
// section on the agent with the Order you want, then patch it by ID.
ID string `json:"id"`
// Body is the replacement or appended text, depending on Mode.
Body string `json:"body"`
// Mode selects how Body is applied. An empty Mode defaults to
// PatchReplace. Any value other than those two is declined, on a
// locked section and an unlocked one alike, so a mode that arrives
// misspelled from a stored overlay changes nothing rather than
// falling through to a replace nobody asked for.
Mode PatchMode `json:"mode,omitempty"`
}
Patch describes a change an overlay wants applied to one section, matched by ID.
type PatchMode ¶
type PatchMode string
PatchMode selects how a Patch applies to the section it targets. Only the two values below are implemented. A Patch carrying anything else names a mode this package cannot apply, and ApplyOverlay declines it rather than guessing which one was meant.
const ( // PatchReplace overwrites the target section's Body. It is the default // when a Patch omits Mode. PatchReplace PatchMode = "replace" // PatchAppend adds to the target section's Body rather than overwriting // it. It is the only mode a Locked section accepts. PatchAppend PatchMode = "append" )
type Section ¶
type Section struct {
// ID identifies the section for patching. Stable across assemblies of
// the same producer so an overlay can keep targeting it.
ID string `json:"id"`
// Source names what produced this section. Purely informational.
Source Source `json:"source,omitempty"`
// Title is prefixed to Body when assembling, if non-empty. Sections
// emitted by the existing producers leave this empty so assembly stays
// byte-identical to the prior single-string prompt.
Title string `json:"title,omitempty"`
// Body is the section's prompt text.
Body string `json:"body"`
// Order controls position in the assembled prompt. Lower runs first.
Order int `json:"order"`
// Locked marks a section a host pins as a guarantee to the platform.
// An append Patch is the only thing a locked section accepts.
// Everything else is declined, replace and unrecognized modes both.
//
// The pin covers overlay patches and stops there. A caller who can
// set a per-run system prompt discards the agent's stored sections
// outright, the locked ones with them, and what that caller wrote
// comes back as a single unlocked "role" section. So Locked pins a
// preamble against the tenants who write overlays, not against the
// callers who start runs. If you need it held against both, hold the
// second one at whatever API boundary lets a caller set that field.
Locked bool `json:"locked,omitempty"`
}
Section is one addressable piece of a system prompt.
type Source ¶
type Source string
Source names what produced a section. It is descriptive only: nothing in assembly branches on it, and it exists so an operator reading an assembled prompt can tell which part of the configuration to go and edit.
const ( // SourceHost marks a section the host wrote directly on the agent, // including the one an agent's plain SystemPrompt lowers into. SourceHost Source = "host" // SourcePersona marks a section produced by a persona. SourcePersona Source = "persona" // SourceSkill marks a section produced by a skill. SourceSkill Source = "skill" // SourceTrait marks a section produced by a trait. SourceTrait Source = "trait" // SourceKnowledge marks a section produced by retrieved knowledge. SourceKnowledge Source = "knowledge" )
type Store ¶
type Store interface {
// CreateOverlay writes a new overlay, stamping the scope from the
// context. A second overlay for the same agent in the same scope
// returns cortex.ErrAlreadyExists.
CreateOverlay(ctx context.Context, o *Overlay) error
// GetOverlay reads one overlay by id within the caller's scope.
GetOverlay(ctx context.Context, overlayID id.OverlayID) (*Overlay, error)
// GetOverlayForAgent reads the overlay an agent has at the caller's
// EXACT scope, which is the lookup prompt assembly runs. It matches
// exactly rather than by prefix on purpose: a prefix match would
// return whichever descendant scope's overlay happened to sort
// first, and "the overlay that applies here" has to be one specific
// row, not an arbitrary one. The unique index on
// (agent_id, scope_canon) is what makes at most one row match.
//
// Returns cortex.ErrOverlayNotFound when the agent has no overlay at
// this scope, which is the ordinary case for most agents.
GetOverlayForAgent(ctx context.Context, agentID id.AgentID) (*Overlay, error)
// GetOverlayForAgentAt reads the overlay an agent has at exactly the
// given scope, rather than at the caller's own. It is how assembly
// inherits: a run walks its scope's ancestor chain from broadest to
// narrowest, asking for each prefix in turn and applying what it
// finds in that order, so a run at [ws=A, proj=B] picks up the
// overlay written at [ws=A] and then the one written at
// [ws=A, proj=B].
//
// It matches the given scope EXACTLY, and inheritance is that walk
// rather than a prefix match. Reaching for ListOverlays instead is
// the mistake this method exists to prevent: prefix matching in this
// codebase widens DOWNWARD, so listing from [ws=A] returns every
// project's overlay inside that workspace. Feeding those into one
// run's prompt would mix a sibling project's instructions into a
// prompt that must never see them, and the call site would look
// perfectly reasonable while it happened.
//
// The scope argument must be the caller's own scope or an ancestor
// of it. Anything else returns cortex.ErrOverlayNotFound, because
// naming a scope is not a way to read outside your own. That upward
// reach is a deliberate widening: no other read here returns a row
// stored above the caller, and this one does only within the
// caller's own ancestry.
GetOverlayForAgentAt(ctx context.Context, agentID id.AgentID, scope cortex.Scope) (*Overlay, error)
// UpdateOverlay rewrites an overlay's mutable fields within the
// caller's scope. Scope itself is never rewritten.
UpdateOverlay(ctx context.Context, o *Overlay) error
// DeleteOverlay removes an overlay within the caller's scope.
DeleteOverlay(ctx context.Context, overlayID id.OverlayID) error
// ListOverlays returns overlays visible from the caller's scope,
// oldest first. Unlike GetOverlayForAgent this matches by prefix
// unless the filter asks for Exact, so a workspace-level caller sees
// every project's overlay beneath it.
ListOverlays(ctx context.Context, filter *ListFilter) ([]*Overlay, error)
}
Store persists overlays. Scope arrives on the context via cortex.ScopeFromContext, so no method carries it.