Eight load-bearing primitives.
Each of these is real, documented, tested, and used by example apps in the repo. Click any to read its detail.
AgentSpec + Runner
The composable agent descriptor and its streaming, event-driven loop. AgentSpec is a value-class: name, instructions, model, tools, hooks, handoffs, retry policy. Runner.runStreamed(config, input: ...) turns it into an event stream of TextDelta, ThinkingDelta, ToolCalled, ToolResult, SnapshotCreated, Finished.
final agent = AgentSpec(
name: 'helper',
instructions: 'You are a helpful mobile assistant.',
model: AnthropicProvider(apiKey: ''),
tools: [...virtualFilesystemTools(), nowTool],
);
final run = Runner.runStreamed(
agent.toRunConfig(vfs: Vfs(), threadId: 'main'),
input: 'What time is it?',
);
await for (final event in run.events) {
if (event is TextDelta) stdout.write(event.delta);
if (event is ToolCalled) print('→ ${event.tool}');
} Subagents
Spawn child agents with their own context window, model, tool allow-list, and snapshot timeline. Three dispatch modes: sync (return result inline), parallel (multiple subagents run concurrently — research + draft simultaneously), and background (long-running, survives orchestrator turnover).
final researcher = AgentDefinition(
name: 'researcher',
description: 'Deep dive on a topic from the workspace.',
tools: ['fs.read', 'fs.grep', 'fs.glob'],
modelOverride: AnthropicProvider(apiKey: ''),
);
final drafter = AgentDefinition(
name: 'drafter',
description: 'Compose an article from research notes.',
tools: ['fs.read', 'fs.write'],
);
final results = await SubagentRunner.runParallel(
parent: ctx,
defs: [researcher, drafter],
prompts: ['research X', 'draft about X'],
); Skills
Markdown bundles at /workspace/.skills/<name>/SKILL.md. The orchestrator sees only name + description until the model invokes the skill — then the body is loaded into context. A 30-skill catalog costs ~600 tokens in the orchestrator, not 12K.
---
name: summarise-email
description: Summarise an email into 3 bullet points.
---
# Steps
1. Read the email via /bin/gmail.show.
2. Extract subject, sender, and the 3 most important sentences.
3. Emit JSON: { subject, from, bullets: [...] }.
# Tools
- /bin/gmail.show
- /bin/jq Snapshots
snapshot() · revert() · fork(). Content-addressed (SHA-256 of serialised RunState + scratch map). SQLite-backed in production via SqfliteSnapshotStore; in-memory for tests. Auto-snapshot before highImpact: true tool dispatches.
// Manual snapshot.
final id = await ctx.snapshot(label: 'before-send');
// Revert.
await ctx.revert(id);
// Fork to a different model.
final forked = await ctx.fork(id, model: OpenAIProvider(...));
21 Hooks
Deterministic interception points where app code can observe, mutate, or deny. Hooks run on the host (Dart), not in the model. Loop hooks: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequestHook. Subagent: SubagentStart/Stop/Message. Compaction: PreCompact, PostCompact. Lifecycle: OnSuspend, OnResume, OnLowMemory, OnLowBattery, OnNetworkChange. Snapshot: OnSnapshot, OnRevert. Shell: OnShellParse. Misc: UserPromptSubmit, StopHook, NotificationHook, OnHandoff.
final hooks = HookRegistry()
..register(PreToolUse((call, _) {
if (call.tool == 'whatsapp.send' && !userHasAcceptedTos) {
return HookOutcome.deny('TOS not accepted');
}
return HookOutcome.carryOn();
}))
..register(OnLowBattery((_, __) async {
// Route to a smaller on-device model below 20%.
return HookOutcome.replace(modelOverride: GeminiNanoProvider());
})); Vfs + Sh
The virtual filesystem mounts: /workspace (real files in app sandbox), /scratch (RAM), /system (device state as JSON), /apps (live accessibility trees), /bin (CLI registry), /memory (SQLite), /snapshots (timeline). The Sh mini-shell parses pipes, redirects, $VAR, $(cmd) — but never execs a real process.
final bin = BinMount()
..register(fsCli())
..register(whatsappCli())
..register(calendarCli());
final vfs = Vfs()
..mount('/workspace', WorkspaceMount(docsDir))
..mount('/scratch', ScratchMount())
..mount('/bin', bin);
final sh = Sh(binMount: bin, vfs: vfs, builtins: defaultShBuiltins());
// Model can now emit:
// Sh("cat /workspace/inbox.md | grep urgent | wc -l")
// Sh("contacts grep 'Mom' | jq -r .number | xargs whatsapp send") ModelRoute
A ModelProvider that walks a list of on-device + cloud providers and picks the first one whose capabilities cover the request. Same agent code runs on-device when possible and escalates to cloud only when needed.
final agent = AgentSpec(
name: 'offline-helper',
instructions: 'You are a private on-device assistant.',
model: ModelRoute.preferOnDevice(
onDevice: [
FoundationModelsProvider(),
GeminiNanoProvider(modelVariant: 'nano-v3'),
FllamaProvider(llm: fllamaAdapter, modelName: 'Qwen3-1.7B-Instruct'),
],
fallback: AnthropicProvider(apiKey: ''),
),
); MCP
HTTP and WebSocket transports for the Model Context Protocol. MCP tool specs are translated to native Tool objects and registered at /bin/mcp.<server>.<tool>, so they show up in Sh pipelines like any other CLI. WebSocket transport adds full-duplex notifications on McpClient.notifications.
final mcp = await McpClient.connect(
WebSocketMcpTransport(Uri.parse('wss://weather.mcp.example.com')),
);
// All tools from the server now live at /bin/mcp.weather.*
final bin = BinMount()..registerMcp(mcp, prefix: 'weather');
// Subscribe to notifications.
mcp.notifications.listen((n) => print('MCP: ${n.method}'));