keel
Context

Assembling context

Follow each source into the model’s opening input.

Keel assembles context once at the beginning of a turn, before entering the model loop. The agent supplies its declarations; the runtime combines them with the current request and ambient information.

Context · ordered sections
System
sys.time · identity · tenancy · rules
Instructions
prompt · skill.0 … skill.n
Tools
tool.<id> · allowed descriptions
State views
render.<block> · reader output
Conversation
history · current user brief
{ id: "render.profile",
  text: "User prefers concise answers.",
  budgetTokens: 128, usedTokens: 8 }
Inspect each source separately before it becomes model input.

Follow the section order

SectionSourcePurpose
sys.timeRuntime clockThe current time, always an ISO instant.
sys.<id>The app's ambient declarationOne section per app-declared ambient section — the scope's meanings, rendered by the app.
sys.rulesFrameworkShared instructions about honest answers.
promptagent.promptThe agent's instructions.
skill.0, skill.1, …agent.skillsAdditional reusable instructions.
tool.<id>Available toolsTool names and descriptions.
render.<block>.<reader>Granted state readersText rendered from stored values. Two grants on one block stay distinguishable by the reader suffix.
historyStored messagesThe selected recent conversation, if nonempty.
userCurrent briefThe request for this turn. Its sectionKind is "user".

The order groups instructions before state and conversation. Some system values, including time, change between turns, so this ordering does not guarantee provider prompt-cache hits.

Declare the ambient sections

The framework hard-codes only sys.time and sys.rules. Everything the window should know about who this turn serves comes from the app's own ambient declaration — defineAmbient pairs the scope schema with the sections rendered from it:

// src/ambient.ts
import { z } from "zod";
import { defineAmbient } from "@keel-dev/core";

export const appAmbient = defineAmbient({
  scope: z.object({
    displayName: z.string(),
    plan: z.enum(["solo", "teams"]),
    workspaceId: z.string().nullable(),
  }),
  sections: (ambient) => [
    { id: "identity", text: `You assist ${ambient.scope.displayName}.` },
    {
      id: "tenancy",
      text: ambient.scope.workspaceId
        ? `Workspace thread (plan: ${ambient.scope.plan}).`
        : "Personal thread — no workspace is attached.",
    },
  ],
});

Pass the declaration as ambient in createRuntime. The scope is validated at the admit gate on every root turn — a scope that fails the schema is refused with an ambient-invalid defect before any assembly. Each returned section becomes sys.<id> in the window (sys.identity and sys.tenancy here), with an optional budgetTokens per section (default 64).

Sections keep raw ids out of the window: render meanings — a display name, a plan description — not identifiers a model could repeat wrongly.

Write the agent's instructions

Keep your prompt beside the agent:

// src/agents/assistant/assistant.prompt.ts
export const ASSISTANT_PROMPT = `
Help the user organize their work.
Use the user's saved answer preference when responding.
If a requested action fails, explain what was not completed.
`.trim();

Import that value into your agent definition:

prompt: ASSISTANT_PROMPT,
skills: [
  "When summarizing notes, preserve dates and unresolved decisions.",
],

Skills here are strings of instructions. Listing them does not execute code, register a tool, or grant access to a state block.

Understand framework sections

System sections are supplied by the assembler on every turn rather than optional entries in agent.prompt — an app cannot build a turn without them. The framework never inserts raw identifiers itself, but application-authored section text can still contain them. The assembler does not scrub arbitrary identifiers or sensitive text; that discipline lives in your sections function.

Ambient sections and the honesty rules describe intended behavior to the model. Textual instructions are not a substitute for tool access checks, storage policy, or application authorization.

See what the adapter receives

The runtime joins opening sections other than user into the adapter's system text. It passes the current brief separately and supplies actual tool specifications derived from input schemas.

When a turn requires a deliverable, the runtime also appends delivery instructions and adds a submission tool. These are additions around the base assembly. See Budgets and fit for the limits of the opening estimate.

Inspect in Studio

Run a turn in Studio, then open CONTEXT. Find prompt, skill.0, and the system sections. Edit one instruction, start another turn, and compare its assembled text. This verifies what reached the model rather than relying only on changes in its answer.

Next: State and tool views.