Skip to content

feat(example): expose Voltra E2E tools to agents through Appduct - #329

Merged
V3RON merged 2 commits into
mainfrom
claude/wonderful-bell-fgdqrf
Sep 29, 2026
Merged

V3RON merged 2 commits into
mainfrom
claude/wonderful-bell-fgdqrf

Conversation

@V3RON

@V3RON V3RON commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

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/e2e walkthrough for ADR 0007 is mostly that — pages of it.

Appduct removes the setup half. The app registers functions as tools; the appduct CLI 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 in docs/agents/appduct-e2e.md.

Appduct drives the app; it does not look at the screen. agent-device is still what handles the launcher, the system widget dialogs, and the screenshots.

How does it work?

example/appduct/AppductTools.tsx mounts once from the root layout and renders the tool set for the platform the app is running on. Each platform's useAppductTool hooks live in a component only that platform renders, so a hook never registers against an API the device does not have, and appduct tools lists 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, runtime serverUpdate overrides, and a buffered read of Voltra's event stream. Voltra interactions arrive asynchronously, long after the call that set the surface up, so ios_read_events drains 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 to appduct events for live following.
  • voltra/android (18 tools) — placements addressed by appWidgetId, an app-initiated pin request, per-placement and per-widget-type env.configuration writes (the stand-in for the system Edit Widget dialog, which iOS has no runtime equivalent for), Dynamic Widget props, runtime serverUpdate overrides, and ongoing notifications driven by their serializable payload — the Android counterpart of a Live Activity.

Both sets carry an open_screen tool with the platform's own route list, and a reset tool for a clean slate between cases. Every tool declares a zod v4 input schema with per-parameter descriptions, an output schema, and readOnlyHint/destructiveHint annotations, 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.yaml gains a minimumReleaseAgeExclude entry 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.
  • zod is 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) and pnpm typecheck all pass. tsc over the example app reports no errors in the new files; the 132 it reports elsewhere are pre-existing and untouched here (the example has no typecheck task).

Two checks specific to this change were run against the installed Appduct and zod:

  • All 35 tool names validate against @appduct/shared's TOOL_NAME_PATTERN, both groups against isValidToolGroup, every description is within MAX_TOOL_DESCRIPTION_LENGTH, and no name is registered twice.
  • Every zod construct used by the two tool files exports JSON Schema through the exact path Appduct uses (~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-native from 0.11 and rebuilding the example app. appduct sessions link --open ios-sim established a session listing the 17 voltra/ios tools, and:

  • ios_open_screen navigated to /ios/activity and /ios/widgets; an unknown route was rejected with a validation error.
  • ios_start_live_activity and ios_update_live_activity put "Order #777 / Delivered" on the Lock Screen; ios_live_activity_status and ios_read_events (a stateChange event) agreed, and ios_stop_live_activity ended it.
  • ios_update_dynamic_widget showed 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_update round-tripped an override and fell back to the app.json value.
  • ios_preload_images, ios_clear_preloaded_images, ios_reload_widgets, ios_clear_widgets and ios_reset succeeded.

Not exercised: the Android tools, and the ios_read_events interaction 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 fresh sessions 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

claude and others added 2 commits September 22, 2026 16:04
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.
@V3RON
V3RON merged commit 667174c into main Sep 29, 2026
16 checks passed
@V3RON
V3RON deleted the claude/wonderful-bell-fgdqrf branch September 29, 2026 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants