Built-in Curators
ToolOutputCurator is GroundedAgent’s extension point for turning raw tool results into the text the presenter LLM receives. It is a pure synchronous transform — no I/O.
See UI Overview for the UIPolicy/UIPayload model, or Custom Curator for writing your own.
ToolOutputCurator protocol
Section titled “ToolOutputCurator protocol”public protocol ToolOutputCurator: Sendable { func curate(_ results: [CapturedTool]) -> String}Called synchronously on the agent’s task. Any data the curator needs must be pre-fetched and stored as immutable properties — no async work inside curate(_:).
CapturedTool
Section titled “CapturedTool”What the curator receives for each result:
public struct CapturedTool: Sendable, Equatable { public let name: String public let ui: String? // the ui:// resource URI, if any public let structuredContent: JSONValue public let content: [ContentPart]?}DataBlockCurator
Section titled “DataBlockCurator”The default curator. Emits one ### <toolName> section per result — model-facing text content when present, otherwise the structured data pretty-printed as JSON. Sections are concatenated with a blank line between them.
public struct DataBlockCurator: ToolOutputCurator { public init() public func curate(_ results: [CapturedTool]) -> String public static func section(_ tool: CapturedTool) -> String}DataBlockCurator.section(_:) is public so PerToolCurator formatters can fall back to it for any unmapped tool.
Usage — DataBlockCurator is the default; no curator: argument required:
let agent = GroundedAgent( name: "Fixtures", gatherer: fastModel, presenter: preciseModel, tools: fixtureTools // curator: .dataBlock is the implicit default)Or pass it explicitly:
let agent = GroundedAgent( name: "Fixtures", gatherer: fastModel, presenter: preciseModel, tools: fixtureTools, curator: .dataBlock)PerToolCurator
Section titled “PerToolCurator”Routes each tool to its own formatter, keyed by tool name. Use this to trim or reformat oversized payloads before they reach the presenter. Unmapped tools fall back to DataBlockCurator.section(_:) by default.
public struct PerToolCurator: ToolOutputCurator { public typealias Formatter = @Sendable (CapturedTool) -> String
public init( _ formatters: [String: Formatter], default fallback: @escaping Formatter )
public func curate(_ results: [CapturedTool]) -> String}Static constructor (preferred):
extension ToolOutputCurator where Self == PerToolCurator { public static func perTool( _ formatters: [String: PerToolCurator.Formatter], default fallback: @escaping PerToolCurator.Formatter = { DataBlockCurator.section($0) } ) -> PerToolCurator}Usage:
let agent = GroundedAgent( name: "Fixtures", gatherer: fastModel, presenter: preciseModel, tools: fixtureTools, curator: .perTool([ "liveScores": { tool in // compact the payload — only emit the fields the presenter needs let scores = tool.structuredContent["scores"] return "### liveScores\n\(scores)" } // unmapped tools fall back to DataBlockCurator.section(_:) ]))To override the fallback:
curator: .perTool( ["liveScores": myFormatter], default: { tool in "### \(tool.name)\n(omitted)" })PresenterPrompt
Section titled “PresenterPrompt”Selects the presenter’s system prompt for a given turn, optionally keyed by the turn’s primary tool.
public struct PresenterPrompt: Sendable { public init(default defaultPrompt: String, perTool: [String: String] = [:]) public func resolve(primaryTool: String?) -> String public static let `default`: PresenterPrompt}resolve(primaryTool:) returns the per-tool prompt when primaryTool matches an entry in the map, and falls back to defaultPrompt otherwise (including when primaryTool is nil — no tools were called that turn).
PresenterPrompt.default instructs the presenter to use only provided data and never invent values. Override it when the presenter needs domain-specific framing:
let prompt = PresenterPrompt( default: "Present the data clearly. Never invent scores or team names.", perTool: [ "standings": "You are presenting a league table. Preserve ordering exactly." ])
let agent = GroundedAgent( name: "League", gatherer: fastModel, presenter: preciseModel, tools: leagueTools, presenterPrompt: prompt)Related
Section titled “Related”- UI Overview — UIPolicy, UIPayload, UITemplate, UISecurity, ToolVisibility
- Custom Curator — implementing
ToolOutputCuratorfor domain-specific layouts - GroundedAgent — the two-LLM gatherer/presenter pattern that uses
ToolOutputCuratorandPresenterPrompt - Messages & Events —
AgentEventand the full event stream shape