flutter_agent_harness
Health Pass
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 11 GitHub stars
Code Fail
- rm -rf — Recursive force deletion command in .github/actions/setup-flutter-selfhosted/action.yml
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
Fa - Factory Agent. Cross-platform AI agent harness for Dart and Flutter — streaming providers, agent loop with tools, session persistence, compaction; backend agent mode: fa behind product Go backends
flutter_agent_harness
Cross-platform AI agent harness for Dart and Flutter — streaming provider
adapters, an agent loop with native tool calling, JSONL session persistence,
context compaction, and a pi-like terminal coding agent (fa). Architecture
ported from pi-mono
(packages/ai + packages/agent), with a pure-Dart core that runs on the
VM, Flutter desktop/mobile, and web.
fa1.dev — the project website: a live in-browser
demo of the full agent (sandboxed shell, git, interpreters — your key
stays in page memory), the iOS public
beta on TestFlight, the
macOS app,
and the CLI installer below.
Design contract
- Pure Dart core. No
dart:io, no Flutter imports inlib/— platform
capabilities live behind abstractions (ExecutionEnv); the IO
implementation is a separate entry point (lib/io.dart), so the core
compiles for web. - Errors-as-events. Providers never throw: network failures, 429s,
malformed SSE — everything arrives as anerrorevent with a stop
reason. The agent loop never dies on a dropped connection. - Streaming-first, partial-first.
Stream<AgentEvent>everywhere;
every delta event carries the live partial message. - Native tool calling per provider (OpenAI
tools, Anthropictool_use, GooglefunctionCalling). Prompt-based calling exists only
as an opt-in adapter for chat-only runtimes. - Token accounting inline, overflow detection per provider, and
token-based (never message-count-based) context management. - Cancellation everywhere via
CancelToken— providers, loop, tools.
What's inside
- Agent core (
lib/src/): the async agent loop, steering + follow-up
message queues (inject input mid-run), hooks (beforeToolCall,afterToolCall,transformContext,prepareNextTurn), model roles
(default/smol/slow/plan) with fallback chains, API-key rotation,
429 mid-turn take-over, and stream watchdogs (connect + idle timeouts
with config overrides). - Providers: openai-completions (covers OpenRouter/DeepSeek/Kimi/Grok/…),
Anthropic, Google, ChatGPT (Codex OAuth), Copilot (device flow), DIAL,
MiniMax, z.ai, CodeMie (SSO), Ollama, plus custom providers saved from
the REPL. One OpenAI-compatible adapter is reused viabaseUrlswap. - Sessions: append-only JSONL trees (branching, labels, tree
navigation with branch summaries), token-based auto-compaction with a
structured summary prompt, and attached-session support (watch a live
CLI session and hand it input from the Flutter app). - Tools:
read(trailing selectors, archives, SQLite),write,ls,bash(background jobs, timeout-retry for transient stalls, per-turn
approval grants),task(parallel subagents with typed roles and
output schemas),lsp(diagnostics/definition/references/rename),
MCP servers (stdio + remote), A2A interop,checkpoint/rewind,memory_*(git-backed long-term memory),ask,request_secret. - Agent skills:
<root>/<name>/SKILL.mdplus Claude/Copilot/Codex
layouts (.claude/skills,.github/skills,.codex/skills, user-level
equivalents) discovered by default with an access prompt; typed
frontmatter (allowed-tools, paths, context: fork),$ARGUMENTS
rendering, and per-turn approval grants. See
docs/migrating-from-claude-copilot-codex.md. - Approval gate (
lib/src/approval/): read/write/exec tiers, session
modes (always-ask/write/yolo/unattended), per-tool overrides, and a
critical-pattern interceptor for dangerousbash— even in yolo. - Trajectory ledger (
lib/src/trajectory/): every session projects
into an immutable snapshot (turns, timeline modes, full-text search)
rendered by the/trajectoryREPL family and thefa trajectory
headless command. - Agent messaging: every agent owns a file inbox in a shared fabric —
twofainstances chat live; subagents are first-class addressable
mailboxes. A2A (fa serve --a2a) mounts the agent as a remote endpoint. - Model-agnostic prompts (
prompts/): all LLM prompts are versioned
Markdown with override support — no prompt strings buried in code.
Install
curl -fsSL "https://fa1.dev/install.sh" | sh # macOS / Linux / WSL
dart pub global activate flutter_agent_harness # fa + fah on your PATH
The installer detects the OS/architecture, downloads a prebuilt binary
from the latest GitHub
Release,
puts it on your PATH, and (on macOS) strips Gatekeeper quarantine and
re-signs it. More install paths — the web demo, the Flutter app — live on
fa1.dev.
CLI (fa / fah)
A pi-like terminal coding agent: a full-screen TUI (streaming markdown,
mouse wheel scrolling, slash-command completion, live steering) with the
same core as the library. Sessions persist under~/.fah/sessions/<cwd-slug>/.
export OPENROUTER_API_KEY=sk-or-... # or ANTHROPIC_API_KEY / GOOGLE_API_KEY
dart run bin/fah.dart # defaults: OpenRouter, claude-sonnet-4
dart run bin/fah.dart --provider anthropic --model claude-sonnet-4-5
dart run bin/fah.dart --model openai/gpt-4o-mini --cwd . --session-root /tmp/fah
Headless mode runs a single non-interactive prompt and exits — the response
streams to stdout, tool indicators and notices go to stderr (stdout stays
pipeable), nothing is ever prompted interactively, and the session persists
like a REPL run. Exit codes: 0 ok, 1 provider error, 130 aborted (Ctrl-C).
A first positional naming an existing file becomes the prompt source: text
files (.md, .markdown, .txt) are inlined as the prompt; any other
(binary) file is attached as a path reference for the agent's tools — in
both cases trailing text appends as the instruction. A path that does not
exist is treated as plain prompt text.
fa "summarize the changelog" # positional prompt
fa -p "fix the typos in README.md" # -p/--prompt alias
fa CHANGELOG.md "summarize this" # text file as prompt
fa screenshot.png "describe it" # binary → path reference
fa "summarize the changelog" | pbcopy # pipes cleanly
Flags: --model <id>,--provider openai-completions|anthropic|google|dial|minimax|zai,--base-url <url>, --cwd <dir>, --session-root <dir>, -p/--prompt <text>, --help, --version.
The chatgpt provider (Codex backend) is also available: sign in with a
ChatGPT account via /provider chatgpt oauth in the REPL (OAuth-only —
there is no headless --provider chatgpt flag). Several ChatGPT accounts
can coexist: the flow offers the saved accounts first, each account keeps
its own named entry and secure-store slot, and re-auth never touches a
sibling account's credentials.
Env preconfig (Docker / headless)
FA_PROVIDER_TYPE + FA_PROVIDER_CONFIG boot a declared provider with
no saved config, and the declaration becomes the session default for
every model role (default/smol/slow/plan) — the same selection a/provider <name> switch makes:
FA_PROVIDER_TYPE=zai
FA_PROVIDER_CONFIG='{"baseUrl":"https://api.z.ai/api/coding/paas/v4","model":"glm-5.3","apiKeyEnvVar":"ZAI_API_KEY"}'
ZAI_API_KEY=sk-...
baseUrl and model are required — no catalog defaults fill gaps; a
missing field fails loud at boot. apiKeyEnvVar is optional: declared,
the named env var (or its _BASE64 twin) must hold the key; omitted,
the provider boots keyless and the spec's usual env names are never
probed. Every text value has a base64 twin for CI platforms that mangle
special characters — FA_PROVIDER_CONFIG_BASE64, and<apiKeyEnvVar>_BASE64 for the key: the plain value wins when both
carry the same value; mismatched or malformed twins fail loud.
# base64 twin form (identical boot):
FA_PROVIDER_CONFIG_BASE64=$(printf '%s' "$FA_PROVIDER_CONFIG" | base64)
ZAI_API_KEY_BASE64=$(printf '%s' "$ZAI_API_KEY" | base64)
GitHub Copilot is a first-class provider. /provider copilot connects a
GitHub account via the device-code flow (open the shownverification_uri, enter the user_code) or by pasting an existing PAT
— the flow works headless too — and the Flutter app offers the same
connect as a sheet. Accounts save as named entries (copilot-<login> by
default); the plan picks the host — individual api.githubcopilot.com,
business api.business.githubcopilot.com, enterpriseapi.enterprise.githubcopilot.com, or a custom --base-url override —
and several accounts can coexist side by side. Tokens live only in the
OS secure store (Keychain / Secret Service); config.yaml carries name,
plan, and baseUrl, never a token. CI runs store-less via theFA_KEY_COPILOT_<NAME> env (plus a _2… ring for more entries), and
headless runs take --provider copilot --model <id> withCOPILOT_GITHUB_TOKEN. Models come from a live GET /models (with
capabilities and limits). The device-flow client id is overridable viaFA_COPILOT_CLIENT_ID — that GitHub endpoint is undocumented, so a
custom client id carries an account-ban risk; override only with cause.
Sleep prevention (power.sleepPrevention, power.hold)
Long-running runs die with the machine: when the Mac sleeps mid-run,
scheduled wake-ups never deliver. A power assertion holds the machine
awake while the agent is WORKING — by default one assertion per RUN,
acquired when the run goes in flight and released when it settles, so
an agent idling between turns never pins the machine awake for hours.
Controlled by ~/.fah/config.yaml:
power:
sleepPrevention: idle # off | idle (default) | display | system
hold: per-run # per-run (default) | session (hold the whole session)
Levels are cumulative — idle prevents idle sleep (caffeinate -i),display also keeps the display awake (-i -d), system also blocks
AC system sleep and declares the user active (-i -d -s -u). hold: session opts into holding the assertion from session start to exit
(always-on deployments). macOS runs caffeinate -w <fa pid> (it
self-exits with fa, so the assertion can never leak); Linux triessystemd-inhibit --what=idle:sleep behind a pid watchdog; other
platforms no-op. A failed assertion logs a warning and the run
continues; a helper that ignores SIGTERM at release gets a SIGKILL
after 5s and release proceeds. /power shows the level, the hold, and
whether the assertion is currently held (pmset -g assertions on macOS
shows the real thing). Windows (SetThreadExecutionState) is a tracked
stub.
Slash commands (selection)
/provider, /models, /model, /approval, /allow, /tools,/skills, /agents, /tasks, /trajectory [view|cost|tail|inspect],/memory [maintain], /power, /compact, /reset, /checkpoint//rewind,/mcp, /a2a, /dap, /stats, /mouse, /settings, /help — plus
every discovered skill as /skill:<name> (a bare /<name> alias works
too). While a run streams, typed input steers the agent; Ctrl-C aborts
the current run (Ctrl-C at the idle prompt exits).
Tool calls pass the approval gate (see above); mode and always-allowed
tools persist in ~/.fah/config.yaml. Tool availability can be scoped
per project/session via .fah/config.yaml and .tools/<id>.yaml
(see docs/tool-availability.md).
The CLI core (AgentCli + CliIO) is pure Dart and lives inlib/src/cli/; only bin/fah.dart and lib/io.dart touch dart:io.
Agent-to-agent messaging (DAP)
The CLI ships a default-on DAP/1 hub plugin (bin/fah_hub_plugin.dart +
the hosted fah_hub_client pub package): agents connect to a hub over a signed
WebSocket, exchange end-to-end encrypted channel messages and DMs (the
hub only ever sees ciphertext), and see each other's presence. Inbound
hub mail is drained into the agent loop as steering messages; /dap and
the dap_* tools drive the connection. See docs/dap.md
for the protocol, the hub server, and an end-to-end setup walkthrough.
Browser extension
browser_ext/ pairs a local fa with Chrome over a loopback WebSocket
bridge — or runs the agent fully self-contained in the extension's
service worker: docs/browser-extension.md.
Outlook add-in
fa also runs as an Outlook taskpane (approval-gated mail tools on top
of the same agent core) — install guide and troubleshooting:
docs/outlook-addin.md.
Mobile automation (Android)
Two Android flavors: store (Play; mobile.launch + mobile.logs) and
god (sideload only, same release key, distinct applicationId; adds
hierarchy / tap / swipe / text / screenshot + opt-in mobile.shell over
Shizuku), gated by the issue #19 availability floor:
| Tier | mobile.* surface |
|---|---|
| store | mobile.launch, mobile.logs |
| god | + mobile.hierarchy, mobile.tap, mobile.swipe, mobile.text, mobile.screenshot |
| god+shizuku | + mobile.shell (opt-in bridge; Shizuku not running when absent) |
Details — tiers, sideload, Shizuku, consent, security, E2E checklist:
docs/android-automation.md · god build.
Development
dart pub get
dart test --coverage=coverage --exclude-tags integration
dart run coverage:format_coverage --lcov -i coverage -o coverage/lcov.info
python3 scripts/check_coverage.py
Pre-commit hook (analyze + tests + coverage ≥ 80% + duplication < 1%):
cp scripts/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
License
MIT — see LICENSE.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found