The directive content must be injected into the LLM request's messages array. This sounds trivial, but it is not — different injection strategies have different effects on model behaviour, and different models have different rules about system message placement and count. The three injection modes (from the `directiv
Milestone 2.3.3 — System Prompt Injection Strategy
Status: Planned
Goal: 2.3 — Directive resolution and prompt injection
Phase: 2 — Single Provider End-to-End
Estimated effort: 2 days
ADR required: ADR-0028 — System prompt injection strategy
Why This Milestone Exists
The directive content must be injected into the LLM request's messages array. This sounds trivial, but it is not — different injection strategies have different effects on model behaviour, and different models have different rules about system message placement and count.
The three injection modes (from the directives.injection_mode column):
system_first: Directive becomes the first system message; all existing system messages follow it. This is the strongest enforcement mode — the directive is always first in context.system_append: Directive is appended as a system message after existing system messages. Less likely to override client-supplied instructions.user_prepend: Directive is prepended to the first user message as[DIRECTIVE]: <content>\n\n<original>. For models that don't support multiple system messages.
This logic belongs in the proxy, not in the provider client. The provider client sees the already-injected messages array. The injection strategy is agent-level configuration — the provider doesn't know about it.
Branch
feature/m2-3-3-prompt-injection
PR Title
feat(proxy): system prompt injection middleware with configurable strategy (m2.3.3)
ADR-0028 — Prompt injection strategy
Document: why IBEX controls injection (not the client SDK), why three modes, what OpenAI's guidance says about multiple system messages, why user_prepend exists for compatibility, and how Phase 3 memory injection will follow the same pattern.
Deliverables
// packages/injection/injector.go
// InjectionMode controls how the directive is inserted into the messages array.
type InjectionMode string
const (
// SystemFirst: directive inserted as the first system message.
// Existing system messages follow it. Recommended for GPT-4o.
ModeSystemFirst InjectionMode = "system_first"
// SystemAppend: directive appended as the last system message.
// Existing system messages precede it.
ModeSystemAppend InjectionMode = "system_append"
// UserPrepend: directive prepended to the first user message.
// For models that do not support multiple system messages.
ModeUserPrepend InjectionMode = "user_prepend"
)
// Inject returns a new messages slice with the directive injected according to mode.
// If directive is empty, returns messages unchanged.
// The input messages slice is never mutated — a new slice is returned.
func Inject(messages []provider.Message, directive string, mode InjectionMode) []provider.MessageAcceptance Criteria
-
system_firstmode: directive is first message;role="system"; original messages follow -
system_appendmode: directive is last system message; user messages follow -
user_prependmode: directive is prepended to the first user message content - Empty directive: messages returned unchanged
- Input slice is never mutated (pure function — safe for concurrent use)
- Injection is tested with realistic GPT-4o message arrays including multi-turn history
Last updated on