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.
sys.time · identity · tenancy · rulesprompt · skill.0 … skill.ntool.<id> · allowed descriptionsrender.<block> · reader outputhistory · current user brief{ id: "render.profile",
text: "User prefers concise answers.",
budgetTokens: 128, usedTokens: 8 }Follow the section order
| Section | Source | Purpose |
|---|---|---|
sys.time | Runtime clock | The current time, always an ISO instant. |
sys.<id> | The app's ambient declaration | One section per app-declared ambient section — the scope's meanings, rendered by the app. |
sys.rules | Framework | Shared instructions about honest answers. |
prompt | agent.prompt | The agent's instructions. |
skill.0, skill.1, … | agent.skills | Additional reusable instructions. |
tool.<id> | Available tools | Tool names and descriptions. |
render.<block>.<reader> | Granted state readers | Text rendered from stored values. Two grants on one block stay distinguishable by the reader suffix. |
history | Stored messages | The selected recent conversation, if nonempty. |
user | Current brief | The 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.
