Inject live state
The model makes better decisions when it already knows your app's state — current selection, project settings, playback position. A context source pushes that state into the request itself, so the model doesn't spend a tool call (and a full round trip) just to ask.
Implement ContextSource and register it on the agent. Before each provider
request, the session collects a one-line summary from every active source and
injects it into the system prompt as [source-id]: text lines.
Define a source
A source has an id, a refresh policy, a short summary() for every request,
and a richer detail() the model can request on demand.
struct SelectionContext: ContextSource {
let id = "selection"
let policy: ContextSummaryPolicy = .always
let editor: EditorStore
func summary() async throws -> ContextSummary {
let names = await editor.selectedClipNames()
let text = names.isEmpty
? "No clips selected"
: "Selected: \(names.joined(separator: ", "))"
return ContextSummary(sourceId: id, text: text)
}
func detail() async throws -> DomainContext {
let names = await editor.selectedClipNames()
return DomainContext(
sourceId: id,
content: [.json(.array(names.map { .string($0) }))]
)
}
}
EditorStore stands in for whatever owns your selection state. Register
sources when you build the agent:
let agent = try runtime.makeAgent(
provider: .anthropic(apiKey: key),
role: AgentRole(staticPersona: "You are a precise video-editing assistant."),
contextSources: [SelectionContext(editor: editor)]
)
Pick a refresh policy
ContextSummaryPolicy decides how often summary() runs:
| Policy | Refreshes | Use for |
|---|---|---|
.always |
before every provider request | fast-changing state — selection, playback position |
.cached(seconds) |
after the TTL expires, or when a tool invalidates it | slow-changing state — project settings, library counts |
.onDemand |
never automatically — only when the model asks | expensive-to-compute state |
.always and valid .cached summaries ride the system prompt on every
request. .onDemand sources contribute nothing until the model reaches for
them (see let the model dig deeper).
Invalidate after mutations
A cached summary goes stale the moment a tool changes what it describes.
Declare which domains' mutations stale your source with
invalidatedByDomains — the default is [id], the source's own id, so
override it whenever your source watches a different domain:
struct TimelineOverviewContext: ContextSource {
let id = "timeline_overview"
let policy: ContextSummaryPolicy = .cached(60)
var invalidatedByDomains: Set<String> { ["timeline"] }
let timeline: TimelineStore
func summary() async throws -> ContextSummary {
let count = await timeline.clipCount()
return ContextSummary(sourceId: id, text: "\(count) clips on the timeline")
}
func detail() async throws -> DomainContext {
let count = await timeline.clipCount()
return DomainContext(
sourceId: id,
content: [.json(.object(["clip_count": .integer(count)]))]
)
}
}
Two things invalidate the cache mid-turn, before the TTL expires:
- a successful tool call from a domain declared
.mutatinginvalidates every cached source that maps that domain ininvalidatedByDomains - a tool result whose
affectedEntitiesname a domain invalidates the cached sources mapped to that domain — even from a read-only domain
Either way, the next provider request rebuilds the summary, so the model sees the state its own tool call just changed.
Let the model dig deeper
Summaries are deliberately one line. When the model needs the full picture, two built-in meta-tools serve it without any code on your side:
| Meta-tool | Returns |
|---|---|
agentkit.list_context |
every registered source with its id and policy |
agentkit.inspect_context |
the detail() content for one source_id |
This is also how .onDemand sources are reached: the model lists, inspects,
and pays the cost only when it decides the detail is worth it.
When a source fails
A throwing source never fails the turn — live state is an enhancement, not a dependency:
- an
.alwayssource that throws contributes a[context unavailable]placeholder for that request - a
.cachedsource whose refresh throws after the TTL expires serves the last good summary instead - every failure is recorded in
lastContextDiagnosticson the session, reset each turn — see watch it work for the diagnostic surface
Next
- Scope tools. Stage discovery. — the other meta-tools, and when to use them.
- Watch it work — the full diagnostic surface.
- Undo every turn — how conflicts force a context refresh.