Building a Production Flutter Agent with GenKit: State Machines, Tool Safety, and Audit Loops
Most Agent demos stop at a single model call. A real enterprise Agent needs tool permission boundaries, state-machine-enforced output consistency, and an audit loop that catches hallucinations without rewriting model answers. This implementation shows how to get those guarantees on Flutter with Google's own SDK, and the patterns for tool safety, structured recovery, and trace-driven debugging apply to any stack.
A production Flutter Agent built with Google's GenKit SDK moves beyond simple API calls by wrapping every turn in a typed Flow with 21 lifecycle steps. The system defines tool boundaries through a required/forbidden/candidate set so the model never sees tools it shouldn't use, and a trust-by-side-effect matrix gates execution of mutating or external actions. An 11-state fine-grained state machine tags every transition with its actor (FSM, LLM, or GenKit) so trace failures immediately identify whether the model or the engineering is at fault.
Retrieval-augmented generation runs across four decoupled grounding pathways and five retrieval modes fused with Reciprocal Rank Fusion, while a query-rewrite layer produces five variant types to catch terminology mismatches. The Agent Loop itself delegates tool-calling cycles entirely to GenKit's `generateStream`, with three structured recovery paths for missing tools, context overflow, and truncated answers. A dedicated Auditor extracts factual claims from the model's answer, grounds each against evidence, and triggers regeneration with specific issue codes rather than rewriting answers.
Every cross-boundary data structure carries a versioned schema, prompts are i18n-ready Dotprompt templates with dual character-and-token budget trimming, and a full trace-and-replay harness packages turn dependencies for offline reproduction. The result is an enterprise Agent where semantic decisions stay with the model but all engineering invariants are enforced by the runtime.
Delegating the tool-call loop entirely to GenKit's generateStream rather than writing a custom while loop is a deliberate architectural choice that avoids reimplementing parsing, backfill, and cancellation logic, keeping the Dart side focused on boundary enforcement.
The three-set tool permission model (required/forbidden/candidate) is more robust than prompt-based instructions because the model simply cannot see forbidden tools in its schema, eliminating an entire class of prompt-injection and privacy-bypass risks.
Tagging every FSM state transition with its actor (FSM, LLM, or GenKit) turns trace debugging from guesswork into a clear attribution of whether the model or the engineering failed, which is critical for regression in multi-agent systems.
Pre-running side-effect-free tools before the Agent Loop starts is a latency optimization that also improves answer quality by giving the model grounding facts on its first generation pass, reducing multi-turn back-and-forth.
The Auditor's refusal to rewrite model answers, even when it detects factual errors, is a principled boundary that keeps the trace a pure reflection of model capability. This makes evaluation results trustworthy and prevents local code from silently papering over model weaknesses.
Query Rewrite generating five variant types (original, keyword, technical, code/config, synonym) and fusing them with RRF is a practical answer to the terminology-mismatch problem that single-query BM25 cannot solve, especially for Chinese-English mixed corpora.
Versioning every cross-boundary data structure with a schema name like 'local_agent.trace_v1' is a low-cost insurance policy against breaking changes that most Agent projects overlook until they need to replay old traces.
Keeping Dotprompt templates free of natural-language strings and injecting all text via locale-aware Dart interfaces forces i18n decoupling and prevents the common anti-pattern of hard-coded prompts that break when switching languages.