Runtime context
Carry server-side state through a run without putting it in the prompt, and mark tools whose schema is only known at runtime.
runtimeContext carries server-side state through a whole run without putting
it in the prompt: tenant, request id, feature flags, a running budget. It is
readable in prepareStep, replaceable from there, and lands on every
StepResult:
let result = try await generateText(
model: model,
prompt: "Do the work.",
tools: [tool],
prepareStep: { context in
let used = context.runtimeContext?["toolCalls"]?.intValue ?? 0
return PrepareStepResult(runtimeContext: ["toolCalls": .number(Double(used + 1))])
},
runtimeContext: ["tenant": "acme", "toolCalls": .number(0)]
)Per-tool context
Per-tool state stays in toolsContext, where each tool sees only its own
entry. A tool can validate that entry and even describe itself from it:
let weather = Tool(
name: "weather",
description: "Get the weather.",
parameters: Schema.object(["city": .string()])
) { arguments, options in
try await fetch(city: arguments["city"]?.stringValue ?? "",
key: options.context?["apiKey"]?.stringValue ?? "")
}
.withContextSchema(Schema.object(["apiKey": .string(), "unit": .string()]))
.describing { context in
"Get the weather in \(context?["unit"]?.stringValue ?? "celsius")."
}Invalid context fails that tool call with AIError.invalidToolContext instead
of reaching the tool.
Neither runtimeContext nor toolsContext reaches telemetry spans unless you
name the keys. See telemetry.
Dynamic tools
Tools whose schema is only known at runtime, like MCP tools and user-defined functions, are marked dynamic so a UI can tell them apart from tools it was compiled against. MCP tools are dynamic automatically:
let tool = Tool.dynamic(name: name, description: description) { input, _ in
try await registry.call(name, input)
}The flag rides ToolCall.isDynamic and the dynamic field on
tool-input-available / tool-output-available chunks.