Agents that survive the OS, the reviewer, and the user.
Suspend & resume, push-resume via APNs/FCM, consent-sheet hooks, audit-log hooks, on-device first for privacy. Built for the team that actually has to ship the app.
Survive backgrounding
Mobile apps get backgrounded all the time. The OS may kill the process. agentlib's RunState is
serialisable: when OnSuspend fires, your app can persist the state to disk in a few milliseconds.
On OnResume (or app cold start), call Runner.resume(state) and the agent picks up where
it left off — same messages, same tools, same context window.
hooks.register(OnSuspend((event, ctx) async {
final state = ctx.serializeState();
await persistedStore.write('agent.state', state);
}));
// On cold start:
final state = await persistedStore.read('agent.state');
if (state != null) Runner.resume(state, config: ...); Push-resume via APNs / FCM
Long-running work doesn't need to live in the app. Your backend can finish a job and ping the device to "pick this up".
agentlib parses standard PushPayload and resumes from a checkpoint.
// Backend pushes:
// { "agentlib.resume": "" }
//
// In the app's notification handler:
final payload = PushPayload.tryParse(notification.userInfo);
if (payload != null) {
await Runner.resumeFromPush(payload, config: ...);
} Privacy: on-device first
For privacy-sensitive workloads (health, finance, legal, kids), ModelRoute.preferOnDevice([...]) keeps
data on the device by default. Cloud only as a fallback you control, and you can refuse to fall back if the data
is classified.
final agent = AgentSpec(
name: 'medical-helper',
instructions: 'You are an on-device medical assistant. No PHI leaves the device.',
model: ModelRoute.onDeviceOnly(
[FoundationModelsProvider(), GeminiNanoProvider(), FllamaProvider(...)],
), // Throws if no on-device provider can serve the request — never falls back to cloud.
); Consent sheets via PermissionRequestHook
Any tool with a capabilities declaration (microphone, camera, contacts, accessibility) triggers
a PermissionRequestHook before the tool dispatches. Your app shows a native iOS/Android consent
sheet; the agent waits for the result.
hooks.register(PermissionRequestHook((req, ctx) async {
final granted = await showCupertinoModalPopup(
context: ctx.uiCtx!,
builder: (_) => ConsentSheet(capability: req.capability, reason: req.reason),
);
return granted == true ? HookOutcome.carryOn() : HookOutcome.deny('user denied');
})); Audit logs for compliance
Every tool call goes through PreToolUse + PostToolUse. Log to your existing analytics
pipeline; agentlib doesn't dictate where logs go.
hooks.register(PostToolUse((call, result, ctx) async {
await audit.log(
runId: ctx.threadId,
tool: call.tool,
args: call.args,
ok: result is ToolResultOk,
at: DateTime.now(),
);
return HookOutcome.carryOn();
})); What this gives the team
- iOS reviewers: no Node binary in your bundle, no
exec, sandbox-safe by design. - Privacy: defaults to on-device, easy to enforce no-fallback for sensitive workloads.
- Auditing: 21 hooks make every tool call observable without prompt hacking.
- Reliability: serialisable state means a crash mid-agent isn't user-visible.