feat(example): expose Voltra E2E tools to agents through Appduct - #329
Merged
Merged
Conversation
Register per-platform Appduct tools in the example app so an agent can set widget, Live Activity, and ongoing notification state directly, instead of tapping through the tab bar and the launcher to get there. iOS and Android register their own tool groups from components only that platform renders. Document the catalogue and the connect/invoke flow in docs/agents/appduct-e2e.md, referenced from AGENTS.md and the E2E README. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KuivuFw4HyvswduXRSsWwU
Appduct 0.13 moved the CLI under subcommands (sessions link/ls, tools ls/describe/call, events tail). Update the E2E guide to match, document that tool calls need the app in the foreground and that a suspended session needs a fresh link, and list the extra MCP tools.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this?
End-to-end testing Voltra means putting a widget, a Live Activity, or an ongoing notification into a known state and then looking at it. Today the setup half is the expensive part: an agent taps through the tab bar to reach a screen, drives the launcher to pin a placement, and reads state back off a screenshot. The
example/e2ewalkthrough for ADR 0007 is mostly that — pages of it.Appduct removes the setup half. The app registers functions as tools; the
appductCLI or its MCP server calls them over a pinned local connection. This registers 35 such tools in the example app, covering the Voltra surfaces an agent actually needs to drive, and documents them indocs/agents/appduct-e2e.md.Appduct drives the app; it does not look at the screen.
agent-deviceis still what handles the launcher, the system widget dialogs, and the screenshots.How does it work?
example/appduct/AppductTools.tsxmounts once from the root layout and renders the tool set for the platform the app is running on. Each platform'suseAppductToolhooks live in a component only that platform renders, so a hook never registers against an API the device does not have, andappduct toolslists exactly what the connected session can serve.The two sets differ because the platforms do:
voltra/ios(17 tools) — Home Screen widgets by family, Dynamic Widget props, Dynamic Live Activities (start/update/stop/status, addressed by the activity name a start returns), image preloading, runtimeserverUpdateoverrides, and a buffered read of Voltra's event stream. Voltra interactions arrive asynchronously, long after the call that set the surface up, soios_read_eventsdrains what arrived since the last read — that is what makes "did the Home Screen tap reach the app?" an assertion rather than a race. The same events are also pushed toappduct eventsfor live following.voltra/android(18 tools) — placements addressed byappWidgetId, an app-initiated pin request, per-placement and per-widget-typeenv.configurationwrites (the stand-in for the system Edit Widget dialog, which iOS has no runtime equivalent for), Dynamic Widget props, runtimeserverUpdateoverrides, and ongoing notifications driven by their serializable payload — the Android counterpart of a Live Activity.Both sets carry an
open_screentool with the platform's own route list, and aresettool for a clean slate between cases. Every tool declares a zod v4 input schema with per-parameter descriptions, an output schema, andreadOnlyHint/destructiveHintannotations, because an agent picks a tool from one signature line.Bootstrapping is one
require('@appduct/react-native/auto')under__DEV__in the app entry. A release build ships no native Appduct module at all, which leaves every registration inert.Two things worth a reviewer's attention:
pnpm-workspace.yamlgains aminimumReleaseAgeExcludeentry for the two@appduct/*packages. The workspace quarantines dependencies for 14 days and every published Appduct version is newer than that, so nothing installs without it. The exclusion is deliberately narrow and commented; drop it once Appduct's release cadence settles.zodis added to the example app. Appduct publishes a tool's shape from its schema, and zod v4's built-in JSON Schema exporter is what gives agents a real signature instead of an opaque blob.Why is this useful?
A pass that used to be "open the app, find the tab, tap into the screen, set four fields, reload" is now one
appduct invoke. Reading state back — which placements exist, what each one is configured with, what a widget would fetch from right now, whether an activity is still running — becomes a tool call returning structured data instead of an inference from a screenshot. And because each platform registers its own group, an agent connected to an emulator cannot reach for a tool that only means something on iOS.Validation
pnpm lint,pnpm format:js:check,pnpm test(25 tasks) andpnpm typecheckall pass.tscover the example app reports no errors in the new files; the 132 it reports elsewhere are pre-existing and untouched here (the example has notypechecktask).Two checks specific to this change were run against the installed Appduct and zod:
@appduct/shared'sTOOL_NAME_PATTERN, both groups againstisValidToolGroup, every description is withinMAX_TOOL_DESCRIPTION_LENGTH, and no name is registered twice.~standard.jsonSchema[mode]({ target: 'draft-2020-12' })) — 19 shapes, 0 failures. This matters because Appduct throws at registration in a dev build for a schema it cannot publish.The iOS tools were exercised end to end on an iOS 18.0 simulator against Appduct 0.13.0, after upgrading
@appduct/react-nativefrom 0.11 and rebuilding the example app.appduct sessions link --open ios-simestablished a session listing the 17voltra/iostools, and:ios_open_screennavigated to/ios/activityand/ios/widgets; an unknown route was rejected with a validation error.ios_start_live_activityandios_update_live_activityput "Order #777 / Delivered" on the Lock Screen;ios_live_activity_statusandios_read_events(astateChangeevent) agreed, andios_stop_live_activityended it.ios_update_dynamic_widgetshowed its props (headline, server city, temperature) on a placed Home Screen widget.ios_set_widget_server_update/ios_get_widget_server_update/ios_clear_widget_server_updateround-tripped an override and fell back to theapp.jsonvalue.ios_preload_images,ios_clear_preloaded_images,ios_reload_widgets,ios_clear_widgetsandios_resetsucceeded.Not exercised: the Android tools, and the
ios_read_eventsinteraction recipe (the demo widget has no button to tap). Two behaviors worth knowing, both documented in the guide: tool calls only run while the app is in the foreground, and a session that suspended (for example after the device locked) needs a freshsessions link.Appduct 0.13 moved the CLI under subcommands (
sessions link,tools call,events tail), so the guide is updated to match.Generated by Claude Code