Message roles ​
Every message has exactly one role. The role determines the valid content shape and how provider adapters serialize the transcript.
1. System messages ​
A system message contains one instruction string:
import type { SystemMessage } from '@anvia/core'
const system: SystemMessage = {
role: 'system',
content: 'Act as a support assistant. Be concise and state uncertainty.',
}Use system messages when behavior belongs inside a manually managed transcript. Direct completions and agents can also receive separate instructions; keep the combined instruction hierarchy intentional.
2. User messages ​
A user message accepts a string or an array of text, image, and file blocks:
import type { UserMessage } from '@anvia/core'
const textOnly: UserMessage = {
role: 'user',
content: 'Why did this checkout fail?',
}
const multimodal: UserMessage = {
role: 'user',
content: [
{ type: 'text', text: 'Explain the error shown in this screenshot.' },
{
type: 'image',
image: { type: 'url', url: 'https://files.example.com/checkout-error.png' },
detail: 'high',
},
],
}A string is normalized to one text block. File and image support depends on the completion model.
3. Assistant messages ​
An assistant message stores model output such as text, images, files, reasoning, and tool calls:
import type { AssistantMessage } from '@anvia/core'
const assistant: AssistantMessage = {
role: 'assistant',
id: 'msg_123',
content: 'The payment provider rejected the authorization.',
}Preserve the provider message ID when it is available. Some adapters need message or content identifiers to continue provider-native state correctly.
4. Tool messages ​
A tool message returns the result of an assistant tool call:
const tool = {
role: 'tool',
content: [{
type: 'tool-result',
toolCallId: 'tool_123',
callId: 'call_123',
toolName: 'get_invoice',
output: { type: 'json', value: { status: 'paid', totalCents: 4900 } },
}],
} satisfies ToolMessageThe tool result must follow the assistant tool-call message it answers. Preserve both messages so later turns know what the model requested and what the application returned.
5. Attach strict-JSON metadata ​
Every message role accepts optional metadata:
const user: UserMessage = {
role: 'user',
content: 'Summarize this ticket.',
metadata: {
source: 'support-api',
ticketId: 'TICKET-1042',
labels: ['billing', 'urgent'],
},
}Metadata must be a strict JSON value: strings, finite numbers, booleans, null, arrays, and plain objects containing those values. Functions, undefined, BigInt, dates, sparse arrays, cycles, NaN, and infinities are rejected.
Metadata is retained by message, memory, UI, and observability flows, but provider adapters do not treat it as prompt content. Do not put facts there when the model must read them.
Continue with Content types.
Validate application metadata ​
Use createMessageSchema({ metadataSchema }) from @anvia/core or @anvia/core/completion when your storage boundary needs a specific metadata shape:
import { createMessageSchema, isMessage, parseMessage, parseMessages } from '@anvia/core'
import { z } from 'zod'
const ticketMessageSchema = createMessageSchema({
metadataSchema: z.object({ tenantId: z.string().min(1), ticketId: z.string().min(1) }).strict(),
})
const stored: unknown = {
role: 'user', content: 'Check this ticket.',
metadata: { tenantId: 'demo', ticketId: 'T-1' },
}
const message = ticketMessageSchema.parse(stored)
console.log(message.metadata?.ticketId)
const valid = isMessage(stored) // boolean type guard for the standard message contract
const standard = parseMessage(stored) // throws for an invalid standard message
const history = parseMessages([stored]) // validates an array of standard messages
console.log(valid, standard.role, history.length)Metadata is optional even with a custom schema; require its presence separately if your application needs it. isMessage checks standard strict-JSON message structure, not your tenant/ticket schema. Use ticketMessageSchema.safeParse(value) to branch without throwing under the custom contract.
The factory checks strict JSON before metadata parsing and again afterward. A transform that produces a Date, undefined value, or other non-JSON data is rejected too. This validates structure; it does not verify tenant ownership, message sequencing, or a caller's access to a transcript.