Skip to content

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.


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(_:).

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]?
}

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.

UsageDataBlockCurator 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
)

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)" }
)

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
)

  • UI Overview — UIPolicy, UIPayload, UITemplate, UISecurity, ToolVisibility
  • Custom Curator — implementing ToolOutputCurator for domain-specific layouts
  • GroundedAgent — the two-LLM gatherer/presenter pattern that uses ToolOutputCurator and PresenterPrompt
  • Messages & EventsAgentEvent and the full event stream shape