embed_flutter 0.3.0
embed_flutter: ^0.3.0 copied to clipboard
A powerful Flutter SDK that provides a powerful voice-enabled AI-agent functionality with real-time widget tree monitoring for screen context fetching and realtime voice communication.
Changelog #
0.3.0 #
Tree-capture hardening, Android/backend wire parity, and multi-step mission parity. Verified against the revrag-core wire contract. Minor version bump: capture semantics, defaults and stable IDs change (see Behaviour changes).
Requires Flutter >= 3.29 / Dart >= 3.7 (SemanticsData.role).
Behaviour changes #
- Missions no longer depend on
dataCapture, widget visibility or the route allowlist. Mission captures and responses (find / verify / info queries / candidates) are blocked only by explicit host exclusions (disabledRoutes, an open excluded popup), as on Android.flags.dataCapture: falsestops telemetry pushes only. A refused mission response is replaced by a failure frame (screen_not_capturable/payload_too_large) instead of being dropped. transmitOnHiddenWidgetdefaults to on (Android parity). Only an explicitfalseunderflagsortreePushturns it off.- Named popup routes in
disabledRoutesblock capture while open, but no longer hide the FAB (popups don't change the page route). Their route-change analytics are restored. - Screen names prefer the route name (or host-provided name) over widget
inference; tabs of an
IndexedStack/PageViewshell are namedroute/tabfrom stable signals (ValueKey<String>,Semanticsidentifier, heading). Class names are never used in release builds, so--obfuscateno longer scrambles screen names, stable-ID prefixes or cache keys. - Stable IDs changed (number-led label words no longer enter IDs and
collisions get content-derived suffixes; tappable images are buttons;
full-screen labelled wrappers are groups; overlay nodes live under
screen.overlay.*; role and screen-name changes below). The on-device cache format is bumped toschemaVersion: 2; older screen files are discarded locally and never uploaded. - The stale-node prune is new: a node is dropped only after it has been absent from eligible changing captures spanning 14 days; a quiet stretch, an open overlay or a partial capture never counts as absence.
- Cache uploads no longer include
_firstSeen/_lastSeen/_seenCountbookkeeping ortokens.
Wire parity #
- Nodes carry
disabled(measured),placeholder, sliderrange(when the host provides min/max),parent_id, and thesetTextaction on editable fields. Roles use Android's vocabulary (scrollable,switch,radio,navigation,modal,overlay,listitem); raw Flutter role names no longer leak. ui_diffkeeps its delta shape:movedentries now carrypath(the key the backend reads),updatedentries carrysubtree_text,normalized_text,layer,editable,secure,parent_id,visible_fraction, and fraction-only visibility changes are reported.- A node is
visibleonly when at least 5% of it is on screen. - Snapshot frames carry
app_user_id,app_versionandsdk. verification_resultcarries the achieved slidervalueandstrategy; slider failure codes match the backend/Android (invalid_params,value_out_of_range,range_unknown,set_slider_not_applied).- Every terminal
mission_statuscarriesstep_index(onsucceeded, the last step), so the agent can say which chained step failed. continue_on_failuremissions emitstep_failedand keep going.- New
flags.nativeBridgegates the synthetic-pointer tap fallback.
Multi-step missions #
- Cancels and newer missions take effect immediately (waits, retry delays and
attempts race the cancel signal); a pre-cancelled mission no longer cancels
the running one; a cancelled step emits only
cancelled. - A blind step right after a settle wait polls up to 2 s for a slow next screen before failing.
- Malformed steps fail
invalid_mission; unexpected errors failexception; terminal statuses are never evicted from the outbound queue. - Standalone highlights (guided walkthrough) run as internal
hl_missions: full find ladder, scroll into view withalign, re-find before drawing;duration_ms: 0stays until cleared. Highlights yield while a mission runs.
Execution safety #
- Taps: the synthetic pointer needs >=50% visibility and a hit test that reaches the target; a tap with no visible effect is never retried (no double side effects); agent taps are not counted as user clicks.
set_texton a non-editable target writes only to a field BOUND to the matched label (or takes the slider path), with no focus-tap side effect; clears are verified.- A measured-disabled target fails fast with
target_disabled; the enabled twin wins ties. backuses the host hook / Navigator first and restores a custom tab only when nothing can pop. Retries reuse the node found on the first attempt.- Text-match promotion never climbs past a control;
find_candidateshonours scope, role and forbid filters; scrolling prefers the topmost layer's vertical scroller, skips pagers, and waits for pagination. - The stable-ID rung cross-checks text, so a shifted ID cannot tap the wrong sibling ("3 Months" vs "6 Months").
Result accuracy #
A mission is reported succeeded only when the INTENDED target was acted on
and verification supports it; otherwise it is failed with a specific reason
(the terminal vocabulary is unchanged).
set_textnever types into a field whose own label/hint names something else ("PIN" for "Full Name") and never redirects to merely the nearest field: the field must sit in the label's own container, directly below / right of it with nothing labelled in between, or be the one field whose label agrees. Otherwisetarget_mismatch, nothing typed. Obscured fields are skipped unless the request asks for a PIN / password.- Side-effect verbs (set_text, click / tap / navigate, check, select,
set_slider) match whole words only ("PIN" ≠ "Pincode"), refuse an
ambiguous match (
ambiguous_target, candidates still sent ondisambiguating) and refuse a qualified label for a write ("Name" → "Nominee Name"). Read-only verbs keep the lenient ladder. - Taps: a tappable twin must have the same text ("Name" never taps "Change
Name"); a container's only button is tapped only when it agrees with the
request (
target_mismatchotherwise). A stable id now naming a different number ("12 Months" for "6 Months") is not acted on. A retry re-finding by rectangle must agree on the label. - Verification is target-scoped: a tap needs a route change, an overlay
opening / closing, or a change on the target, its option group, its own
subtree or its bounded container. Changes elsewhere on the page — and
anything that was already changing before the tap (a baseline capture
~200 ms earlier) — no longer count.
navigateneeds a change of place; optionalparams.expect_screenmust match. set_textverifies the requested field's VALUE (never its label), which must have changed unless it already held exactly the text (set_text_already_satisfied); formatter tolerance (case, spaces, separators, masks) is kept.- When it cannot look, the SDK says so:
verification_unavailable(one retry for idempotent verbs, none after a tap / back),check_not_applied,select_not_applied,back_had_no_observable_effect. A tap after which something changed elsewhere on screen (not on the target, and not a self-updating clock or countdown) is reportedverification_unavailablewith evidenceunattributed_change— never success, and never "nothing happened". - Second look, observation only. When a dispatched tap / navigate / back cannot be confirmed, the SDK re-captures once ~1.5 s later and re-runs the verifier — a late, network-driven update becomes a verified success. The action is never dispatched a second time; still unconfirmed → the original failure is reported.
read_fieldon an empty field failsfield_has_no_textinstead of answering with the label.- Additive wire keys:
verification_resultgainsrequested_stable_id,requested_text(label only, ≤60 chars, never for obscured fields),acted_stable_id,acted_role,redirected,match_source,verified,value_matched(a boolean, never the value); terminalmission_statusgainssteps_completed,steps_total,steps_skipped,steps_failedand mirrors the identity keys on failure. Inboundset_textvalues are redacted in the debug wire log. - More conservative on device: taps whose only visible effect is elsewhere
on the screen (a counter, a badge, a status line outside the tapped card)
now report
tap_had_no_observable_effectinstead of success.
Icon-only controls and custom tab bars #
- Small icon-only tappables are named from what they draw: an image's asset
file name (
ic_home_active.png→home), a Material / Cupertino icon's literal name (Icons.pie_chart→pie chart), or — for a tab — "Tab i of n". Large wrappers are never named; a real label is never overridden. - Custom bottom-navigation rows are reported as
tab(excluding Material button bars, grids and keypads) and kept first under the 15 KB budget. - Custom tab rows report
selectedwhen exactly one item is drawn differently, else from the tab the session saw lead to the current screen. backafter a custom-tab switch re-selects the previous tab.
Fixes #
- A modal no longer hides on-screen UI outside it; an inline
Dialogno longer turns its page into an overlay; sheet scrims attach to their sheet. - Effect durations of 0 no longer crash the highlight / border / awakening animations (clamped to 1 ms).
mission_explorationand mission responses are trimmed to fit the 15 KB transport cap instead of being dropped.- Cache: on-screen nodes are never capped away; over-cap overlays no longer re-upload on every capture; a 200 with zero rows written is retried; a file the backend keeps rejecting is quarantined instead of blocking sync; sync enables as soon as the gate and API key allow it.
- Lifecycle flushes send a diff, not a full baseline; the active border is dismissed on stop.
- Release builds: tracker IDs no longer collapse to
Instance of …(0.2.0 formats kept for strings, numbers, enums, dates and lists);WireLogand inbound frame logging are silent. - The SDK's own panel is no longer captured as host UI.
0.2.0 #
Native-parity release: the Flutter SDK's config wiring, call/agent modes, back navigation, and widget UX brought in line with the Android and React Native SDKs and the revrag-core wire contract. Verified live on a physical device.
Breaking changes — migration required #
- Multi-agent (
flowName) support removed — React / React Native parity. One integration is one agent, so the per-flow scoping is gone:embedInitialize,EmbedProviderandembedEvent(andembedTrack*) no longer take aflowName.EmbedWidget.enabledRouteschanges shape fromMap<String, List<String>>(flow → routes) to a flatList<String>:enabledRoutes: {'main': ['home','account']}→enabledRoutes: ['home','account'].EmbedWidget.groupContinuity/groupDelays(flow-keyed maps) become the singlecontinuity:/buttonDelay:.EmbedRouteManager's flow surface (currentFlow,catalogFlow,getFlowForRoute,setCurrentFlow,flowRoutes,continuityForFlow/delayMsForFlow/delayPolicyForFlow) is replaced by a flat API (activeGroup,isRouteEnabled,groupForRoute,enabledRoutes,continuity,delayMs,delayPolicy). Config, cache and event scoping collapse to one agent; existing on-device caches stay valid (the default scope is unchanged). Multi-agent apps must split into separate integrations.
EmbedWidgetapiKey/embedUrl/primaryColor/groupInsetsremoved (and theEmbedButtonInsettype). Credentials come fromembedInitialize; the FAB position (widget_config.position) and colors (widget_config.colorPalette) come from the backend — the host no longer overrides them on the widget. Use a per-widget key only viaembedInitialize.EmbedWidget/EmbedProviderrightPadding/bottomPaddingremoved. FAB position is now driven solely by the backendwidget_config(right_padding/bottom_padding/widget_position) — the host no longer overrides it in code. Set the position from the dashboard instead; remove the two params from yourEmbedWidgetcall.embedInitializeis now the single, all-named intake —embedInitialize(apiKey: …, appVersion: …, embedUrl: …).apiKeymoved from positional to a required named parameter, andappVersionis now required (embedSetAppVersion()and thepackage_info_plusauto-derivation are gone).EmbedProviderremoved. It only forwardedapiKey/embedUrl/appVersionintoembedInitializeand mounted anEmbedWidget; withembedInitializeas the single intake it was a redundant wrapper. CallembedInitialize+ wrap withEmbedWidgetdirectly.- Server-driven panels/cards now render only in
workflowmode. A call the user starts from the FAB (or the inactivity auto-trigger) is a co-pilot call and drops inbound panel frames. To keep panels, start the call from host code withEmbedWidget.startCall(agentTriggerMode: AgentTriggerMode.WORKFLOW). other_dataflags removed — no peer SDK ever read them:widget_tree_snapshot_enabledandscreen_capture_enabled(widget-tree snapshot sends now gate onaction_config.flags.dataCapture), andembed_route_allowlist(the platform route allowlist is now host-prop-only viaEmbedWidget(usePlatformRouteAllowlist: true)). Tenants relying on the old flags must move the opt-out todataCapture/ the host prop.
New #
AgentTriggerMode(co-pilot / workflow) — host-declared atEmbedWidget.startCall(agentTriggerMode:)and latched per call; a pureresolveCallPlandecides who owns the UI, whether panels render, and whether the avatar surface opens. The enum values areCO_PILOT/WORKFLOW(identifier-for-identifier with Android), and the mode's cross-SDK wire string is lowercasecopilot/workflow— serialize withmode.wire, read back withparseAgentTriggerMode(String?)(case- and separator-insensitive, mirrors Android's parser).- Visibility groups (
buttonVisibility) — React Native parity.EmbedWidget(buttonVisibility: EmbedButtonVisibilityConfig(groups: [...]))lets different sets of screens carry different FAB continuity / pre-show delay / delay-policy (EmbedButtonGroupConfig{id, screens, continuity, delayMs, delayPolicy}). Resolves per current route (first matching group wins); a group'sscreensalso join the allow-list. Screens in no group fall back to the widget-levelcontinuity/buttonDelay/embedButtonDelayMs. EmbedWidget.expandWidget()/embedExpandWidget()— programmatic inverse ofcollapseWidget()(emitsagent_tap_to_open);startCall()does not auto-expand.EmbedWidget.openAvatarScreen()— programmatically open the full-screen agent surface (starts a workflow call if none is running, else re-opens the surface after a collapse).action_configcoverage completed —dataCapturemaster gate,awakeningEffect/activeBorderGlow/highlighting/clickTrackingkill-switches, the three themed gradients, and all 26effects.*tuning knobs (unset knobs reproduce the built-in look exactly). New: an active-border watchdog and nudge-time FAB breathing.widget_configcompleted —position.*andagentAvatar.avatarSizemerged; sizing follows the nativeAvatarSizingrule and bar typography scales with it.isCollapsedopening state honored once per lifetime.call_idlatched from/tokenand sent flat onagent_conversation_started/_ended(Android parity).- System-back rung —
backnow sweeps every reachable navigator (deepest-first, PopScope-aware) so nested / per-tab / activity-per-screen hosts no longer exhaust the ladder.
Fixes & behavior (native parity) #
- Inactivity:
nudgeDelay <= 0= off;show_popupshows only with a non-empty tooltip;shouldAutoCollapseCard(captions suppress the fold and cancel a running countdown). - Widget UX: one centered 90/60/40 expanded-bar width; awakening plays only while no call is live and collapse returns the FAB to its dock; static collapsed avatar; avatar-open icon hidden in co-pilot; ink-square and glow-bloom collapse artifacts fixed; a silent config revalidate no longer collapses an open bar; avatar cannot surface on a co-pilot call (stale-plan race closed).
- Memory (OOM guard): the
ExplorationLognode union is now call-scoped (reset onAgentRuntime.stop, not only at mission start) and capped LRU to 12 screens, so it can no longer grow unbounded across calls for the life of a long-lived host process. - Internal:
lib/src/embed.dartsplit into focused part files; comment cleanup;CLAUDE.mdanddocs/CONFIG_WIRING.mdadded.
Fixes (device-verified) #
- Orb-flight performance. The awakening overlay no longer paints a full-screen radial-gradient bloom or two rainbow sweep-shader rings per frame: the bloom is bounded to a circle and a single burst ring is drawn, with smaller blur radii. On a mid-tier device this cut janky frames from ~10% to ~6% (raster p95 22.8 ms → 16.9 ms) — the bottleneck was fill-rate/shader churn, not blur.
- Mic and speaker toggles are single-flight. Rapid mute/unmute (or speaker)
taps no longer stack concurrent native
setMicrophoneEnabled/setSpeakerOncalls that churned the audio session and lagged the agent's speech; a burst now collapses to one native op at a time, and the stale-isMutedread is gone. - FAB collapse no longer dips. On call end the FAB settles straight at its
dock instead of dropping ~14 px and springing back — the
elasticOutlanding bounce that restarted after the FAB had already docked is removed.
Previously staged (0.0.21-dev) #
Behavioral change — retry policy. A tap/navigate whose SEMANTIC dispatch
fired but produced zero observable evidence now fails after ONE attempt
(VerificationOutcome.unknown, terminal): retrying an already-fired action
re-fires it physically (double/triple submits). Synthetic-pointer fallbacks
and null after-snapshots keep their retries — nothing semantic was delivered.
Hosts that tuned retriesPerStep for this case should rely on the backend's
failure_reason: tap_had_no_observable_effect to re-plan instead.
Source-breaking for StepVerifier subclasses. StepVerifier.verify
gained an optional named parameter UiNode? resolvedTarget (the find-ladder
winner the runner acted on). Custom verifier overrides must add it (and may
ignore it).
Also in this release: failure_reason emitted alongside error on
mission_status (the backend reads only the former); node model gains
value/checked/editable/secure with capture-time (sticky) secure
redaction — typed field contents no longer ride text or stable ids; find
ladder edge fixes (rung-1 promotion, single-token subtree hatch, IoU floor,
normalised forbid_text, container-anchored inside_text); config cache
un-brick (other_data versioned + TTL + revalidate; isLive hides only on
explicit false); wait clamp; stale-node tap refusal; CI + lib/core test
suite (180+ tests).
0.0.20 #
Fixes #
- Fixed agent audio dropping when a wired/Bluetooth headset is connected mid-call. The SDK no longer forces the audio route on a polling timer; it relies on LiveKit's native headset prioritization and reacts only to real device-change events.
- The inactivity tooltip no longer appears while a call is connecting/connected (previously it could pop up after the bar auto-collapsed).
- The inactivity tooltip now auto-dismisses on any user interaction (tap, scroll, drag), not only a FAB tap.
Features #
- The expanded voice bar auto-collapses back to the FAB after a call connects while keeping the call live. Delay is configurable via
collapsedView.collapsedTime(seconds, default 15s). - Added a "live call" pulse around the collapsed FAB that appears only after the bar fully collapses and only while the user or agent is speaking. Color is configurable via
colorPalette.pulseColor(default purple#B391F3). - The default avatar fallback now uses a Lottie animation instead of the Material
chat/personicons.
Tweaks #
- Reduced the expanded bar height, Connect-button padding, and mute/end-call button sizes.
- Bumped the SDK version reported to the backend (
sdk_version) to0.0.20.
0.0.19 #
Fixes #
- Relaxed the
livekit_clientconstraint from the exact pin2.6.5back to^2.5.3. The exact pin transitively forcedmeta ^1.17.0(required by livekit_client 2.6.5), which brokepub getfor apps on older Flutter SDKs that pinmeta 1.16.0. The range lets pub select a compatible livekit_client version per app.
0.0.18 #
FAB pre-show delay & per-flow delay policy #
- Added
embedButtonDelayMs(onEmbedWidget/EmbedProvider) — global delay before the FAB appears after navigating to an enabled route. Default0(no delay). - Added
groupDelays(Map<String, EmbedButtonDelay>) — per-flow delay override withEmbedButtonDelayPolicy:perScreen,oncePerGroupEntry,oncePerAppSession. Falls back toembedButtonDelayMs. - Added React-API aliases on
EmbedButtonContinuity:continuous(=keepVisible) andperScreen(=reset), plusEmbedButtonContinuity.fromString/EmbedButtonDelayPolicy.fromStringparsers.
Fixes #
- Nudge/inactivity countdown now starts when the FAB becomes visible (after
embedButtonDelayMs), not when the route is first entered. inactivity_behaviour: "auto_trigger"now maps toInactivityMode.AUTO_TRIGGER(auto-starts the call likestart_call); previously it silently fell back to showing the popup. AddedInactivityMode.fromBehaviour.- FAB no longer renders off-screen on the app's initial/home route — position is recomputed once the screen size is available (post first layout) and on metrics changes.
- Fixed
agentTapToOpen/agentTapToCloseenum identifiers (backend valuesagent_tap_to_open/agent_tap_to_closeunchanged).
Docs #
- Added
doc/fab-visibility-advanced.md(Flutter FAB visibility guide) anddoc/embed-button-delay.md.
0.0.17 #
- Typewriter transcription panel: vertical expansion from bottom to top when
type == "calculation"data received via LiveKit - FAB positioning:
widget_position(bottom-right,bottom-left,top-right,top-left) now respected from backend config - Fixed
bottomPaddingandrightPaddingnot applying correctly to FAB position - Bounce animation on FAB when transcription panel closes
- Analytics event names updated:
agent_tap_to_open/agent_tap_to_close SdkEventNameenum added for all internally-fired analytics event names (exported as public API)- All public event APIs (
embedEvent,embedTrackEvent, etc.) now returnFuture<bool>for send-success feedback EmbedRouteListenerwidget added for GoRouter / ShellRoute automatic route detectionEmbedNavigatorObservernow accepts optionalnameExtractorfor GoRouter alias support- Route matching now strips leading slashes — GoRouter paths (
/home) match aliases (home) - Cache-first config strategy restored in
_fetchUIConfigwith API fallback on cold start - Removed all debug logs; all exceptions handled internally — zero crash guarantee for host app
- Removed dead
_buildTranscriptionPaneloverlay (superseded by inline vertical expansion)
0.0.16 #
- Graceful exception handling across SDK — zero crash guarantee
- Fixed event routing and SDK key resolution issues
- Stability Improvements
0.0.15 #
- Stability Improvements
0.0.14 #
- Minor Bug Fixes and Stability Improvements
0.0.13 #
- Minor Bug Fixes and Stability Improvements
0.0.12 #
- Added capability to send ANALYTICS_DATA event from screens which are not part of enabled routes.
0.0.11 #
- Added ANALYTICS_DATA event for tracking structured analytics events with a specific event identifier
0.0.10 #
- Minor Bug Fixes and UI improvements
0.0.9 #
- Added rightPadding and bottomPadding to set the initial position of Embed Widget
0.0.8 #
- Added onPermissionStatusChanged callback to recieve the permission status change events
- Minor Bug Fixes and Stability Improvements
0.0.7 #
- Minor Bug Fixes and Stability Improvements
0.0.6 #
- Bluetooth Headset Support added
- Added Inactivity Actions
- Minor Bug Fixes and Stability Improvements
0.0.5 #
- Added Multi Flow Support with enabledroutes
0.0.4 #
- Added Support for Re-initialisation of embed agent
0.0.3 #
- Added Support for Buttons
- Added Route based activation
- Added embedEvent call for user generated USER_DATA nd SCREEN_STATE events
- Added Embed Config Service for management of apiKey and embedUrl
- Improved widget context extraction
- Updated Documentation
0.0.2 #
- Removed the requirement for bluetooth permissions
- Updated License
0.0.1 #
- Initial release of Embedded Agent SDK
- Core functionality for automatic widget tree traversal
- Real-time widget monitoring with collapsible interface
- Comprehensive widget property extraction
- Cross-platform support for Android and iOS
- Example applications demonstrating SDK usage
- JSON output format for widget information
- Performance-optimized widget tree analysis