primitives

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.

01

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.

agent.dart
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}');
}
02

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).

Orchestrator Task() · skills · vfs researcher own context parallel drafter own context parallel indexer survives backgrounding background
Subagents: parallel, sync, or background. Each gets its own context window.
subagents.dart
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'],
);
03

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.

/workspace/.skills/summarise-email/SKILL.md
---
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
04

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.

init read snapshot send done highImpact fork() · alt model revert()
Snapshot timeline. fork() branches; revert() rolls back. Content-addressed, SQLite-backed.
snapshots.dart
// 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(...));
05

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.

hooks.dart
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());
  }));
06

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.

vfs-sh.dart
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")
07

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.

route.dart
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: ''),
  ),
);
08

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.

mcp.dart
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}'));
next

See on-device routing in detail.

Four on-device providers, one preferOnDevice([...]) call. Same code on a Pixel, an iPhone, and a desktop dev box.