Examples
Context Assembly
How to decide what belongs in instructions, messages, memory, retrieval, documents, and tools.
Context assembly decides how information enters a run. Use the narrowest reliable channel for each kind of information instead of stuffing everything into one prompt.
Scenario
A customer asks, “Can I change the shipping address for A-100 like last time?” The answer needs stable support behavior, conversation history, prior preferences, policy documents, and fresh order state.
Channels
| Channel | Put this there | Avoid |
|---|---|---|
| instructions | stable behavior, safety rules, response style | request-specific user ids or live product facts |
| static context | small fixed docs needed by every run | large or frequently changing knowledge |
| messages | the current conversation | durable facts that should be normalized |
| memory/session | prior messages or durable conversation state | permission decisions or current order state |
| dynamic context | retrieved evidence from indexed knowledge | account-specific live state |
| document input | files the provider can read directly | documents that need indexing, filtering, or reuse |
| tools | live product state, permissions, writes, expensive reads | policy text that should be retrieved as evidence |
Example
import { AgentBuilder } from "@anvia/core";
import { vectorFilter } from "@anvia/core/vector-store";
const CHECKOUT_AGENT_INSTRUCTIONS = [
"Answer checkout questions for signed-in customers.",
"Use retrieved policy for policy answers.",
"Use tools for live order state and account-specific actions.",
"If policy evidence is missing, say what needs to be checked.",
].join("\n");
export function createCheckoutAgent(scope: CheckoutAgentScope) {
return new AgentBuilder("checkout-support", scope.model)
.instructions(CHECKOUT_AGENT_INSTRUCTIONS)
.context(
"Company support tone: concise, direct, and specific about next steps.",
"support_tone",
)
.memory(scope.memoryStore, { savePolicy: "turn" })
.dynamicContext(scope.policyIndex, {
topK: 4,
threshold: 0.72,
filter: vectorFilter.and(
vectorFilter.and(
vectorFilter.eq("productArea", "checkout"),
vectorFilter.eq("visibility", "public"),
),
vectorFilter.eq("locale", scope.user.locale),
),
format: (result) => ({
id: result.id,
text: [
`<policy id="${result.id}" updated="${result.metadata?.updatedAt ?? "unknown"}">`,
String(result.document),
"</policy>",
].join("\n"),
}),
})
.tools([
createLookupOrderTool(scope),
createChangeAddressTool(scope),
createTicketTool(scope),
])
.build();
}
export async function answerCheckoutQuestion(input: CheckoutQuestionInput) {
const user = await input.auth.requireUser();
const agent = createCheckoutAgent({ ...input, user });
const session = agent.session(input.conversationId, {
userId: user.id,
metadata: { tenantId: user.tenantId },
});
return session.prompt(input.message).send();
}
Fresh order state stays behind tools because tools can enforce permissions and read current data. Support policy goes through dynamic context because it is searchable evidence. Conversation history goes through memory because it is part of the session, not a product authorization source.
Decision Rules
| Question | Usually choose |
|---|---|
| Is it stable behavior? | instructions |
| Is it part of the conversation? | messages or memory |
| Is it reusable knowledge selected by relevance? | dynamic context |
| Is it a user-provided file for this run only? | document input when supported |
| Is it live product state? | tool |
| Can it change the world? | permissioned tool with validation, approval, idempotency, and audit |
Failure Modes
- Current account state is copied into memory instead of read through tools.
- Tenant filtering is applied after retrieval instead of in the retrieval query.
- Retrieved docs are treated as permission to access account data.
- Large static context crowds out relevant messages and tool results.
- The same fact is stored in memory, retrieval, and product tables with no source of truth.
