Versioning Prompts and Tool Schemas Alongside Code
Prompts and tool JSON schemas drive agent control flow as much as Python modules do.
Search across all documentation pages
Prompts and tool JSON schemas drive agent control flow as much as Python modules do.
Version them in the same repo, review them in the same PRs, pin them at deploy time, and stamp every run with the versions that actually ran.
Keep prompts and tool schemas as immutable packs under git, load them through a resolver that takes an explicit version, emit a deploy manifest that pairs pack versions with code_sha, and refuse "edit prod prompt in place" as a release process.
agent_pack/ (or similar) tree: prompts/, tools/, policies/, MANIFEST.json.system_v3.txt, tools_2026_04.json) or content hashes.open("prompt.txt") scattered in nodes.{code_sha, prompt_version, tool_schema_version, model_route, policy_version}.from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
ROOT = Path("agent_pack")
@dataclass(frozen=True)
class AgentPin:
code_sha: str
prompt_version: str
tool_schema_version: str
model_id: str
policy_version: str = "default"
def as_log_fields(self) -> dict[str, str]:
return {
"code_sha": self.code_sha,
"prompt_version": self.prompt_version,
"tool_schema_version": self.tool_schema_version,
"model_id": self.model_id,
"policy_version": self.policy_version,
}
class PackStore:
"""Load versioned prompts and tool schemas from the repo tree."""
def __init__(self, root: Path = ROOT) -> None:
self.root = root
def load_prompt(self, version: str) -> str:
path = self.root / "prompts" / f"system_{version}.txt"
text = path.read_text(encoding="utf-8")
return text.strip()
def load_tools(self, version: str) -> list[dict[str, Any]]:
path = self.root / "tools" / f"{version}.json"
data = json.loads(path.read_text(encoding="utf-8"))
assert isinstance(data, list), "tools file must be a JSON array"
return data
def content_hash(self, versioned_path: Path) -> str:
digest = hashlib.sha256(versioned_path.read_bytes()).hexdigest()
return digest[:12]
def build_release_manifest(pin: AgentPin, store: PackStore) -> dict[str, Any]:
prompt_path = store.root / "prompts" / f"system_{pin.prompt_version}.txt"
tools_path = store.root / "tools" / f"{pin.tool_schema_version}.json"
return {
**pin.as_log_fields(),
"prompt_sha256_12": store.content_hash(prompt_path),
"tools_sha256_12": store.content_hash(tools_path),
}
# Demo layout would include:
# agent_pack/prompts/system_v1.txt
# agent_pack/tools/tools_v1.json
store = PackStore()
pin = AgentPin(
code_sha="deadbeef",
prompt_version="v1",
tool_schema_version="tools_v1",
model_id="openrouter/auto", # verify at build
)
# print(json.dumps(build_release_manifest(pin, store), indent=2))
print(pin.as_log_fields())| Concern | Git-backed packs | Only in a prod admin UI |
|---|---|---|
| Review | PR diffs, CODEOWNERS | Often none |
| CI | Path filters + evals | Easy to skip |
| Rollback | Prior pin / git tag | "Who changed it?" |
| Audit | Commit + release ID | Chat archaeology |
| Env parity | Same files in stage/prod | Drift by hand |
A CMS or flag service can serve the active pin. The source of truth should still be reviewable artifacts.
agent_pack/
prompts/
system_v1.txt
system_v2.txt
fewshot_v1.json
tools/
tools_v1.json
tools_v2.json
policies/
spend_v1.yaml
MANIFEST.json # optional catalog of known versions
deploy/
pins/
active.json
2026-04-01T12-00Z-deadbee.jsonKeep generated dumps (pretty-printed API exports) out of the pack unless they are the real source.
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| Monotonic integer | v12 | Simple | Collisions across branches |
| Date stamp | 2026_04_15 | Chronology | Multiple same-day |
| Semver for tools | 2.1.0 | Compat rules | Overkill for prose prompts |
| Content hash | a3f1c9 | Exact identity | Harder human chat |
Common pattern: human label + content hash in the pin (prompt_version=v12, prompt_sha256_12=...).
Agents plan against the schema the model sees.
Breaking changes:
Safer sequence:
See also Versioning and Deprecating Agent Tools Without Breaking Agents.
code_shaA pack that references a tool implemented only in commit B must not pin on workers still at A.
Options:
request → read active pin (or flag override) → PackStore.load_* → agent run → log pinDo not read "latest file mtime" in production. That is non-reproducible.
Route agent_pack/** to agent owners and safety reviewers.
Treat prompt PRs like security-sensitive config when tools can spend money or send messages.
Never put API keys, customer PII, or raw prod transcripts into pack files.
Few-shots must be redacted and licensed for retention.
system.txt. Labels become theater.agent_pack/** are a footgun.| Approach | Strength | Weakness |
|---|---|---|
| Git packs + pin (this recipe) | Reviewable, CI-friendly | Needs loader discipline |
| Feature-flag payload only | Fast toggles | Weak review unless flags are PR-generated |
| DB rows edited by ops | Hotfix | Audit and env drift |
| Fully dynamic prompt from user prefs | Personalization | Hard evals; injection surface |
| Monolith config in app YAML | Simple | No fine-grained rollback of prompt alone |
Many mature teams combine git source + flag service for percentage rollout of an already-built pin.
Short constants are fine for tiny demos. Production packs should be files (or generated from files) so non-engineers can review diffs without hunting string literals.
Store a JSON or YAML list of roles/contents under a versioned name. The loader validates role enums before run.
Hubs are fine as distribution if each pulled revision is pinned by ID and mirrored or recorded in your release manifest (verify vendor APIs at build).
Yes in the pin. Model choice is behavior. Keep provider secrets out of the pack.
At least long enough for incident review and compliance (often 90 days or more). Immutable object storage works well.
Yes via a submodule, package, or internal artifact. Pin the pack version independently per service deploy.
Contract tests on JSON Schema, plus loop simulations with mocked tool handlers for old and new shapes.
A catalog of known versions, owners, deprecation dates, and min code SHA - optional but helpful for humans and validators.
Optional. Prompts rarely have clean compat. Immutability + pin + eval matters more than 1.2.3.
As high-cardinality-careful attributes on the root span: prompt_version, tool_schema_version, code_sha. See What Agent Observability Actually Needs to Capture.
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Reviewed by Chris St. John·Last updated Jul 16, 2026