Skip to content

Latest commit

 

History

History
411 lines (297 loc) · 21.2 KB

File metadata and controls

411 lines (297 loc) · 21.2 KB
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

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.

Overview

Context injection uses three independent, composable layers:

  1. Layer 1 — Dynamic Tool Registration — permission-driven tool resolution, registered in the LLM's native tool palette
  2. Layer 2 — Resolve Extends Hooks — routing layer that dynamically assigns an extends chain to a directive
  3. 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.

Layer Composition

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.

Layer 1 — Dynamic Tool Registration

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/readrye_file_system_read)
  • The tool's JSON Schema from CONFIG_SCHEMA
  • A _primary field 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.

Token Budget and Prioritization

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: 2000

Setting 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

Layer 2 — Resolve Extends Hooks

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).

Layer 3 — Extends Chain

Directives can extend base operating contexts that carry Identity, Behavior, tool protocol knowledge, permissions, and suppressions through an inheritance chain.

Standard Base Contexts

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

Decomposed Tool Protocol

ToolProtocol has been decomposed into 3 per-primary knowledge items, each explaining how to call one Rye primary action:

  • rye/agent/core/protocol/execute
  • rye/agent/core/protocol/fetch
  • rye/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.

Chain Composition

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

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.

Message Assembly Order

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.

<context> in Directives

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.

Suppressing Context Layers

The <suppress> tag skips a context layer inherited from the extends chain. It matches against:

  • The full knowledge item_id of 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.

Provider-Specific Delivery

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.

Project-Level Context Customization

Projects can customize what context threads receive without modifying system files or individual directives.

Override via Knowledge Items

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.

Additive Hooks via hooks.yaml

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.

Dynamic Routing via resolve_extends Hooks

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.

Precedence Summary

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

Hooks vs Extends Chain vs <context>

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

Transcript Rendering

Context injection produces two transcript events:

system_prompt

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.

context_injected

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.

Token Budget

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.

What's Next