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.