Skip to content

Prompt auto-injections

Status: Current | Last reviewed: 2026-09-01

Every agent prompt the model actually receives is the author's text plus blocks the platform injects. This page covers what gets injected, where each block comes from, and how an author sees and overrides them.

The three block families

Block Source Default
Platform context Code (build_platform_context_preamble) — the current date/time in UTC, stamped at session build On for every stage
Tenant instructions tenant_settings key global_instructions ({enabled, text}), edited at Settings → Tenant Settings → Instructions (admin-only, same gating as Knowledge/Telemetry) On when the tenant has text and enabled
Capability guidance Each enabled capability's own prompt builder — Payments, Customer Authentication, External Consult (origin + transfer agent), End Session On whenever the capability is on

The platform context exists because a model's guess at "today" is its training cutoff, delivered confidently — the reported symptom was agents not knowing the current date. The preamble is UTC with an explicit label; a per-tenant local timezone is a plausible later addition, but a wrong local time asserted confidently is worse than a right UTC one.

The one runtime decision point

lib/pipecat/pipeline/auto_injections.py is the single module that decides whether each block ships and with what text, driven by the stage-level config.auto_injections key:

"auto_injections": {
  "platform_context": false,        // suppress the date/time preamble
  "tenant_instructions": false,     // suppress the tenant block
  "capabilities": {
    "payments":                { "mode": "off" },                 // remove
    "customer_authentication": {
      "mode": "custom", "text": "…",                            // replace
      "main": false,                                               // hide from Main
      "subagents": { "billing": false }                           // hide there
    }
    // absent / mode "default" = built-in text
  }
}

Three rules, each earned:

  • Absent config means everything ships. Only a literal false / mode: "off" suppresses anything, so every agent authored before the feature existed is byte-identical. Per-prompt switches are sparse too: absent main / subagents[topic_id] means visible.
  • A malformed override falls back to the DEFAULT text, never to silence. Losing an author's customization is recoverable; a payments stage running with no Rule 1 is not.
  • The tenant-instructions DB read fails OPEN (to "no instructions") with a ~60 s TTL cache — an admin edit lands without a restart, and a DB hiccup never blocks a session build.

Ambient blocks (platform + tenant) are appended in phase 3 (build_llm_context), after template resolution — the template path replaces raw_config["messages"], so an earlier injection would be overwritten — and before the consult prompt appends. Capability overrides are consulted at each capability's existing append site (payments/customer-auth/ end-session in phase 1 tool_wiring.py, the two consult roles in phase 3). Overriding changes only the injected guidance text — the capability's tools always register.

Coverage boundary: the blocks ride the LLMContext messages, so they reach cascaded LLM stages, Nova Sonic, OpenAI Realtime, and ADK Live. Services that consume a prompt only at create() time (Moshi, ElevenLabs Agents, GECX) do not get them — the same boundary the consult prompts and tool instructions already have.

Registry capabilities (capability:<slug>)

A registry capability's prompt text is injected by a DIFFERENT mechanism than the built-ins: the manifest's prompt.fragment is appended to the stage's authored prompt by capability-expander.ts at publish (and at the orchestrator's draft-expansion bridge for Preview), not by an orchestrator prompt builder. Both halves are still covered:

  • Preview text: /api/prompt-injections resolves the stage's capability: refs against the registry and renders each fragment with the expander's own substituteFragment — so the chip shows the same {{capability_config.<key>}}-substituted text the expansion will embed.
  • Overrides: the expander reads the same auto_injections.capabilities[<slug>] shape (capabilityFragmentOverride) — off skips ONLY the fragment (tools/logic/variables still expand, with a publish warning naming the suppression), custom replaces it (still substituted), malformed falls back to the default. Because Preview runs the expanded draft, the override applies there too.
  • Per-prompt visibility: expansion records the exact rendered fragment in _capability_prompt_segments[slug]. It appends the fragment to the initial Main prompt only when main !== false; the runtime refresher uses the sidecar to append it to each subagent according to its topic-id switch. Generated guidance never gets written into the authored subagent text.

Regressions: the three auto_injections override cases in capability-expander.test.ts.

