Skip to main content
Host tools are Swift functions your app registers with the Chappie client so the model can call them during a conversation. When the model decides a tool is relevant, Chappie invokes your handler, collects the result, and seamlessly feeds it back into the transcript — all without any extra orchestration code on your part. The two-step setup keeps the model’s declaration separate from the Swift implementation: first describe the tool in a ChappieHarness, then attach a ChappieHostTool handler to the client.

Register a host tool

1
Declare the tool in the harness
2
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.
3
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)
            ])
        )
    ]
)
4
Register the Swift handler
5
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.
6
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 a ChappieToolCall value. Use argumentsJSON to get a parsed ChappieJSONValue? rather than decoding the raw JSON string yourself.
id
String?
The response-level output item ID assigned by the backend. May be nil for some transport paths.
callID
String
The unique call identifier used to correlate the result with the request. Always present.
name
String
The tool name as declared in the harness — used by the registry to route to your handler.
arguments
String
The raw JSON string the model produced for this call’s input arguments.
argumentsJSON
ChappieJSONValue?
A convenience computed property that parses arguments into a ChappieJSONValue tree. Returns nil if the string is not valid JSON.
Return your result as a ChappieToolResult. Because ChappieToolResult conforms to ExpressibleByStringLiteral, you can return a plain string literal from your handler when you don’t need the output: label:
let echoTool = ChappieHostTool(
    name: "echo",
    description: "Echoes the input back."
) { call in
    return "{\"echo\":true}"   // String literal works directly
}

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:
CaseSwift type
.string(String)String
.number(Double)Number
.bool(Bool)Boolean
.object([String: ChappieJSONValue])Object
.array([ChappieJSONValue])Array
.nullNull
Use nested .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. Use ChappieToolPolicy to require user approval or block a tool entirely.
ChappieToolPolicy.automatic
static property
Runs the tool without prompting. This is the default when you omit the policy parameter.
ChappieToolPolicy.ask(scopes:)
static factory
Emits an .approvalRequested stream event and calls your toolApprovalHandler before running the tool. Provide at least one ChappieToolPermissionScope to describe what the tool accesses.
ChappieToolPolicy.deny(scopes:)
static factory
Blocks the tool unconditionally. The handler is never called; the stream receives a .toolCallCompleted event with status == .denied.

Permission scopes

Pass one or more ChappieToolPermissionScope values to .ask or .deny so your approval UI can present a meaningful description of what the tool needs:
ScopeMeaning
.hostToolGeneric host-app function
.contactsAccess to contacts
.photosAccess to the photo library
.locationAccess to device location
.filesAccess to the filesystem
.networkOutbound network requests
.destructiveActionPermanently modifies or deletes data
.openWorldActionReaches outside the app (e.g., sends a message, opens a URL)

Requiring approval before a destructive action

let deleteItem = ChappieHostTool(
    descriptor: ChappieHarnessTool(
        name: "delete_item",
        description: "Permanently deletes a record from the database."
    ),
    policy: .ask(scopes: [.destructiveAction])
) { call in
    // Only executes after the user approves
    try await Database.shared.delete(id: call.arguments)
    return ChappieToolResult(output: #"{"deleted":true}"#)
}

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:
let client = Chappie.client(
    harness: harness,
    tools: [deleteItem],
    toolApprovalHandler: { request in
        let confirmed = await showApprovalAlert(
            toolName: request.toolCall.name,
            scopes: request.scopes,
            reason: request.reason
        )
        if confirmed {
            return .approved()
        } else {
            return .denied(reason: "User cancelled the action.")
        }
    }
)
If a tool’s policy is .ask but you don’t provide a toolApprovalHandler, Chappie denies the tool call automatically and throws ChappieClientError.toolApprovalDenied. Always pair .ask policies with a handler.

Observing tool execution with stream events

When you use client.stream(_:) or client.streamHandle(_:), Chappie emits stream events at each stage of tool execution so your UI can show real-time feedback:
EventWhen 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
for try await event in client.stream("Check stock levels for all SKUs.") {
    switch event {
    case .textDelta(let text):
        print(text, terminator: "")
    case .toolCallRequested(let request):
        print("\n⚙️ Calling tool: \(request.toolCall.name)")
    case .toolCallCompleted(let result):
        print("✅ Tool finished with status: \(result.status)")
    case .completed:
        break
    default:
        break
    }
}

Tool call round limit

Chappie allows up to 6 tool-call rounds per send, 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.
Returning structured JSON from your handler (rather than plain prose) lets the model extract individual fields reliably and reduces the number of follow-up tool calls needed.