Prompt Mode Design: Full, Task, Minimal, and None
A well-built agent usually has a large system prompt. Persona, response style, tool conventions, skill catalog, memory usage, team collaboration rules, security boundaries. The main agent talking to a user needs all of it.
The problem is that the same agent also runs in other situations. Does a heartbeat job checking "any new mail?" every five minutes need 20,000 tokens of persona? Does a subagent that summarizes one file and exits need the team collaboration rules?
It doesn't. But most implementations have exactly one prompt, so they send it every time. This article is about splitting a prompt into size tiers by situation.
Why Four Tiers
In practice, four modes is where things converge.
- Full — the main agent talking directly to a user. Every section.
- Task — automation. Less personality needed, but it has to actually get work done, so tool- and skill-related sections stay.
- Minimal — periodic checks, simple repetitive work. The bare minimum.
- None — one identity line. For pure transformation work (translation, classification) where a prompt gets in the way.
Teams often start with two (full/minimal) and have Task force its way in later. Minimal was built to strip personality, but doing that also strips skill search and the execution-oriented instructions, and suddenly the task agent replies "I'll take care of that" and does nothing. Task fills that gap.
Treat Modes as Ordered Values
Leave modes as plain strings and you can't ask "which of these two is more restrictive?" Give them a rank.
type PromptMode string
const (
PromptFull PromptMode = "full" // main agent — all sections
PromptTask PromptMode = "task" // automation — lean but capable
PromptMinimal PromptMode = "minimal" // subagent/periodic — reduced
PromptNone PromptMode = "none" // identity line only
)
var modeRank = map[PromptMode]int{
PromptFull: 3, PromptTask: 2, PromptMinimal: 1, PromptNone: 0,
}
// Return the more restrictive of two modes
func minMode(a, b PromptMode) PromptMode {
if modeRank[a] <= modeRank[b] {
return a
}
return b
}
minMode is needed because more than one party gets a say in the mode. When the agent's config says minimal but the run kind calls for task, the more restrictive one must win. If the configuration said "keep it light," the run kind shouldn't override that and make it heavier.
Resolving the Mode: Four Layers
The actual decision walks several sources in priority order.
// Priority: runtime override > run-kind auto-detect > agent config > default
func resolvePromptMode(runtimeOverride PromptMode, sessionKey string,
configMode PromptMode) PromptMode {
// Layer 1: an explicit caller override always wins
if runtimeOverride != "" {
return runtimeOverride
}
// Layer 2a: heartbeat — a simple check, so cap at minimal
if isHeartbeatSession(sessionKey) {
if configMode != "" {
return minMode(configMode, PromptMinimal)
}
return PromptMinimal
}
// Layer 2b: subagent/scheduled — cap at task.
// Memory stays slim, but skill search and execution bias must survive
// or the run won't actually do anything.
if isSubagentSession(sessionKey) || isCronSession(sessionKey) {
if configMode != "" {
return minMode(configMode, PromptTask)
}
return PromptTask
}
// Layer 3: per-agent configuration
if configMode != "" {
return configMode
}
// Layer 4: default
return PromptFull
}
The key detail is that layer 2 is a ceiling, not a mandate. Because it goes through minMode, an already-more-restrictive config is respected. Auto-detection never overrides configuration to make the prompt heavier. That directionality matters — automatic rules stay predictable only when they can only reduce.
Section Gating — Assemble from Four Flags
The assembly function expands the mode into four booleans and lets each section declare its own condition.
func BuildSystemPrompt(cfg Config) string {
isFull := cfg.Mode == PromptFull || cfg.Mode == ""
isTask := cfg.Mode == PromptTask
isMinimal := cfg.Mode == PromptMinimal
var lines []string
// Identity — every mode, including None
lines = append(lines, identityLine(cfg))
// Persona — full/task only. Automation still needs some character
if (isFull || isTask) && len(cfg.PersonaFiles) > 0 {
lines = append(lines, buildPersonaSection(cfg.PersonaFiles)...)
}
// Execution bias — full/task.
// "Don't just describe a plan; call a real tool this turn"
if (isFull || isTask) && cfg.HasTools {
lines = append(lines, buildExecutionBiasSection()...)
}
// Tool call style (don't expose internal tool names, etc.) — full only.
// Pointless for runs no user will ever read
if isFull && cfg.HasTools {
lines = append(lines, buildToolCallStyleSection()...)
}
// Skills — full/task. Pinned inline, everything else via search
if (isFull || isTask) && cfg.HasSkills {
lines = append(lines, buildSkillsHybridSection(cfg.PinnedSkills, cfg.HasSkillSearch)...)
} else if isMinimal && cfg.PinnedSkills != "" {
// Minimal gets the pinned-skill summary only, no search guidance
lines = append(lines, buildPinnedSkillsMinimalSection(cfg.PinnedSkills)...)
}
return strings.Join(lines, "\n")
}
What makes this work is that adding a section declares its condition right there. Keep four separate per-mode templates instead and every new section means editing four places — which will drift.
What to Cut — the Deciding Question
For each section, ask: "without this, does the run fail, or does it just get less elegant?"
- Fails → keep it: tool usage, execution bias, safety boundaries, definition of done.
- Less elegant → cut it: tone guides, humor calibration, emoji policy, conversational style. A cron job doesn't need to fail wittily.
- Invisible to users → cut it: instructions like "never expose tool names to the user" are meaningless in a run no human reads.
The third one is the most valuable in practice. A substantial fraction of most prompts is guidance about how to appear in front of a user — and background runs have no user.
Caution 1: Cut Too Much and It Stops Working
Here's the trap people fall into chasing token savings. Trim the prompt aggressively and the model reverts to conversation mode. Given "clean up these files," it doesn't call a tool — it replies "Sure, I'll clean those up. What criteria should I use?" and stops.
The execution bias section is what prevents this. It's only a few lines, but it must survive in automation modes.
## Execution Bias
If the user asks you to do work, start doing it in the same turn.
Use a real tool call when the task is actionable; do not stop at a plan or a promise-to-act reply.
Commentary-only turns are incomplete when tools are available and the next action is clear.
This is exactly why Task exists separately from Minimal. Automation doesn't need personality but does need drive.
Caution 2: It Interacts with Caching
If you also use prompt caching, there's an interaction you have to know about. Caching works on an exact match of the prompt's leading region, and changing the mode changes the content of that stable region. Which means every mode change creates a new cache entry.
Two practical rules:
- Don't let the mode wobble within a session. If your architecture recomputes the mode per turn, pin it off the session key so input can't change the outcome.
- Assume one cache per mode. An agent mixing three modes has three cache entries. Check that each sees enough traffic to be reused — if not, you're paying creation cost three times over.
Caution 3: Don't Ship Gating Without a Preview
Introduce section gating and seeing the prompt that actually goes out gets hard. Once a dozen conditions interlock, "what exactly is this agent's task-mode prompt?" isn't answerable by reading code.
So build a preview function that takes a mode and returns the finished prompt from day one. Wire it into an admin screen or a CLI and debugging time drops sharply — plus you can compare real token counts per mode instead of guessing.
// Take an agent and a mode, return the final prompt.
// Shared by the admin UI, the CLI, and tests.
func BuildPreviewPrompt(ctx context.Context, agent *AgentData,
mode PromptMode, userID string, deps PreviewDeps) PreviewResult
Tests build on the same function. Pin rules like "team sections must not appear in minimal mode" as substring assertions, and you'll catch a section leaking through when someone adds one later.
The Real-World Effect
How much you save depends on your prompt's composition, but the trend is clear: the more an agent invests in persona, style guides, and team rules, the more the lower modes save. And these background runs are typically the high-frequency ones — a heartbeat every five minutes runs 288 times a day, far more often than user conversations do.
So the largest token savings sit exactly where no human is watching, which is precisely where you can cut the most. That's a fortunate alignment.
Summary
- Split the prompt into Full / Task / Minimal / None — two tiers isn't enough
- Give modes a rank and combine them with
minMode - Auto-detection acts only as a ceiling — it never overrides config upward
- Use per-section gating, never copies of a template per mode
- The test: "does it fail without this, or just get less elegant?" — runs invisible to users need no style guidance
- Keep execution bias in automation modes — cut it and the agent stops doing work
- Mind the caching interaction — pin the mode within a session
- Build the preview function first
Prompt modes ultimately come down to one question: "who is present for this run?" With a person there, how it looks matters. Without one, only what it accomplishes matters. Trim the prompt by exactly that difference.
댓글
댓글 쓰기