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: absentmain/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-injectionsresolves the stage'scapability:refs against the registry and renders each fragment with the expander's ownsubstituteFragment— 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) —offskips ONLY the fragment (tools/logic/variables still expand, with a publish warning naming the suppression),customreplaces 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 whenmain !== 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-injectionsproxies to the orchestrator'sPOST /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'sform.watchconfig 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 staleauto_injectionsoverride 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_injectionson the stage. It rides the collab Y.Doc via the generic stage-config pass-through (no shape-converter change — pinned inpipeline-shape.test.ts, same rule ascapability_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 intests/test_llm_context_build.py. - ai-manager: the
auto_injectionsround-trip pin inlib/collab/pipeline-shape.test.ts.
See also¶
- Payments — the capability whose injected block carries the warning
- Customer Authentication
- Warm handoff / External Consult
- Universal pipeline builder — the phase model the injection sites live in