system-prompt
Optimize an agent's system prompt, developer message, or policy text — the instructions that shape its behavior. Use when the artifact to improve is a prompt or policy file rather than tools or a skill package: the agent lacks a rule, misses the required output format, or applies
Install
npx skills add https://github.com/skillberry-ai/cap-evolve/tree/main/skills/capabilities/system-prompt
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install skillberry-ai-cap-evolve@llmmart
git clone https://github.com/skillberry-ai/cap-evolve.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole skillberry-ai/cap-evolve collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Capability: system prompt
This capability treats one or more prompt/policy text files (prompt.txt,
policy.md, SYSTEM.md) as the optimizable artifact — whatever text the runtime
prepends to the agent's context as its instructions, output contract, and decision
policy.
Prose is the right lever when the agent lacks something it could be told: a
format, a rule, a decision criterion. It is the wrong lever when the agent already
has the rule and does not act on it — that needs the behavior enforced in code, so
it belongs to whatever capability edits the agent's tools, not here. Classify the
failure clusters first (./guidance/diagnose/SKILL.md, when present) and spend
prose only on the clusters this capability can actually move.
Pick the lever
Each item is a bounded edit class. Fix the biggest cluster with the narrowest lever
that reaches it; ship every class the traces call for in one candidate. Examples are
1-line and generic; depth is in references/concepts.md.
- Rewrite a rule for clarity, positively framed — say what TO do, specifically. A prohibition fences off one wrong path; a positive instruction names the target. Ex: "Don't be vague" → "State the record ID in every reply."
- Add the reason to a bare rule — a rule paired with its rationale extends to cases the rule's author never wrote down; a bare imperative does not. Ex: "Never use ellipses" → "Never use ellipses — the output is read by a TTS engine that cannot pronounce them."
- Consolidate redundant rules — merge duplicates into one, keeping every distinct constraint. Ex: three "confirm before deleting" lines → one "Confirm before any destructive action (delete, overwrite, send)."
- Add a rule the source requires but the prompt omits — it must trace to a real source (the policy doc, the runner, the task spec), never be invented. The added rule may introduce a constraint the prompt lacked, or state a stricter condition on an existing one. It may not broaden an existing permission or flip a decision the agent currently gets right: that changes behavior for every task in the class, including the passing ones whose gold answer was the stricter behavior. When a cluster needs different behavior, name the exact condition that separates the qualifying cases instead. Ex: the source says refunds need a manager code → "Require a manager code before any refund."
- Add an example — one or a few
<example>-tagged exemplars to pin a format that is hard to describe in prose. Ex: one<example>showing the exact JSON envelope expected. Examples are re-read every turn, so add the smallest set that pins the shape and treat a larger set as a hypothesis to gate, not a free win. - Restructure — separate instructions, context, examples, and input into their own sections or tags so the model does not conflate them, and put long reference data before the instruction that acts on it.
- Add a role / goal line — one sentence on who the agent is and what "done" means, when the prompt has none. Ex: "You are a careful support agent; resolve the request in one turn."
- Tighten the output contract — make the required shape explicit and exact.
Ex: "Reply with only a JSON object
{status, reason}— no prose." If the scorer reads the agent's final message, the contract must require the agent to state every value the scorer checks: agents routinely perform the action correctly and never report the result, and the scorer sees only the omission. (A missing action, as opposed to a missing report of it, is not fixable here — see the scope note above.) - Soften over-strong wording — when a cluster shows the agent over-doing rather
than under-doing (excess tool calls, over-engineering, triggering a behavior where
it did not apply), downgrade
CRITICAL/MUST/ALWAYSto "Use … when …". The edit that fixes an over-eagerness cluster is a cut, not an addition.
Never drop a needed rule — change, consolidate, or add
When an edit removes text, every distinct constraint that text carried must survive somewhere: rewritten, merged into a combined rule, or relocated. Deletion is legitimate when the information is genuinely redundant, contradicted by the source, or now enforced deterministically elsewhere — and in the first two cases prefer rewriting the conflicting rule. Consolidation cuts words, never rules.
An optimizer that deletes a needed rule can make one iteration's metric go up and
leave the class permanently broken, so the check is mechanical as well as stated:
apply() counts constraint-bearing lines before and after every edit and reports a
net loss in report["warnings"]. A warning is not a failure — a legitimate
consolidation triggers it too — it is a prompt to state, in PROCESS.md, where each
dropped constraint went. op: "set" on a whole file is the edit most likely to lose
one silently.
Keep the edit general
- Never hardcode a task's specifics. A rule must state the general policy that
holds across the class, not one task's case or answer. Good: "Reverse the charge
to the original payment method on file." Bad: "If the record id is
<TASK_SPECIFIC_ID>, apply the amount that task expects." Baking an id, value, date, or answer into the prompt overfits, gets rejected by the held-out gate, and can mislead other tasks. Use a failing task's specifics to identify the class, then write the general rule. The test for any edit: would this help on a task the optimizer has never seen? - Resolve conflicts, don't stack rules. Before editing, list the rules that govern the same action and check that no two give a different verdict on the same input; rewrite toward the stricter one rather than dropping either. A contradiction is the one failure mode detectable by reading the artifact alone, so it is worth the pass.
- Consolidate as constraints move out of the prompt. When a rule is now enforced deterministically elsewhere, remove its now-redundant prose: the enforcement is authoritative and the duplicate sentence only competes for attention. The prompt should get shorter as constraints become enforced, not longer. (This drops no rule — the constraint still lives, enforced elsewhere.)
- Watch length, but measure it.
validate()reports each file's line, token, and constraint-line counts. There is no universal length threshold worth quoting; compare a candidate against the accepted candidates of your own run and treat a prompt that grows every iteration without moving val as the signal to prune.
Handlers (scripts/abstract.py)
materialize(dir) -> {file: text} · apply(dir, edits) -> {changed, warnings} ·
validate(dir, baseline=None) -> {ok, files, stats, problems, warnings} ·
is_empty(dir) -> bool. Edit ops: set, append, ensure_contains. Pass
baseline (a directory or a {file: text} dict, e.g. the parent candidate) to have
validate report a constraint-line drop against it. A project adapter's apply can
call these directly.
How to run
python scripts/check.py
python scripts/run.py --path <capability_dir> # candidate + validity
python scripts/run.py --path <candidate_dir> --baseline <parent_dir> # + rule-loss check
References
references/concepts.md— what the prompt controls, the six authoring practices and five failure modes in full, how to adapt a prompt to the runtime reader's capability tier, pitfalls, and cited sources. Read once before your first non-trivial edit, and again when a candidate is accepted but barely moves the metric.
Files (cap-evolve)
-
references
-
concepts.md 6.4 KB
# Concepts — optimizing a system prompt Depth behind `SKILL.md`'s lever menu: what the prompt actually controls, the authoring practices and failure modes in full, how to adapt to the runtime reader, and the pitfalls that look like improvements. ## What the system prompt controls - **Role & task framing** — who the agent is and what "done" means. - **Output contract** — the exact shape the downstream consumer or scorer expects. The common silent failure is a capable agent that formats its answer wrong and scores zero, so diagnose shape before content. - **Decision rules** — when to call which tool, when to ask versus act, refusal and escalation rules. Many agents are scored on adherence to such rules. - **Reasoning scaffolds & exemplars** — added inline to shape how the model works through the task before answering. ## Adapting to the runtime reader The right prompt edit depends on WHO reads this prompt at runtime (see the `THE READER` block in your instructions, if present). Most of the advice here — soften imperatives, explain the reason, keep exemplars minimal — assumes a strong reader. Flip it for a weaker one: - **strong reader:** lean, reasoning-first prose; give the reason; keep exemplars minimal; soften brittle imperatives, because over-constraining hurts this reader. - **mid / weak reader:** be explicit. Prefer imperative step-by-step rules; include at least one worked exemplar per non-trivial behavior; keep decision chains short; make the output contract rigid and literal; and push behavioral rules into tool code rather than prose this reader will skip. When no reader is declared, default to the strong-reader advice — and say so in `PROCESS.md` so a later run can set the tier deliberately. ## The six authoring practices 1. **Be clear, direct, and specific — write for a capable new hire with no context.** If a colleague with minimal context would be confused by the instruction, so will the model. Spell out the desired output and the constraints; number steps when the order matters. 2. **Give the reason, not just the command.** A rule with its rationale extends to cases the author never enumerated. The canonical rewrite: "never use ellipses" → "the output is read by a TTS engine that cannot pronounce ellipses." 3. **Say what TO do, not only what NOT to do.** "Compose your reply in flowing prose" beats "do not use markdown" — a prohibition fences off one path, a positive instruction names the target. 4. **Structure deliberately.** Wrap instructions, context, examples, and input in their own sections or tags so the model does not conflate them; put long reference data before the instruction that acts on it, and keep the output contract adjacent to the point where the model produces the output rather than buried mid-preamble. 5. **Define the output contract explicitly, and use exemplars where prose cannot describe the shape.** For structured output, a schema or enum in the tool surface constrains it more reliably than prose asking for it. 6. **Keep the prompt lean and self-consistent, and tune trigger strength to the reader.** Redundant preambles and conflicting clauses compete for attention; when a cluster shows over-eagerness, soften `CRITICAL/MUST/ALWAYS` rather than adding more. ## The five failure modes 1. **Missing or loose output contract** — right content, wrong shape, zero reward. The most common silent prompt failure; diagnose shape before content. When the scorer reads the final message, the contract must require the agent to state every value the scorer checks. *Illustration:* on a customer-service benchmark scored on communicated figures (a total, a refund, a saving, a count, a balance), agents performed the write correctly and never reported the number — "After computing a refund, state the exact figure in your final message (e.g. 'Your refund is $42.00')" recovered the class. Read it as one instance of the general shape, not as a rule about money. 2. **Conflicting or over-broad instructions** — a later clause contradicts an earlier one and the resolution is not predictable. Detectable by reading the artifact alone: list the rules governing the same action, check for two different verdicts on one input, rewrite toward the stricter. 3. **Redundant preamble** — repeated or stale guidance competes with the rules that matter. Length is not safety, and `validate()` reports the counts so growth is visible across iterations. 4. **Negative-only phrasing** — "don't do X" with no positive alternative leaves the model to guess what Y is. 5. **Stale over-strong language** — anti-laziness `MUST/ALWAYS` phrasing that helped an older reader produces over-engineering, excess tool calls, and behaviors triggered where they did not apply. The fix for an over-doing cluster is a cut. ## Edit model Artifact = one or more text files (`prompt.txt`, `policy.md`, `SYSTEM.md`). Edit ops: `set`, `append`, `ensure_contains`. `validate` requires at least one non-empty prompt file, reports per-file line/token/constraint-line counts, and — given a `baseline` — warns when the candidate carries fewer constraint-bearing lines than its parent. That warning implements `SKILL.md`'s never-drop rule mechanically; it flags a net loss for a human or the optimizer to justify, and cannot tell a legitimate consolidation from a lost rule. ## Pitfalls - **Verbosity creep** — each iteration tends to add. Prune deliberately; the counts from `validate()` make the trend visible. - **Overfitting to the scorer's quirks** rather than the task — watch the val→test gap on an accepted candidate. - **Prompt-injection surface** — a rule that task input can override is not a rule. Prefer phrasing that survives adversarial input, and enforce anything security- relevant in code. - **Editing prose where the failure was never a knowledge gap** — the most expensive wasted iteration in this capability, because a longer prompt looks like progress. ## Sources - OpenAI / Anthropic prompt-engineering guides (instruction following, output contracts, exemplars) — https://platform.openai.com/docs/guides/prompt-engineering , https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview - "Large Language Models Are Human-Level Prompt Engineers" (APE), arXiv:2211.01910. - "Large Language Models as Optimizers" (OPRO), arXiv:2309.03409. - tau-bench (adherence to decision rules as a scored behavior), arXiv:2406.12045.
-
-
scripts
-
abstract.py 6.5 KB
"""system-prompt capability — concrete handlers for a prompt/policy artifact. A "system prompt" capability is one or more text files (default ``prompt.txt``) that constitute the agent's instructions/policy. These handlers are concrete (a prompt is just text), so there is nothing to stub — they form a small reusable library a project adapter's ``apply`` can call. Edit schema (what an optimizer may emit, mirrored by the mock ops): {"file": "prompt.txt", "op": "set"|"append"|"ensure_contains", "text": "..."} ``apply`` and ``validate`` also account for CONSTRAINT-BEARING lines, so the never-drop-a-needed-rule invariant is reported rather than merely documented: an optimizer that deletes a rule to make one iteration's metric go up leaves the class permanently broken, and ``op: "set"`` can do it in a single edit. The accounting is a crude line count -- it flags a net loss for a human or the optimizer to justify and cannot tell a legitimate consolidation from a lost rule -- so it is a warning, never a failure. """ from __future__ import annotations from pathlib import Path DEFAULT_FILES = ["prompt.txt", "policy.md", "SYSTEM.md"] # Markdown scaffolding that carries no constraint on its own. _STRUCTURE_CHARS = set("-=*_ \t") def rule_lines(text: str) -> int: """Count the constraint-bearing lines of a prompt. Headings, code fences and horizontal rules structure a prompt without stating a rule; every other non-blank line might state one. Deliberately crude: the number only has to make a NET LOSS visible, not judge meaning. """ n = 0 for line in text.splitlines(): s = line.strip() if not s or s.startswith("#") or s.startswith("```") or set(s) <= _STRUCTURE_CHARS: continue n += 1 return n def _stats(parts: dict) -> dict: """Per-file size signals so "the preamble is too long" becomes a number.""" return { name: {"lines": len(text.splitlines()), "tokens": len(text) // 4, # ~chars/4, the same estimate skill-package uses "rule_lines": rule_lines(text)} for name, text in parts.items() } def _rule_loss(before: str, after: str) -> int: """How many constraint-bearing lines an edit removed (0 when it added or held).""" return max(0, rule_lines(before) - rule_lines(after)) def materialize(capability_dir: Path) -> dict: """Read the prompt artifact into a named-component dict (gepa's view).""" capability_dir = Path(capability_dir) parts = {} for name in DEFAULT_FILES: f = capability_dir / name if f.exists(): parts[name] = f.read_text(encoding="utf-8") if not parts: # fall back to any single .txt/.md file present for f in sorted(capability_dir.glob("*.txt")) + sorted(capability_dir.glob("*.md")): parts[f.name] = f.read_text(encoding="utf-8") return parts def apply(capability_dir: Path, edits: list[dict] | None = None) -> dict: """Apply edits to the prompt files. Returns a report of what changed.""" capability_dir = Path(capability_dir) report = {"changed": [], "warnings": []} for e in edits or []: target = capability_dir / e["file"] op = e.get("op", "set") text = e.get("text", "") cur = target.read_text(encoding="utf-8") if target.exists() else "" if op == "set": new = text elif op == "append": new = cur + text elif op == "ensure_contains": new = cur if text.strip() and text.strip() in cur else cur + text else: raise ValueError(f"unknown op {op!r}") if new != cur: lost = _rule_loss(cur, new) target.write_text(new, encoding="utf-8") report["changed"].append(e["file"]) if lost: report["warnings"].append( f"{e['file']}: op {op!r} removed {lost} constraint-bearing line(s) " f"({rule_lines(cur)} -> {rule_lines(new)}). Change/consolidate/add rather " f"than delete: confirm every dropped constraint survives somewhere " f"(rewritten, merged, or enforced deterministically) and say where in " f"PROCESS.md.") return report def is_empty(capability_dir: Path) -> bool: """Return True when the capability directory has no meaningful content yet. "Meaningful" is judged after ``strip()`` — the same notion of non-empty that ``validate()`` uses — so a missing prompt file and an empty/whitespace-only one are both treated as an empty seed (nothing for the optimizer to build on yet).""" return not any(v.strip() for v in materialize(Path(capability_dir)).values()) def validate(capability_dir: Path, baseline: Path | dict | None = None) -> dict: """A prompt artifact is valid if it has at least one non-empty text file. A capability with no non-empty (non-whitespace) prompt content is accepted as a valid empty-seed starting state so the optimizer can create the initial content from failing trajectories. ``baseline`` is the text this candidate was derived from -- a directory (e.g. the parent candidate) or an already-materialized ``{file: text}`` dict. When given, a file carrying fewer constraint-bearing lines than its baseline is reported in ``warnings``. ``warnings`` is always present on both branches so a caller can read it without guarding. """ capability_dir = Path(capability_dir) if is_empty(capability_dir): return {"ok": True, "empty": True, "files": [], "stats": {}, "problems": [], "warnings": []} parts = materialize(capability_dir) nonempty = {k: v for k, v in parts.items() if v.strip()} warnings = [] if baseline is not None: base = baseline if isinstance(baseline, dict) else materialize(Path(baseline)) for name, text in sorted(base.items()): lost = _rule_loss(text, parts.get(name, "")) if lost: warnings.append( f"{name}: {lost} constraint-bearing line(s) fewer than the baseline " f"({rule_lines(text)} -> {rule_lines(parts.get(name, ''))}). A needed " f"rule must be changed, consolidated, or relocated -- not dropped; " f"record where each one went.") return {"ok": bool(nonempty), "files": list(nonempty), "stats": _stats(nonempty), "problems": [] if nonempty else ["no non-empty prompt file found"], "warnings": warnings} -
check.py 3 KB
"""Round-trip materialize → apply → validate on a temp prompt artifact.""" from __future__ import annotations import json import sys import tempfile from pathlib import Path import _bootstrap # noqa: F401 import abstract def main() -> int: report = {"skill": "system-prompt", "ok": False, "problems": [], "notes": []} with tempfile.TemporaryDirectory() as d: cap = Path(d) (cap / "prompt.txt").write_text("You are helpful.", encoding="utf-8") parts = abstract.materialize(cap) if "prompt.txt" not in parts: report["problems"].append("materialize did not read prompt.txt") rep = abstract.apply(cap, [{"file": "prompt.txt", "op": "ensure_contains", "text": " Be concise."}]) if "prompt.txt" not in rep["changed"]: report["problems"].append("apply did not record a change") v = abstract.validate(cap) if not v["ok"]: report["problems"].append(f"validate failed: {v['problems']}") if "warnings" not in v: report["problems"].append("validate omitted 'warnings' on the non-empty branch") report["notes"].append("materialize/apply/validate round-trip ok") # The two ops that can destroy content, plus the unknown-op guard. abstract.apply(cap, [{"file": "prompt.txt", "op": "append", "text": "\nCite sources.\n"}]) if "Cite sources." not in (cap / "prompt.txt").read_text(encoding="utf-8"): report["problems"].append("apply op=append did not concatenate") rep = abstract.apply(cap, [{"file": "prompt.txt", "op": "set", "text": "Be helpful.\n"}]) if (cap / "prompt.txt").read_text(encoding="utf-8") != "Be helpful.\n": report["problems"].append("apply op=set did not replace the file") if not rep["warnings"]: report["problems"].append("apply op=set dropped rule-bearing lines without warning") report["notes"].append("set/append ops behave; a rule-dropping set is flagged") try: abstract.apply(cap, [{"file": "prompt.txt", "op": "nope", "text": "x"}]) except ValueError: pass else: report["problems"].append("apply accepted an unknown op") # validate against a baseline sees the loss the edit above introduced. vb = abstract.validate(cap, baseline={"prompt.txt": "Rule one.\nRule two.\nRule three.\n"}) if not vb["warnings"]: report["problems"].append("validate(baseline=) missed a rule-bearing-line drop") # The empty-seed branch is a valid starting state, not a failure. with tempfile.TemporaryDirectory() as d: ve = abstract.validate(Path(d)) if not (ve["ok"] and ve.get("empty") and ve["warnings"] == []): report["problems"].append(f"validate on an empty dir should be ok/empty: {ve}") report["notes"].append("empty seed validates as a valid starting state") report["ok"] = not report["problems"] print(json.dumps(report, indent=2)) return 0 if report["ok"] else 1 if __name__ == "__main__": sys.exit(main()) -
run.py 1.1 KB
"""Expose a system-prompt artifact as a Candidate for the algorithm to optimize.""" from __future__ import annotations import argparse import json import sys from pathlib import Path import _bootstrap # noqa: F401 from cap_evolve import Candidate import abstract def main(argv=None) -> int: p = argparse.ArgumentParser(prog="system-prompt") p.add_argument("--path", required=True, help="capability dir holding the prompt file(s)") p.add_argument("--baseline", help="the dir this candidate was derived from (e.g. the parent " "candidate); warns when a prompt file lost rule-bearing lines") args = p.parse_args(argv) parts = abstract.materialize(Path(args.path)) v = abstract.validate(Path(args.path), baseline=Path(args.baseline) if args.baseline else None) cand = Candidate(id="seed", component="system-prompt", text_parts=parts, dir=str(args.path)) print(json.dumps({"candidate": cand.to_dict(), "valid": v}, indent=2)) return 0 if v["ok"] else 1 if __name__ == "__main__": sys.exit(main()) -
_bootstrap.py 3.6 KB
"""Thin shim: locate cap_evolve, then defer to cap_evolve._bootstrap. Skill scripts ``import _bootstrap`` first. The real path-resolution logic lives ONCE in ``cap_evolve._bootstrap`` (so it can't drift across skills); this shim only has to find that package, which means a minimal upward walk for ``core/`` — the single bit of bootstrapping that genuinely must run before cap_evolve is importable. Everything else delegates. """ from __future__ import annotations import os import sys from pathlib import Path def _seed_path() -> None: """Minimal: put a dir containing the cap_evolve package on sys.path. ``CAPEVOLVE_CORE`` is honoured BEFORE any ambient import. An editable install of a *different* cap-evolve checkout registers a ``sys.meta_path`` finder, which outranks both ``sys.path`` and ``PYTHONPATH`` — so "cap_evolve imports fine" is not evidence that it imports the checkout you are standing in. Deferring to the ambient package here made an explicit override unreachable, and the symptom was a stale core silently answering for this one (``ModuleNotFoundError: cap_evolve.constraints`` from a checkout that predates that module). An explicit env var wins. """ env = os.environ.get("CAPEVOLVE_CORE") want = Path(env).resolve() if env else None if want and (want / "cap_evolve" / "__init__.py").exists(): loaded = sys.modules.get("cap_evolve") already = getattr(loaded, "__file__", None) if already and Path(already).resolve().parent.parent == want: return # right checkout already imported: touch nothing p = str(want) if p in sys.path: sys.path.remove(p) sys.path.insert(0, p) if loaded is not None: # Evicting a module makes a re-import yield a DIFFERENT object, so anything # already holding a reference fails an `is` check. Only ever do it when the # loaded package really is the wrong checkout — otherwise this "fix" becomes # the bug (it broke two identity assertions in core/tests exactly once). for name in [m for m in sys.modules if m == "cap_evolve" or m.startswith("cap_evolve.")]: sys.modules.pop(name, None) for finder in list(sys.meta_path): if "cap_evolve" in getattr(finder, "MAPPING", {}): sys.meta_path.remove(finder) return # A checkout's own core outranks an ambient install. Without this, a skill script run # from checkout X silently executed against checkout Y's cap_evolve (an editable install # registers a sys.meta_path finder, which outranks sys.path), and the only symptom was # missing modules — or, worse, a green result measured against the wrong tree. here = Path(__file__).resolve() own = next((p / "core" for p in here.parents if (p / "core" / "cap_evolve" / "__init__.py").exists()), None) if own is not None: os.environ.setdefault("CAPEVOLVE_CORE", str(own)) return _seed_path() try: import cap_evolve # noqa: F401 return except Exception: pass cands = [] for parent in here.parents: cands.append(parent / "core") cands.append(parent) for c in cands: if (c / "cap_evolve" / "__init__.py").exists(): p = str(c) if p not in sys.path: sys.path.insert(0, p) return _seed_path() from cap_evolve._bootstrap import ensure_core # noqa: E402 # Anchor the upward walk at THIS skill script's location (not the core module's). ensure_core(Path(__file__).resolve())
-
-
meta.yaml 314 B
component: capability name: system-prompt summary: Optimize an agent's system prompt / policy text (instructions, output contract, decision policy). entry: scripts/run.py abstract: scripts/abstract.py check: scripts/check.py needs: [] provides: [candidate] compatible_with: optimizers: ["*"] algorithms: ["*"] -
SKILL.md 8.5 KB
--- name: system-prompt description: Optimize an agent's system prompt, developer message, or policy text — the instructions that shape its behavior. Use when the artifact to improve is a prompt or policy file rather than tools or a skill package: the agent lacks a rule, misses the required output format, or applies the wrong decision criterion. Covers the safe edit classes (rewrite, consolidate, add a sourced rule, reorder, tighten the output contract, soften over-strong wording), how to keep an edit general instead of overfitting one task, and the rule that a needed constraint is never deleted. A failure where the agent already knows the rule and skips the action belongs to the capability that edits tool code, not here. component: capability argument-hint: "--path DIR" allowed-tools: Read, Write, Edit, Bash provides: [candidate] needs: [] sources: [tau2bench] --- # Capability: system prompt This capability treats one or more prompt/policy text files (`prompt.txt`, `policy.md`, `SYSTEM.md`) as the optimizable artifact — whatever text the runtime prepends to the agent's context as its instructions, output contract, and decision policy. Prose is the right lever when the agent lacks something it could be *told*: a format, a rule, a decision criterion. It is the wrong lever when the agent already has the rule and does not act on it — that needs the behavior enforced in code, so it belongs to whatever capability edits the agent's tools, not here. Classify the failure clusters first (`./guidance/diagnose/SKILL.md`, when present) and spend prose only on the clusters this capability can actually move. ## Pick the lever Each item is a bounded edit class. Fix the biggest cluster with the narrowest lever that reaches it; ship every class the traces call for in one candidate. Examples are 1-line and generic; depth is in [`references/concepts.md`](references/concepts.md). 1. **Rewrite a rule for clarity, positively framed** — say what TO do, specifically. A prohibition fences off one wrong path; a positive instruction names the target. *Ex:* "Don't be vague" → "State the record ID in every reply." 2. **Add the reason to a bare rule** — a rule paired with its rationale extends to cases the rule's author never wrote down; a bare imperative does not. *Ex:* "Never use ellipses" → "Never use ellipses — the output is read by a TTS engine that cannot pronounce them." 3. **Consolidate redundant rules** — merge duplicates into one, keeping every distinct constraint. *Ex:* three "confirm before deleting" lines → one "Confirm before any destructive action (delete, overwrite, send)." 4. **Add a rule the source requires but the prompt omits** — it must trace to a real source (the policy doc, the runner, the task spec), never be invented. The added rule may introduce a constraint the prompt lacked, or state a stricter condition on an existing one. It may not broaden an existing permission or flip a decision the agent currently gets right: that changes behavior for every task in the class, including the passing ones whose gold answer was the stricter behavior. When a cluster needs different behavior, name the exact condition that separates the qualifying cases instead. *Ex:* the source says refunds need a manager code → "Require a manager code before any refund." 5. **Add an example** — one or a few `<example>`-tagged exemplars to pin a format that is hard to describe in prose. *Ex:* one `<example>` showing the exact JSON envelope expected. Examples are re-read every turn, so add the smallest set that pins the shape and treat a larger set as a hypothesis to gate, not a free win. 6. **Restructure** — separate instructions, context, examples, and input into their own sections or tags so the model does not conflate them, and put long reference data before the instruction that acts on it. 7. **Add a role / goal line** — one sentence on who the agent is and what "done" means, when the prompt has none. *Ex:* "You are a careful support agent; resolve the request in one turn." 8. **Tighten the output contract** — make the required shape explicit and exact. *Ex:* "Reply with only a JSON object `{status, reason}` — no prose." If the scorer reads the agent's final message, the contract must require the agent to state every value the scorer checks: agents routinely perform the action correctly and never report the result, and the scorer sees only the omission. (A missing action, as opposed to a missing report of it, is not fixable here — see the scope note above.) 9. **Soften over-strong wording** — when a cluster shows the agent over-doing rather than under-doing (excess tool calls, over-engineering, triggering a behavior where it did not apply), downgrade `CRITICAL/MUST/ALWAYS` to "Use … when …". The edit that fixes an over-eagerness cluster is a cut, not an addition. ## Never drop a needed rule — change, consolidate, or add When an edit removes text, every distinct constraint that text carried must survive somewhere: rewritten, merged into a combined rule, or relocated. Deletion is legitimate when the information is genuinely redundant, contradicted by the source, or now enforced deterministically elsewhere — and in the first two cases prefer rewriting the conflicting rule. Consolidation cuts *words*, never *rules*. An optimizer that deletes a needed rule can make one iteration's metric go up and leave the class permanently broken, so the check is mechanical as well as stated: `apply()` counts constraint-bearing lines before and after every edit and reports a net loss in `report["warnings"]`. A warning is not a failure — a legitimate consolidation triggers it too — it is a prompt to state, in `PROCESS.md`, where each dropped constraint went. `op: "set"` on a whole file is the edit most likely to lose one silently. ## Keep the edit general - **Never hardcode a task's specifics.** A rule must state the general policy that holds across the class, not one task's case or answer. *Good:* "Reverse the charge to the original payment method on file." *Bad:* "If the record id is `<TASK_SPECIFIC_ID>`, apply the amount that task expects." Baking an id, value, date, or answer into the prompt overfits, gets rejected by the held-out gate, and can mislead other tasks. Use a failing task's specifics to identify the class, then write the general rule. The test for any edit: *would this help on a task the optimizer has never seen?* - **Resolve conflicts, don't stack rules.** Before editing, list the rules that govern the same action and check that no two give a different verdict on the same input; rewrite toward the stricter one rather than dropping either. A contradiction is the one failure mode detectable by reading the artifact alone, so it is worth the pass. - **Consolidate as constraints move out of the prompt.** When a rule is now enforced deterministically elsewhere, remove its now-redundant prose: the enforcement is authoritative and the duplicate sentence only competes for attention. The prompt should get shorter as constraints become enforced, not longer. (This drops no rule — the constraint still lives, enforced elsewhere.) - **Watch length, but measure it.** `validate()` reports each file's line, token, and constraint-line counts. There is no universal length threshold worth quoting; compare a candidate against the accepted candidates of your own run and treat a prompt that grows every iteration without moving val as the signal to prune. ## Handlers (scripts/abstract.py) `materialize(dir) -> {file: text}` · `apply(dir, edits) -> {changed, warnings}` · `validate(dir, baseline=None) -> {ok, files, stats, problems, warnings}` · `is_empty(dir) -> bool`. Edit ops: `set`, `append`, `ensure_contains`. Pass `baseline` (a directory or a `{file: text}` dict, e.g. the parent candidate) to have `validate` report a constraint-line drop against it. A project adapter's `apply` can call these directly. ## How to run ``` python scripts/check.py python scripts/run.py --path <capability_dir> # candidate + validity python scripts/run.py --path <candidate_dir> --baseline <parent_dir> # + rule-loss check ``` ## References - [`references/concepts.md`](references/concepts.md) — what the prompt controls, the six authoring practices and five failure modes in full, how to adapt a prompt to the runtime reader's capability tier, pitfalls, and cited sources. **Read once before your first non-trivial edit**, and again when a candidate is accepted but barely moves the metric.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.