Agent Logic (#when) stages

The InstructionRefresher re-renders a #when-bearing prompt and performs subagent prompt takeover when session variables change. It removes only the exact capability paragraphs recorded in _capability_prompt_segments, then recomposes the paragraphs enabled for Main or the active topic. The remaining post-resolution tail (tool instructions and ambient blocks) is captured once and restored on each refresh. This prevents both duplicate capability text and the older failure where the first variable change stripped payments rules and other appends.

The payments warning (warn, not lock)

The payments guidance's Rule 1 ("never ask the caller to say a card number…") is what keeps PANs away from the model, so editing or removing that block is a compliance risk. Product decision: warn, don't lock — the runtime logs a WARNING for any non-default payments injection, and the editor's payments row shows a destructive callout. The secure capture path itself (DTMF gate, vault) is unaffected by the prompt override.

What the author sees

The prompt editor (Behavior tab) renders the blocks inline, as chips inside the editor's own frame — a strip at the bottom of the prompt box, because the runtime appends these blocks after the authored text, so that is where they truthfully sit (InjectedPromptChips.tsx, rendered through PromptEditor's footer slot; the frame wraps the CodeMirror host and the footer as siblings, since CM appends its DOM imperatively and must not share a div with React-rendered children). The chips are React chrome, never document text — same rule as the #when placeholder hints: injected text written into the stored prompt would ship to the model twice, churn collab, and freeze against future default changes. Clicking a chip opens a detail panel under the strip:

  • Each block shows the exact text the runtime would inject. Capability texts are rendered by the orchestrator itself — ai-manager's POST /api/prompt-injections proxies to the orchestrator's POST /capability-prompts, which runs the same prompt builders as the live build, so there is no TS mirror to drift. The fetch re-keys on a debounced signature of the injection-relevant config (never once-per-mount — the inspector's form.watch config starts as a pre-reset transient with no tools), so chips appear and disappear live as capabilities are toggled. Chip presence treats the server answer as authoritative — a stale auto_injections override for a since-removed capability is harmless config residue and renders nothing; only a failed fetch falls back to the override keys. An unreachable orchestrator degrades to generic descriptions, never an authoring-blocking error.
  • A capability detail also lists the tools that capability contributes. Those tools remain available in the prompt editor's @ / {{ menus, but do not appear as independently removable rows in the ordinary Actions strip. The capability is their owner and is the place an author inspects them.
  • Platform context and tenant instructions get a stage suppress toggle. Each Main/subagent prompt tab gets its own Include this guidance switch for every capability. Capability text is shared across the stage, so Edit shared text and Remove everywhere affect Main and every subagent; the scope switch hides it only in the prompt whose tab is open. A removed block renders as a struck-through dashed chip (still clickable to restore); a customized one is amber with an "edited" marker — the state is visible without opening anything. Chip presence is the union of the server-reported blocks and any configured overrides, so a "removed" block never disappears just because the preview fetch failed.
  • Edits write config.auto_injections on the stage. It rides the collab Y.Doc via the generic stage-config pass-through (no shape-converter change — pinned in pipeline-shape.test.ts, same rule as capability_configs), and it is declared in the generated config JSON Schema (generate-config-schemas.ts).

Published rows contain the registry fragment and concrete tools because that is the executable snapshot. The editor projects _capability_provenance back to the authored view: it removes only exact publish-appended fragment suffixes from the visible document, restores the capability footer chip from the pinned manifest, and tags its expanded tools with their capability owner. Copy full prompt and runtime rendering still use the unmodified expanded config. This keeps draft and published editor presentation aligned without changing the published execution contract. Regression coverage lives in capability-authoring-view.test.ts.

Subagent Copy full prompt projects that subagent's sparse visibility onto the renderer's Main switch before sending the temporary config. The copied prompt therefore matches that tab without mutating stored config.

Copy the full prompt

The prompt editor's slim in-frame header has one action: Copy full prompt (FullPromptHeader.tsx, via PromptEditor's header slot). It copies the prompt the model would actually receive: authored text with tokens expanded and variables substituted, plus every platform append — rendered server-side on click by POST /api/prompt-injections/render, which first expands registry capability: refs (the expander is their runtime mechanism) and then calls the orchestrator's POST /render-prompt (prompt_preview.py — the real PromptResolver plus the same append helpers as a live build, replayed in build order). Render warnings surface in the toast: an unresolved {{TOOL:}} token, and any {{variable}} that rendered EMPTY (chevron silently blanks a variable with no value — a live call without that session data reads the same way). Multi-line {{COMPONENT:…}} expansions are set off with newlines at substitution so a mid-sentence token's tail no longer glues onto the block's last prop line (this also improves live prompts). Unlike the chip preview this path does not silently degrade: a copy of the wrong prompt is worse than no copy, so an unreachable renderer is a 502 the UI toasts. Deliberately absent: runtime-only turns (Nova greeting trigger, session-switch patching, text-turns silence sentinel) — they are conversation mechanics, not prompt content.

The template-resolution carry (bug found building this)

Phase 1 appends capability guidance to raw_config["messages"], but a templated stage's phase-3 resolution replaces messages with the rendered template — so every editor-authored stage (the editor always writes template.inline) silently lost payments/customer-auth/end-session/DTMF/A2UI guidance from the LLMContext. OpenAI Realtime masked it (its session-level instructions extract from the PRE-resolution messages); Nova and cascaded stages quietly degraded. build_llm_context now carries the appended tail across the resolution (the system message is the template mirror + appends, so the suffix past template.inline is exactly what phase 1 added — same prefix rule as the InstructionRefresher). Regressions: the two test_template_resolution_* cases in tests/test_llm_context_build.py.

Regressions

  • Orchestrator: tests/test_auto_injections.py (defaults-ship, malformed-override fallback, fail-open tenant read, append semantics, and Main/subagent scope composition) plus the preamble assertion in tests/test_llm_context_build.py.
  • ai-manager: the auto_injections round-trip pin in lib/collab/pipeline-shape.test.ts.

See also