Watch it work

The session reports on its own work through observable properties — no delegate, no notification soup. Bind to them like any other state. For the errors that end a turn, see when it fails.

Read the usage

lastUsageReport holds the usage report from the current turn — reset to nil when a turn starts, then set each time the provider reports usage, so after a multi-step turn it reflects that turn's final round trip. A turn whose provider emits no usage report leaves it nil rather than showing an earlier turn's.

try await agent.send("Summarize the project")

if let usage = agent.lastUsageReport {
    print("\(usage.inputTokens ?? 0) in, \(usage.outputTokens ?? 0) out")
}

Every UsageReport field is optional — providers report what they know:

Field What it is
provider which provider produced the report
model the model that served the request
inputTokens tokens in the request
outputTokens tokens generated
cacheReadTokens tokens served from the provider's prompt cache
cacheWriteTokens tokens written to the provider's prompt cache
estimatedCost provider-estimated cost, when reported

Catch schema warnings

lastSchemaWarnings is non-fatal: a warning means the provider had to down-convert or simplify part of a tool schema to fit its native format. The turn continues with the adjusted schema. The list resets at the start of each turn and fills after the provider validates the turn's tools.

for warning in agent.lastSchemaWarnings {
    print("\(warning.toolID): \(warning.warning.path)\(warning.warning.message)")
}

Each ToolSchemaWarning names the tool (toolID) and carries a SchemaWarning with the path into the schema and a message. If the model starts misusing a tool's parameters on one provider, look here first.

See context failures

A failing context source never fails the turn — the request goes out with [context unavailable] in place of that source's summary (or a stale cached value, when one exists), and the failure lands in lastContextDiagnostics:

for diagnostic in agent.lastContextDiagnostics {
    switch diagnostic.reason {
    case .refreshFailed:
        print("\(diagnostic.sourceId) failed to refresh")
    case .concurrentInvalidation:
        print("\(diagnostic.sourceId) changed while it was being fetched")
    }
}

If the model seems blind to state it normally knows, this is the property that says why.

Watch tools run

activeToolCalls holds the calls executing right now — each appears when its execution starts and disappears as it finishes. It is the property behind "Running timeline.trim_clip…" progress UI, and it is empty whenever no turn is in flight.

Filter the logs

The SDK logs through SwiftLogger. It is a package dependency of AgentKit, so add the same package to your app and configure the shared logger:

import Logger

Log.minimumLevel(.warning)
Log.subsystem("agentkit.session", level: .debug)

Subsystem filtering is hierarchical on dots — setting a level on "agentkit" covers agentkit.session and agentkit.runtime unless a child is configured more specifically. The SDK's subsystems:

Subsystem Covers
agentkit.session turn lifecycle, tool execution, guard verdicts, context refresh
agentkit.runtime domain registration, agent creation, scoping
providers.anthropic Anthropic request building
providers.openai OpenAI request building
providers.gemini Gemini request building
providers.backend-router backend/cloud request building, stream parsing, redirect protection
agentkit-apple.provider the on-device provider's turn handling
agentkit-apple.bridge conversation-to-transcript bridging
agentkit-apple.wrapper the generated tool wrappers the on-device model calls

The SDK logs at .debug for internal transitions, .info for lifecycle events, .warning for recoverable issues (the same ones surfacing in the diagnostics above), and .error for failures.