Skip to content

agentkit.agents.prompts

Versioned Prompt value type and the built-in prompts used by shipped cognitions.

See the Prompts concept for the mental model.

agentkit.agents.prompts — the prompt-management skeleton.

The framework defines WHAT a prompt is. Applications own storage, discovery, resolution, and any validation/evaluation contracts they choose to add — those don't belong in the framework's public surface unless the framework actually dispatches them.

The skeleton: - Prompt (data type) — id + version + template + bind() + render() - builtin — the framework's own seed prompts (Compactor)

Why no validator/evaluator skeleton: exporting Validator / PromptEvaluator type aliases the framework never dispatches would imply a runtime validation surface that doesn't exist. The Agent's parse-and-repair uses its own SchemaAdapter path. Apps that need that pattern ship their own shapes.

Why no store/registry/resolver: prompts are code (defined as constants, version-controlled in git). Apps that need runtime mutation, dynamic loading, or a UI to inspect prompt history ship their own infrastructure on top of this skeleton.

Prompt dataclass

Prompt(
    id: str,
    version: str,
    template: str,
    inputs: tuple[str, ...] = tuple(),
    bound: Mapping[str, Any] = FrozenDict(),
)

A versioned system prompt. The version label travels with every output the prompt produces (stamped via RequestBuilder onto AgentResult + traces), so a regression in quality maps back to a specific template version.

inputs class-attribute instance-attribute

inputs: tuple[str, ...] = field(default_factory=tuple)

The placeholder names template declares, e.g. ("tenant", "tone") for a template containing {tenant} and {tone}. Declared on the value so a caller can validate a prompt against its call site before a run, and so a stored prompt carries its own contract.

bound class-attribute instance-attribute

bound: Mapping[str, Any] = field(default_factory=FrozenDict)

Values already bound to the declared inputs. Normally set via :meth:bind rather than passed to the constructor. Because the values live on the value, a bound Prompt renders with no arguments — which is what lets every consumer in the framework (RequestBuilder, the CLI cognitions) keep calling render() with nothing while still shipping substituted text. Frozen to a read-only mapping in __post_init__.

bind

bind(**values: Any) -> Prompt

Return a copy of this prompt with values bound to its inputs.

The prompt is frozen, so this never mutates — it hands back a new value, which is the point: a bound prompt is still a pinnable, diff-able Prompt carrying the same id / version, and can be handed anywhere an unbound one goes (Agent(prompt=...), RequestBuilder).

Binding is incremental and last-write-wins, so partial binding at construction and the rest at the call site both work::

p.bind(tenant="acme").bind(tone="terse")   # == p.bind(tenant="acme", tone="terse")

An undeclared name raises ValueError HERE rather than at render() — the earlier the typo surfaces, the cheaper it is.

render

render(**values: Any) -> str

Substitute the declared inputs and return the prompt text.

Values come from :meth:bind and/or from **values here, with **values winning on a collision (a per-call override of a bound default). With no inputs declared this is what it has always been — the stripped template — so every existing caller is unaffected.

Substitution is a literal replacement of {name} for each DECLARED input, never str.format. That is deliberate: system prompts are full of braces that are not placeholders — a JSON Schema, an example payload, a code fence — and format would either raise KeyError on them or silently eat the doubling a user added to escape them. Replacing only the names the prompt declared leaves every other brace untouched.

Raises ValueError when an input is neither bound nor supplied, rather than rendering a half-filled prompt. A {tenant} that reaches the model unsubstituted is not a crash, which is exactly the problem: it is a plausible-looking prompt that quietly describes the wrong task.

compose

compose(
    parts: Sequence[PromptPart],
    inputs: Mapping[str, Any],
    *,
    max_chars: int = 200000,
    persona_screening: Callable[[str], str] | None = None,
    images: Sequence[ImagePart] = (),
) -> ComposedPrompt

Render every part strictly, refuse overflow, and keep untrusted text in user content.