feat(ui): add `UIAdapter.manage_system_prompt` + `ReinjectSystemPrompt` capability

Merged
DS
6mo ago
+1661-36

Summary

Two changes, both composing around a new capability.

manage_system_prompt on UIAdapter

New manage_system_prompt: Literal['server', 'client'] = 'server' on [UIAdapter][pydantic_ai.ui.UIAdapter] (and the handle_ag_ui_request / run_ag_ui convenience functions, and VercelAIAdapter).

  • 'server' (default): any SystemPromptPart sent by the frontend is stripped with a warning, since a malicious client could otherwise inject arbitrary instructions via crafted API requests. The agent's own system_prompt is then reinjected at the head of the first request via the [ReinjectSystemPrompt][pydantic_ai.capabilities.ReinjectSystemPrompt] capability (added automatically in this mode).
  • 'client': frontend SystemPromptParts are preserved as-is. The agent's configured system_prompt is not injected — the caller is fully responsible for sending it on every turn if desired. To opt into the fallback behavior anyway, add ReinjectSystemPrompt to the agent's capabilities.

If you want per-request guidance that doesn't need to live in message history, prefer instructions over system_prompt.

ReinjectSystemPrompt capability

New capability at [pydantic_ai.capabilities.ReinjectSystemPrompt][pydantic_ai.capabilities.ReinjectSystemPrompt]. Ensures the agent's configured system_prompt is at the head of the first ModelRequest on every model request. If any SystemPromptPart is already present anywhere in the history (preserved from a prior run, handed off from another agent, or sent by a client), the capability is a no-op.

Useful for any scenario where message_history comes from a source that doesn't round-trip system prompts — UI frontends, database persistence layers, conversation compaction pipelines. Community users have asked for this repeatedly on #1646; shipping it as an opt-in capability avoids breaking the documented contract of agent.run(message_history=[...]) while giving people a single one-line opt-in.

Supporting infrastructure:

  • New public Agent.system_prompts(...) method on [AbstractAgent][pydantic_ai.agent.AbstractAgent] that resolves configured static strings + runner functions into SystemPromptParts, reusable by anyone who needs to resolve system prompts outside an agent run.
  • New capabilities= run-time kwarg on Agent.run / Agent.iter / Agent.run_stream / Agent.run_stream_events — additive with the agent's configured capabilities (matches the toolsets= / builtin_tools= pattern). Used internally by UIAdapter to opt into the capability in server mode.

A # TODO (v2): comment marks the capability for potential default inclusion in v2 — which would close #1646 directly, at the cost of a documented behavior change to agent.run(message_history=[...]).

Behavior change (security-relevant)

Frontend-originated SystemPromptParts are now stripped by default in UI adapter flows and a warning is emitted. Rationale: a malicious client could otherwise inject arbitrary instructions via crafted API requests. The agent's configured system_prompt is authoritative in this default mode and is reinjected when missing.

Callers who intentionally let the frontend own the system prompt must opt in with manage_system_prompt='client'.

Pre-Review Checklist

  • Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
  • No breaking changes in accordance with the version policy.
  • Linting and type checking pass per `make format` and `make typecheck`.
  • PR title is fit for the release changelog.

Pre-Merge Checklist

  • New tests for any fix or new behavior, maintaining 100% coverage.
  • Docs updated: `docs/capabilities.md` documents the capability with an example; `docs/message-history.md` points to the capability for histories missing sys_parts; `docs/ui/ag-ui.md` and `docs/ui/vercel-ai.md` reframed around the `'server'` / `'client'` ownership model.