ChappieHarness, then attach a ChappieHostTool handler to the client.
Register a host tool
Create a
ChappieHarness and list every tool you want the model to know about using ChappieHarnessTool. The inputSchema field accepts a ChappieJSONValue tree that maps directly to a JSON Schema object — the model uses it to construct well-formed arguments.let harness = ChappieHarness(
name: "Inventory Harness",
summary: "Native iOS surface for inventory questions.",
capabilities: ["multi-turn transcript"],
tools: [
ChappieHarnessTool(
name: "lookup_item",
description: "Looks up one inventory item by SKU.",
inputSchema: .object([
"type": .string("object"),
"properties": .object([
"sku": .object(["type": .string("string")])
]),
"required": .array([.string("sku")]),
"additionalProperties": .bool(false)
])
)
]
)
Create a matching
ChappieHostTool with the same name and an async handler closure. Pass your tools to Chappie.client(harness:tools:) — the client matches incoming tool calls by name and dispatches to your handler automatically.let lookupItem = ChappieHostTool(
name: "lookup_item",
description: "Looks up one inventory item by SKU.",
inputSchema: .object([
"type": .string("object"),
"properties": .object([
"sku": .object(["type": .string("string")])
]),
"required": .array([.string("sku")]),
"additionalProperties": .bool(false)
])
) { call in
// Decode the arguments from the call
guard case .object(let args) = call.argumentsJSON,
case .string(let sku) = args["sku"] else {
return ChappieToolResult(output: #"{"error":"missing sku"}"#)
}
// Call your own data layer
let stock = await InventoryStore.shared.stock(for: sku)
return ChappieToolResult(output: #"{"sku":"\#(sku)","stock":\#(stock)}"#)
}
let client = Chappie.client(harness: harness, tools: [lookupItem])
let reply = try await client.send("Find alternatives for SKU-123.")
Inspect tool call arguments
Every handler receives aChappieToolCall value. Use argumentsJSON to get a parsed ChappieJSONValue? rather than decoding the raw JSON string yourself.
ChappieToolCall fields
ChappieToolCall fields
The response-level output item ID assigned by the backend. May be
nil for some transport paths.The unique call identifier used to correlate the result with the request. Always present.
The tool name as declared in the harness — used by the registry to route to your handler.
The raw JSON string the model produced for this call’s input arguments.
A convenience computed property that parses
arguments into a ChappieJSONValue tree. Returns nil if the string is not valid JSON.ChappieToolResult. Because ChappieToolResult conforms to ExpressibleByStringLiteral, you can return a plain string literal from your handler when you don’t need the output: label:
ChappieJSONValue — schema building and argument parsing
ChappieJSONValue is the enum used both for defining input schemas and for parsing the arguments the model sends back. Its cases map one-to-one with JSON primitives:
| Case | Swift type |
|---|---|
.string(String) | String |
.number(Double) | Number |
.bool(Bool) | Boolean |
.object([String: ChappieJSONValue]) | Object |
.array([ChappieJSONValue]) | Array |
.null | Null |
.object and .array values to express any JSON Schema structure your tool requires. ChappieJSONValue is Codable, so you can also decode model-produced arguments directly.
Tool approval policies
By default Chappie runs tools automatically. UseChappieToolPolicy to require user approval or block a tool entirely.
Policy factories
Policy factories
Runs the tool without prompting. This is the default when you omit the
policy parameter.Emits an
.approvalRequested stream event and calls your toolApprovalHandler before running the tool. Provide at least one ChappieToolPermissionScope to describe what the tool accesses.Blocks the tool unconditionally. The handler is never called; the stream receives a
.toolCallCompleted event with status == .denied.Permission scopes
Pass one or moreChappieToolPermissionScope values to .ask or .deny so your approval UI can present a meaningful description of what the tool needs:
| Scope | Meaning |
|---|---|
.hostTool | Generic host-app function |
.contacts | Access to contacts |
.photos | Access to the photo library |
.location | Access to device location |
.files | Access to the filesystem |
.network | Outbound network requests |
.destructiveAction | Permanently modifies or deletes data |
.openWorldAction | Reaches outside the app (e.g., sends a message, opens a URL) |
Requiring approval before a destructive action
Providing an approval handler
When any tool uses.ask, supply a toolApprovalHandler closure on the client. Chappie calls it with a ChappieApprovalRequest describing the pending call — display your confirmation UI there, then return a ChappieApprovalDecision:
Observing tool execution with stream events
When you useclient.stream(_:) or client.streamHandle(_:), Chappie emits stream events at each stage of tool execution so your UI can show real-time feedback:
| Event | When it fires |
|---|---|
.toolCallRequested(ChappieToolExecutionRequest) | Immediately after the model requests a tool call, before the handler runs |
.approvalRequested(ChappieApprovalRequest) | When a tool’s policy is .ask and the handler has been invoked |
.toolCallCompleted(ChappieToolExecutionResult) | After the handler returns (or is denied/cancelled), with status set to .completed, .denied, .cancelled, or .failed |
Tool call round limit
Chappie allows up to 6 tool-call rounds persend, stream, or response call. If the model requests more consecutive rounds than that limit, the client throws ChappieClientError.toolLoopLimitExceeded. Design your tools to return complete, actionable results so the model can reach a final answer within a few rounds.