id: context-injection
title: "Context Injection"
description: How layered context injection delivers knowledge to threads through dynamic tool registration, routing hooks, and extends chains
category: orchestration
tags: [context, extends, hooks, tool-schemas, layers]
version: "2.0.0"Context injection is the system that loads knowledge into threads before the LLM starts reasoning. It prevents thinking loops ("what tools do I have?"), tool avoidance ("I don't know how to use that"), and identity confusion ("who am I?") by front-loading the information the agent needs.
Context injection uses three independent, composable layers:
- Layer 1 — Dynamic Tool Registration — permission-driven tool resolution, registered in the LLM's native tool palette
- Layer 2 — Resolve Extends Hooks — routing layer that dynamically assigns an
extendschain to a directive - Layer 3 — Extends Chain — delivers the operating context (identity, behavior, tool protocol knowledge) via inheritance
These layers combine to give the agent a complete working context on its first turn — no discovery required. Each layer is optional; a directive can use any combination or none at all.
| Configuration | What the thread gets |
|---|---|
| Permissions only (layer 1) | Granted tools registered in LLM's tool palette. No identity, no behavioral framing. |
| Permissions + hooks (layer 1+2) | Registered tools + hook routes into an extends chain → full operating context |
| Permissions + extends (layer 1+3) | Registered tools + explicit inherited context, identity, behavior |
| All three (layer 1+2+3) | Full stack — hook routing + inheritance + registered tools |
| Nothing declared | Only primary actions available. No additional tools or context. |
Key principle: Identity, Behavior, and tool protocol knowledge are delivered via the extends chain's <context> blocks. A directive without extends gets NO inherited identity/behavior context. It gets tool schemas from Layer 1 only if it declares permissions.
After capabilities are resolved, the system scans harness._capabilities for rye.execute.tool.* capability strings. Matching tool files are resolved across three-tier space (project → user → system), and their CONFIG_SCHEMA and __tool_description__ are extracted via AST parsing — safe, no execution.
Instead of injecting schemas as text, the resolved tools are registered directly in the LLM's native tool palette as individual tool definitions. Each tool gets:
- A flattened API name (e.g.,
rye/file-system/read→rye_file_system_read) - The tool's JSON Schema from
CONFIG_SCHEMA - A
_primaryfield set to"execute"for dispatch routing
Primary actions (rye/fetch, rye/sign) are also registered with their own _primary field matching their action (e.g., _primary: "fetch"). The runner uses a tool_primary_map to route all calls uniformly — primary and non-primary actions are peers.
A capabilities tree is also generated by walking the filesystem (respecting bundle ignore patterns) and rendered in thread transcript metadata for observability.
Primary actions are included in the registration and treated as peers — they are no longer a separate category.
The token budget controls how many non-primary action definitions are registered in the LLM's tool palette (default ~2000 tokens, estimated at 4 chars per token based on JSON schema size + description length). The budget is configured via the tool_preload section in resilience.yaml:
# .ai/config/agent/resilience.yaml
tool_preload:
enabled: true
max_tokens: 2000Setting enabled: false disables tool registration entirely. When the budget is tight, specific capability strings (e.g. rye.execute.tool.project/deploy/kubectl) are prioritized over broad wildcards (e.g. rye.execute.tool.*).
Implementation: tools/rye/agent/threads/loaders/tool_schema_loader.py
The resolve_extends lifecycle event fires during step 4 of thread_directive.execute(), after the directive is parsed but before the extends chain is resolved. Hooks registered for this event can dynamically set or override the extends attribute of a directive.
Hook context includes:
| Field | Type | Description |
|---|---|---|
directive |
str |
The directive name/ID |
has_extends |
bool |
Whether the directive already declares extends |
category |
str |
The directive's category |
inputs |
dict |
Inputs passed to the directive |
model |
str |
The model configuration |
First match wins — hooks are evaluated in layer order, and the first hook whose condition matches terminates the resolution loop. This enables dynamic routing without modifying the directive itself:
# .ai/config/agent/hooks.yaml — route deploy directives to a deploy context
hooks:
- id: "route_deploy_extends"
event: "resolve_extends"
layer: 2
condition:
path: "directive"
op: "contains"
value: "deploy"
action:
set_extends: "project/contexts/deploy"If no hook matches and the directive has no extends declared, it proceeds without an extends chain (Layer 3 is skipped entirely).
Directives can extend base operating contexts that carry Identity, Behavior, tool protocol knowledge, permissions, and suppressions through an inheritance chain.
Three standard bases are provided:
| Base | Permissions | Protocol Items | Description |
|---|---|---|---|
rye/agent/core/base |
Full (execute, fetch, sign) | All 3 protocol items + Identity + Behavior | Standard operating context |
rye/agent/core/base_execute_only |
Execute only | protocol/execute + Identity + Behavior |
Narrow — execute tools only |
rye/agent/core/base_review |
Read-only file access | protocol/execute, fetch + Identity + Behavior |
Review and analysis |
ToolProtocol has been decomposed into 3 per-primary knowledge items, each explaining how to call one Rye primary action:
rye/agent/core/protocol/executerye/agent/core/protocol/fetchrye/agent/core/protocol/sign
Base contexts declare only the protocol items that match their permissions — base_execute_only includes only protocol/execute, while base includes all four. This means what you can do determines what you're told about — permission attenuation through the chain also controls context narrowing.
When a directive uses extends, context items from the entire inheritance chain are composed root-first:
<!-- rye/agent/core/base declares: -->
<context>
<system>rye/agent/core/Identity</system>
<system>rye/agent/core/Behavior</system>
<before>rye/agent/core/protocol/execute</before>
<before>rye/agent/core/protocol/fetch</before>
<before>rye/agent/core/protocol/sign</before>
</context>
<!-- project/deploy/base extends rye/agent/core/base, declares: -->
<context>
<before>project/deploy/environment-rules</before>
</context>
<!-- deploy_staging (leaf) extends project/deploy/base, declares: -->
<context>
<after>project/deploy/completion-checklist</after>
</context>Resolution walks the chain leaf → parent → root, then reverses to compose root-first:
Chain: rye/agent/core/base → project/deploy/base → deploy_staging
System: [Identity, Behavior] ← from root
Before: [protocol/execute, fetch, sign, environment-rules] ← root + middle
After: [completion-checklist] ← from leaf
Duplicates are deduplicated — if both parent and child declare the same knowledge item, it appears only once. Circular extends chains are detected and rejected.
See Authoring Directives — Directive Inheritance for the directive-side syntax.
User message context is injected into the first user message via thread_started hooks (or thread_continued for resumed threads). Two built-in hooks inject before the directive body:
| Position | Built-in Hook | Knowledge Item | wrap |
Purpose |
|---|---|---|---|---|
before |
ctx_environment |
rye/agent/core/Environment |
true |
Runtime environment, project info |
before |
ctx_directive_instruction |
rye/agent/core/DirectiveInstruction |
false |
How to interpret directive bodies |
Hooks can set wrap: false to inject content without XML wrapping. By default, injected knowledge is wrapped in an XML tag derived from the knowledge item's name field (e.g. <Environment id="rye/agent/core/Environment" type="knowledge">...</Environment>). With wrap: false, the raw content is injected directly — this is used for DirectiveInstruction which needs to appear as plain instructions before the directive body, not as a tagged context block.
With all layers active, the first user message is assembled as:
hook before-context (environment) ← from thread_started hooks (wrapped)
hook before-context (dir. instr) ← from thread_started hooks (raw, wrap: false)
extends before-context ← from extends chain <before> items (protocol, domain knowledge)
directive before-context ← from directive's own <before> items
directive prompt (body + outputs) ← from _build_prompt()
directive after-context ← from <after> knowledge items
Tool schemas are not part of the user message — they are registered in the LLM's native tool palette (Layer 1).
A context_injected event is emitted to the transcript recording which sources contributed.
Directives can declare additional knowledge items to inject using the <context> metadata section:
<directive name="deploy_staging" version="1.0.0">
<metadata>
<context>
<system>project/deploy/system-rules</system>
<before>project/deploy/environment-rules</before>
<after>project/deploy/completion-checklist</after>
<suppress>rye/agent/core/protocol/sign</suppress>
</context>
...
</metadata>
</directive>These items are loaded at thread startup and merged with extends-chain context:
| Tag | Destination |
|---|---|
<system> |
Appended to the system message (after extends-chain layers) |
<before> |
Injected between extends-chain before-context and directive body |
<after> |
Injected after directive body |
<suppress> |
Skips a named context layer from the extends chain |
Directive-declared context items are resolved via the load tool (same as knowledge items loaded by hooks), so they follow the standard three-tier resolution: project → user → system.
The <suppress> tag skips a context layer inherited from the extends chain. It matches against:
- The full knowledge
item_idof a context entry (e.g.rye/agent/core/protocol/sign) - Context entries declared at any level of the extends chain
This is useful when a directive needs to remove an inherited layer or replace it with something custom:
<context>
<suppress>rye/agent/core/protocol/sign</suppress>
<before>project/custom-sign-protocol</before>
</context>Suppressions compose through extends chains — if any directive in the chain suppresses a layer, it stays suppressed for all descendants.
System messages (from <system> context entries in the extends chain and directives) are passed to the LLM via each provider's native system message mechanism:
| Provider | How system messages are sent |
|---|---|
| Anthropic | system parameter on the messages API call |
| OpenAI | {"role": "system", "content": "..."} message |
| Gemini | system_instruction parameter |
The runner passes the assembled string to the provider — each provider adapter maps it to the correct API field.
Projects can customize what context threads receive without modifying system files or individual directives.
The simplest approach: create a project-level knowledge item that shadows the system default. FetchTool cascades project → user → system, so a project file wins:
.ai/knowledge/rye/agent/core/Identity.md ← project override
All threads in the project that extend a base context will load this instead of the system identity. No other changes needed.
Add extra context for specific directive categories using .ai/config/agent/hooks.yaml:
hooks:
- id: "project_deploy_rules"
event: "thread_started"
layer: 2
position: "before"
condition:
path: "directive"
op: "contains"
value: "deploy"
action:
primary: "fetch"
item_type: "knowledge"
item_id: "project/deploy/rules"This adds deploy-specific context alongside the default layers — it doesn't replace anything.
For dynamic context switching — different operating contexts for different directive types — use resolve_extends hooks in .ai/config/agent/hooks.yaml:
hooks:
# Web directives get a specialized base context with web-specific identity
- id: "route_web_extends"
event: "resolve_extends"
layer: 2
condition:
path: "directive"
op: "contains"
value: "web"
action:
set_extends: "project/contexts/web-agent-base"
# Deploy directives get a deploy-specific context chain
- id: "route_deploy_extends"
event: "resolve_extends"
layer: 2
condition:
path: "directive"
op: "contains"
value: "deploy"
action:
set_extends: "project/contexts/deploy-base"This routes web directives into a project/contexts/web-agent-base extends chain (which can declare its own Identity, Behavior, and protocol knowledge), while deploy directives get a different chain. Directives that don't match any hook proceed with their declared extends (or none).
The condition evaluator supports:
| Operator | Example | Matches when |
|---|---|---|
eq |
{path: "directive", op: "eq", value: "init"} |
Exact match |
contains |
{path: "directive", op: "contains", value: "web"} |
Substring match |
regex |
{path: "directive", op: "regex", value: "^project/deploy/"} |
Regex match |
in |
{path: "model", op: "in", value: ["gemini", "claude"]} |
Value in list |
not |
{not: {path: "directive", op: "contains", value: "web"}} |
Inverts child |
any |
{any: [{...}, {...}]} |
Any child matches |
all |
{all: [{...}, {...}]} |
All children match |
The context dict available to conditions includes: directive, has_extends, category, inputs, model.
| Mechanism | Scope | Effect |
|---|---|---|
| Project knowledge item override | All threads in project | Shadows system knowledge via FetchTool cascade |
Project hooks.yaml (thread_started) |
All threads matching condition | Adds extra context hooks |
Project hooks.yaml (resolve_extends) |
All threads matching condition | Dynamically routes directives into extends chains |
Directive extends |
Single directive | Inherits full operating context from a base directive |
Directive <suppress> |
Single directive | Skips specific extends-chain context layers |
Directive <before>/<after>/<system> |
Single directive | Adds extra knowledge items |
| Aspect | thread_started Hooks |
resolve_extends Hooks |
Extends Chain <context> |
Directive <context> |
|---|---|---|---|---|
| Definition | hooks.yaml |
hooks.yaml |
Base directive <context> metadata |
Leaf directive <context> metadata |
| When | Dynamic — evaluated at runtime with conditions | Dynamic — evaluated before chain resolution | Static — declared in base directive | Static — declared at authoring time |
| Conditional | Yes — condition field with full evaluator |
Yes — first match wins | No — always loaded when extended | No — always loaded if declared |
| Delivers | Environment info, directive instruction | Routes into an extends chain | Identity, Behavior, tool protocol | Domain knowledge specific to a task |
| Use case | Infrastructure concerns, runtime context | Dynamic identity/context switching per category | Foundational operating context | Task-specific knowledge |
Context injection produces two transcript events:
Emitted after system message assembly. Contains the full system prompt text and the list of contributing context sources from the extends chain:
{
"event": "system_prompt",
"text": "You are a Rye agent...",
"layers": ["rye/agent/core/Identity", "rye/agent/core/Behavior"]
}The layers array reflects which <system> context entries from the extends chain contributed to the system message. If no extends chain is active, the layers array is empty.
Emitted after user message context is assembled. Records which sources contributed before/after content:
{
"event": "context_injected",
"before": ["ctx_environment", "ctx_directive_instruction", "rye/agent/core/protocol/execute", "rye/agent/core/protocol/fetch"],
"after": []
}These events are useful for debugging — they show exactly what the LLM received and where it came from.
Context injection overhead depends on which layers are active:
| Source | Approximate Tokens | Notes |
|---|---|---|
| Tool registration (Layer 1) | 0 (message) / ~200–2000 (tool palette) | Tools registered in native tool palette, not injected as text. Budget controls how many tools are registered. |
| Environment + directive instruction | ~200 | Fixed cost from thread_started hooks |
| Identity + Behavior (Layer 3) | ~400 | From extends chain <system> entries |
| Protocol items (Layer 3) | ~100–400 | Depends on how many protocol items the base declares |
Tool schemas are registered in the LLM's native tool palette rather than injected as text, so they don't consume user message tokens. The palette cost is handled by the LLM provider. This eliminates "what tools do I have?" discovery loops on the first turn.
A fully-loaded thread (all three layers active with the base context) adds approximately ~600–1,000 tokens to the first user message (environment, identity, behavior, protocol items). Tool palette overhead (~200–2000 tokens) is separate and provider-managed. This pays for itself on the first turn by:
- Eliminating "what tools do I have?" discovery loops (saves 2–3 turns)
- Preventing tool call format errors (saves retry turns)
- Providing directive instruction so the agent knows how to interpret directive bodies
A directive with no extends chain and no permissions adds only the ~200-token environment/instruction overhead.
For a haiku-tier thread at ~$0.001/turn, the context overhead costs less than $0.002 and saves $0.002–0.003 in avoided discovery turns.
- Authoring Directives — Context Injection — How to declare
<context>and<suppress>in directives - Authoring Knowledge — How to create knowledge items for context injection
- Permissions and Capabilities — How capabilities control thread access
- Safety and Limits — Cost controls and the SafetyHarness