@aotui/mobile-ai-native 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/GUIDE.md +317 -0
  2. package/README.md +114 -0
  3. package/dist/core/action/createActionRuntime.d.ts +13 -0
  4. package/dist/core/action/createActionRuntime.js +69 -0
  5. package/dist/core/action/defineAction.d.ts +21 -0
  6. package/dist/core/action/defineAction.js +3 -0
  7. package/dist/core/effect/types.d.ts +1 -0
  8. package/dist/core/effect/types.js +1 -0
  9. package/dist/core/ref/ref-index.d.ts +5 -0
  10. package/dist/core/ref/ref-index.js +11 -0
  11. package/dist/core/snapshot/createSnapshotBundle.d.ts +6 -0
  12. package/dist/core/snapshot/createSnapshotBundle.js +12 -0
  13. package/dist/core/state/createStore.d.ts +5 -0
  14. package/dist/core/state/createStore.js +19 -0
  15. package/dist/core/types.d.ts +30 -0
  16. package/dist/core/types.js +1 -0
  17. package/dist/demo/inbox/InboxGUI.d.ts +1 -0
  18. package/dist/demo/inbox/InboxGUI.js +7 -0
  19. package/dist/demo/inbox/InboxTUI.d.ts +1 -0
  20. package/dist/demo/inbox/InboxTUI.js +12 -0
  21. package/dist/demo/inbox/actions.d.ts +16 -0
  22. package/dist/demo/inbox/actions.js +56 -0
  23. package/dist/demo/inbox/createInboxApp.d.ts +20 -0
  24. package/dist/demo/inbox/createInboxApp.js +50 -0
  25. package/dist/demo/inbox/effects.d.ts +9 -0
  26. package/dist/demo/inbox/effects.js +12 -0
  27. package/dist/demo/inbox/state.d.ts +33 -0
  28. package/dist/demo/inbox/state.js +56 -0
  29. package/dist/index.d.ts +10 -0
  30. package/dist/index.js +10 -0
  31. package/dist/projection/gui/AppProvider.d.ts +20 -0
  32. package/dist/projection/gui/AppProvider.js +15 -0
  33. package/dist/projection/gui/hooks.d.ts +10 -0
  34. package/dist/projection/gui/hooks.js +18 -0
  35. package/dist/projection/tui/renderTUI.d.ts +6 -0
  36. package/dist/projection/tui/renderTUI.js +14 -0
  37. package/dist/ref/RefContext.d.ts +11 -0
  38. package/dist/ref/RefContext.js +15 -0
  39. package/dist/ref/useArrayRef.d.ts +1 -0
  40. package/dist/ref/useArrayRef.js +19 -0
  41. package/dist/ref/useDataRef.d.ts +1 -0
  42. package/dist/ref/useDataRef.js +8 -0
  43. package/dist/tool/createToolBridge.d.ts +18 -0
  44. package/dist/tool/createToolBridge.js +57 -0
  45. package/package.json +49 -0
package/GUIDE.md ADDED
@@ -0,0 +1,317 @@
1
+ # GUIDE: Building an Agent Native iOS Calendar App
2
+
3
+ This guide is for a developer who wants to build a high-quality Agent Native calendar app on iOS using `@aotui/mobile-ai-native`.
4
+
5
+ The goal is simple:
6
+
7
+ - humans use a normal GUI calendar
8
+ - the LLM uses tools through a TUI snapshot
9
+ - both operate on the same app state
10
+ - the human can see the result of the LLM's actions
11
+
12
+ ## 1. What You Are Actually Building
13
+
14
+ Do not think of this as:
15
+
16
+ - "an AI that clicks buttons"
17
+
18
+ Think of it as:
19
+
20
+ - "one calendar app with two input channels"
21
+
22
+ Those two channels are:
23
+
24
+ 1. human input through GUI
25
+ 2. LLM input through tools
26
+
27
+ They must meet in the same place:
28
+
29
+ - the same `State`
30
+ - the same `Action` layer
31
+
32
+ That is the whole trick.
33
+
34
+ ## 2. Why This Architecture Matters
35
+
36
+ If the LLM drives the GUI by fake taps, your system becomes fragile.
37
+
38
+ Why?
39
+
40
+ - the UI layout changes and the AI breaks
41
+ - the AI acts on pixels, not meaning
42
+ - debugging becomes miserable
43
+
44
+ Instead, this framework makes the LLM act on meaning:
45
+
46
+ - `openEvent`
47
+ - `createEvent`
48
+ - `moveEvent`
49
+ - `searchEvents`
50
+ - `changeCalendarView`
51
+
52
+ The GUI is one projection of state.
53
+ The TUI snapshot is another projection of state.
54
+
55
+ The LLM should never guess ids from the screen.
56
+ It should receive semantic refs from the current `SnapshotBundle`.
57
+
58
+ ## 3. The Core Mental Model
59
+
60
+ Your calendar app should follow this loop:
61
+
62
+ `State -> GUI`
63
+ `State -> TUI Snapshot`
64
+ `Tool -> Action -> Event/Effect -> State`
65
+ `GUI Event -> Action -> Event/Effect -> State`
66
+
67
+ That means:
68
+
69
+ - GUI controls do not own business logic
70
+ - TUI does not own business logic
71
+ - tools do not own business logic
72
+ - `Action` is the one real business entry
73
+
74
+ ## 4. Calendar App State
75
+
76
+ A good Agent Native calendar app should have state shaped more like this:
77
+
78
+ ```ts
79
+ type CalendarState = {
80
+ shell: {
81
+ currentView: "day" | "week" | "month";
82
+ selectedDate: string;
83
+ recentTrace: string | null;
84
+ };
85
+ events: {
86
+ items: CalendarEvent[];
87
+ selectedEventId: string | null;
88
+ searchQuery: string;
89
+ isLoading: boolean;
90
+ };
91
+ };
92
+ ```
93
+
94
+ Good rule:
95
+
96
+ - if it affects GUI, TUI, tool visibility, or trace, it belongs in framework state
97
+ - if it is only a tiny render helper, keep it local
98
+
99
+ ## 5. Calendar Actions
100
+
101
+ Your first calendar app should expose a very small action set:
102
+
103
+ - `openEvent`
104
+ - `searchEvents`
105
+ - `createEvent`
106
+ - `moveEvent`
107
+ - `changeCalendarView`
108
+
109
+ Start smaller than you want.
110
+ You can always add more later.
111
+
112
+ The important rule is:
113
+
114
+ - actions should be domain actions
115
+ - not UI actions like `tapButton` or `scrollList`
116
+
117
+ ## 6. Why Refs Matter in Calendar Apps
118
+
119
+ Calendars are full of structured data:
120
+
121
+ - one day
122
+ - one week
123
+ - one event
124
+ - one search result list
125
+
126
+ The LLM should see semantic markers like:
127
+
128
+ ```text
129
+ (1:1 with Sarah at 14:00)[event:events[0]]
130
+ (Today)[day:visible_days[0]]
131
+ ```
132
+
133
+ That is what `useDataRef` and `useArrayRef` are for.
134
+
135
+ ### Use `useDataRef`
136
+
137
+ Use it for one event, one selected day, one active calendar.
138
+
139
+ ```tsx
140
+ const selectedEventRef = useDataRef("event", selectedEvent, "selected_event");
141
+ <text>{selectedEventRef("Selected event")}</text>;
142
+ ```
143
+
144
+ ### Use `useArrayRef`
145
+
146
+ Use it for event lists, visible day buckets, agenda results.
147
+
148
+ ```tsx
149
+ const [eventsRef, eventRef] = useArrayRef("event", visibleEvents, "events");
150
+ <text>{eventsRef("Visible events")}</text>
151
+ {visibleEvents.map((event, index) => (
152
+ <item key={event.id}>{eventRef(index, event.title)}</item>
153
+ ))}
154
+ ```
155
+
156
+ ## 7. Why `snapshotId` Is Non-Negotiable
157
+
158
+ The screen can change after the LLM reads it.
159
+
160
+ Maybe:
161
+
162
+ - the selected day changed
163
+ - search results refreshed
164
+ - the event was deleted
165
+
166
+ So tool execution must always be tied to the exact snapshot the LLM saw:
167
+
168
+ ```ts
169
+ await bridge.executeTool("openEvent", { event: "events[0]" }, snapshotId);
170
+ ```
171
+
172
+ This prevents the runtime from guessing against the latest UI.
173
+
174
+ That is a big deal.
175
+ It is the difference between:
176
+
177
+ - a reliable system
178
+ - and a haunted house
179
+
180
+ ## 8. How To Build the iOS App
181
+
182
+ ### Step 1: Keep this package as the core
183
+
184
+ Use `@aotui/mobile-ai-native` for:
185
+
186
+ - state
187
+ - refs
188
+ - snapshot bundle creation
189
+ - tool bridge
190
+
191
+ ### Step 2: Build a thin React Native host
192
+
193
+ The React Native app should do only host work:
194
+
195
+ - render native GUI
196
+ - host the agent session
197
+ - ask the framework for `SnapshotBundle`
198
+ - pass tool calls into `executeTool`
199
+
200
+ ### Step 3: Build GUI and TUI as separate projections
201
+
202
+ Do not auto-generate TUI from GUI.
203
+
204
+ For calendar apps this is especially important because:
205
+
206
+ - GUI cares about touch and spatial layout
207
+ - TUI cares about semantic clarity for the model
208
+
209
+ ### Step 4: Start with one vertical slice
210
+
211
+ Do not build the whole calendar app at once.
212
+
213
+ Start with:
214
+
215
+ - month or week view
216
+ - visible event list
217
+ - `openEvent`
218
+ - `searchEvents`
219
+
220
+ Only after that is stable, add:
221
+
222
+ - create
223
+ - move
224
+ - delete
225
+ - recurring events
226
+
227
+ ## 9. Suggested First Calendar TUI
228
+
229
+ Your first TUI should be boring and clear, not clever:
230
+
231
+ ```tsx
232
+ <screen name="Calendar">
233
+ <text>View: week</text>
234
+ <text>Date: 2026-03-24</text>
235
+ <text>{daysRef("Visible days")}</text>
236
+ {events.map((event, index) => (
237
+ <item key={event.id}>{eventRef(index, `${event.title} at ${event.startTime}`)}</item>
238
+ ))}
239
+ </screen>
240
+ ```
241
+
242
+ The LLM does not need visual beauty.
243
+ It needs:
244
+
245
+ - stable structure
246
+ - meaningful refs
247
+ - clear tool choices
248
+
249
+ ## 10. Quality Bar for a Good Agent Native Calendar App
250
+
251
+ Before you call the app "good", verify these:
252
+
253
+ ### State correctness
254
+
255
+ - GUI and TUI always reflect the same event state
256
+ - tool calls only act on snapshot-scoped refs
257
+
258
+ ### Tool correctness
259
+
260
+ - invisible tools are not callable
261
+ - stale `snapshotId` fails cleanly
262
+ - missing refs fail with explicit errors
263
+
264
+ ### UX correctness
265
+
266
+ - human sees the result of AI actions
267
+ - recent AI action summary is visible
268
+ - event openings, searches, and edits are understandable
269
+
270
+ ### Product correctness
271
+
272
+ - no tool is named after a button
273
+ - tools match domain intent
274
+ - TUI exposes enough semantic data without dumping noise
275
+
276
+ ## 11. What This Package Does Not Give You Yet
277
+
278
+ Be honest with yourself:
279
+
280
+ this package is still an alpha core.
281
+
282
+ It does not yet give you:
283
+
284
+ - a full React Native adapter
285
+ - iOS simulator harness
286
+ - production persistence
287
+ - production networking
288
+ - voice or background agent integration
289
+
290
+ That is okay.
291
+
292
+ It gives you the hardest part first:
293
+
294
+ - the protocol spine
295
+
296
+ ## 12. Recommended Build Order
297
+
298
+ If your friend is building the calendar app, this is the order I recommend:
299
+
300
+ 1. build one RN screen shell
301
+ 2. integrate framework state and tool bridge
302
+ 3. implement week view with event refs
303
+ 4. implement `openEvent`
304
+ 5. implement `searchEvents`
305
+ 6. add trace banner for recent AI action
306
+ 7. add create/edit flows
307
+ 8. only then add advanced calendar features
308
+
309
+ ## 13. The One Sentence To Remember
310
+
311
+ An Agent Native iOS app is not:
312
+
313
+ - "AI controlling UI"
314
+
315
+ It is:
316
+
317
+ - "one app where GUI and LLM share the same domain actions and the same state"
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @aotui/mobile-ai-native
2
+
3
+ This package is an alpha host-agnostic core for building Agent Native mobile apps.
4
+
5
+ It currently proves the smallest end-to-end loop of the framework:
6
+
7
+ `State -> SnapshotBundle -> Tool Call(ref_id + snapshotId) -> Action -> Event/Effect -> State -> GUI/TUI refresh`
8
+
9
+ If you want a practical build guide for a real iOS app, read [GUIDE.md](./GUIDE.md).
10
+
11
+ ## What This Slice Proves
12
+
13
+ - one shared state system drives both GUI and TUI
14
+ - TUI is handwritten and can expose semantic refs with `useDataRef` and `useArrayRef`
15
+ - the framework builds one atomic `SnapshotBundle`
16
+ - tools execute against the exact `snapshotId` the LLM saw
17
+ - `refIndex` stores serializable snapshot payloads, not live object references
18
+ - one pure local action and one effect-driven action both work
19
+
20
+ ## Core Contract
21
+
22
+ The LLM does not operate on guessed ids.
23
+ It sees semantic markers like:
24
+
25
+ ```text
26
+ (Welcome back)[message:messages[0]]
27
+ ```
28
+
29
+ Then it calls a tool with:
30
+
31
+ ```ts
32
+ await bridge.executeTool("openMessage", { message: "messages[0]" }, snapshotId);
33
+ ```
34
+
35
+ The runtime resolves that `ref_id` from the `SnapshotBundle.refIndex` for the same `snapshotId`.
36
+
37
+ ## Ref APIs
38
+
39
+ ### `useDataRef`
40
+
41
+ Use for a single object:
42
+
43
+ ```tsx
44
+ const pinnedRef = useDataRef("message", message, "messages[0]");
45
+ <text>{pinnedRef("Pinned message")}</text>;
46
+ ```
47
+
48
+ ### `useArrayRef`
49
+
50
+ Use for an array:
51
+
52
+ ```tsx
53
+ const [listRef, itemRef] = useArrayRef("message", messages, "messages");
54
+ <text>{listRef("Inbox messages")}</text>;
55
+ <item>{itemRef(0, "Welcome back")}</item>;
56
+ ```
57
+
58
+ ## SnapshotBundle
59
+
60
+ The LLM-facing read model is:
61
+
62
+ ```ts
63
+ type SnapshotBundle = {
64
+ snapshotId: string;
65
+ generatedAt: number;
66
+ tui: string;
67
+ refIndex: Record<string, { type: string; value: unknown }>;
68
+ visibleTools: ToolDefinition[];
69
+ };
70
+ ```
71
+
72
+ This is intentionally atomic:
73
+
74
+ - `tui`
75
+ - `visibleTools`
76
+ - `refIndex`
77
+
78
+ must all come from the same render tick.
79
+
80
+ ## Why `refIndex` Stores Serializable Payloads
81
+
82
+ Screens can change after the LLM reads them.
83
+ Live object references may already be gone.
84
+
85
+ So `refIndex` stores serializable snapshot payloads.
86
+ When the LLM later calls a tool, the framework reconstructs the action input from the payload attached to that `snapshotId`.
87
+
88
+ ## Current Status
89
+
90
+ This is not yet a full React Native runtime or iOS shell.
91
+
92
+ What it gives you today:
93
+
94
+ - shared state core
95
+ - semantic refs with `useDataRef` and `useArrayRef`
96
+ - atomic `SnapshotBundle`
97
+ - snapshot-scoped tool execution
98
+ - a working inbox vertical slice
99
+
100
+ What you still need for a production iOS app:
101
+
102
+ - a thin React Native host adapter
103
+ - real GUI components
104
+ - model orchestration and networking
105
+ - product-level trace UI and persistence
106
+
107
+ ## Current Demo
108
+
109
+ The inbox demo exposes:
110
+
111
+ - `openMessage`
112
+ - `searchMessages`
113
+
114
+ and proves that GUI and TUI both refresh from the same state after tool execution.
@@ -0,0 +1,13 @@
1
+ import type { ActionDefinition } from "./defineAction";
2
+ import type { ActionResult, Store, ToolDefinition } from "../types";
3
+ export declare function createActionRuntime<State, Event>(config: {
4
+ store: Store<State, Event>;
5
+ actions: Array<ActionDefinition<State, Event, any>>;
6
+ effects?: Record<string, (ctx: {
7
+ getState(): State;
8
+ emit(event: Event): void;
9
+ }, input: any) => Promise<void> | void>;
10
+ }): {
11
+ executeAction: (name: string, input: Record<string, unknown>) => Promise<ActionResult>;
12
+ listVisibleTools: () => ToolDefinition[];
13
+ };
@@ -0,0 +1,69 @@
1
+ export function createActionRuntime(config) {
2
+ const actionsByName = new Map(config.actions.map((action) => [action.name, action]));
3
+ const trace = {
4
+ start(_summary) { },
5
+ update(_summary) { },
6
+ success(_summary) { },
7
+ fail(_summary) { },
8
+ };
9
+ async function executeAction(name, input) {
10
+ const action = actionsByName.get(name);
11
+ if (!action) {
12
+ return {
13
+ success: false,
14
+ error: {
15
+ code: "ACTION_NOT_FOUND",
16
+ message: `Action ${name} was not found`,
17
+ },
18
+ };
19
+ }
20
+ if (!action.visibility(config.store.getState())) {
21
+ return {
22
+ success: false,
23
+ error: {
24
+ code: "ACTION_NOT_VISIBLE",
25
+ message: `Action ${name} is not currently visible`,
26
+ },
27
+ };
28
+ }
29
+ const parsed = action.schema.safeParse(input);
30
+ if (!parsed.success) {
31
+ return {
32
+ success: false,
33
+ error: {
34
+ code: "ACTION_INVALID_INPUT",
35
+ message: parsed.error.message,
36
+ },
37
+ };
38
+ }
39
+ const ctx = {
40
+ getState: () => config.store.getState(),
41
+ emit: (event) => config.store.emit(event),
42
+ runEffect: async (name, input) => {
43
+ const effect = config.effects?.[name];
44
+ if (!effect) {
45
+ return;
46
+ }
47
+ await effect({
48
+ getState: () => config.store.getState(),
49
+ emit: (event) => config.store.emit(event),
50
+ }, input);
51
+ },
52
+ trace,
53
+ };
54
+ return await action.handler(ctx, parsed.data);
55
+ }
56
+ function listVisibleTools() {
57
+ const state = config.store.getState();
58
+ return config.actions
59
+ .filter((action) => action.visibility(state))
60
+ .map((action) => ({
61
+ name: action.name,
62
+ description: action.description,
63
+ }));
64
+ }
65
+ return {
66
+ executeAction,
67
+ listVisibleTools,
68
+ };
69
+ }
@@ -0,0 +1,21 @@
1
+ import type { ZodType } from "zod";
2
+ import type { ActionResult } from "../types";
3
+ export type ActionContext<State, Event> = {
4
+ getState(): State;
5
+ emit(event: Event): void;
6
+ runEffect(name: string, input: unknown): Promise<void>;
7
+ trace: {
8
+ start(summary: string): void;
9
+ update(summary: string): void;
10
+ success(summary?: string): void;
11
+ fail(summary: string): void;
12
+ };
13
+ };
14
+ export type ActionDefinition<State, Event, Input> = {
15
+ name: string;
16
+ description: string;
17
+ schema: ZodType<Input>;
18
+ visibility(state: State): boolean;
19
+ handler(ctx: ActionContext<State, Event>, input: Input): Promise<ActionResult> | ActionResult;
20
+ };
21
+ export declare function defineAction<State, Event, Input>(config: ActionDefinition<State, Event, Input>): ActionDefinition<State, Event, Input>;
@@ -0,0 +1,3 @@
1
+ export function defineAction(config) {
2
+ return config;
3
+ }
@@ -0,0 +1 @@
1
+ export type EffectMap = Record<string, (input: unknown) => Promise<void> | void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { RefIndexEntry } from "../types";
2
+ export declare function createRefCollector(): {
3
+ register(refId: string, entry: RefIndexEntry): void;
4
+ snapshot(): Record<string, RefIndexEntry>;
5
+ };
@@ -0,0 +1,11 @@
1
+ export function createRefCollector() {
2
+ const refIndex = {};
3
+ return {
4
+ register(refId, entry) {
5
+ refIndex[refId] = structuredClone(entry);
6
+ },
7
+ snapshot() {
8
+ return structuredClone(refIndex);
9
+ },
10
+ };
11
+ }
@@ -0,0 +1,6 @@
1
+ import type { SnapshotBundle, ToolDefinition, RefIndexEntry } from "../types";
2
+ export declare function createSnapshotBundle(input: {
3
+ tui: string;
4
+ refIndex: Record<string, RefIndexEntry>;
5
+ visibleTools: ToolDefinition[];
6
+ }): SnapshotBundle;
@@ -0,0 +1,12 @@
1
+ let snapshotCounter = 0;
2
+ export function createSnapshotBundle(input) {
3
+ const generatedAt = Date.now();
4
+ snapshotCounter += 1;
5
+ return {
6
+ snapshotId: `snap_${generatedAt}_${snapshotCounter}`,
7
+ generatedAt,
8
+ tui: input.tui,
9
+ refIndex: input.refIndex,
10
+ visibleTools: input.visibleTools,
11
+ };
12
+ }
@@ -0,0 +1,5 @@
1
+ import type { StateReducer, Store } from "../types";
2
+ export declare function createStore<State, Event>(config: {
3
+ initialState: State;
4
+ reduce: StateReducer<State, Event>;
5
+ }): Store<State, Event>;
@@ -0,0 +1,19 @@
1
+ export function createStore(config) {
2
+ let state = config.initialState;
3
+ const listeners = new Set();
4
+ return {
5
+ getState() {
6
+ return state;
7
+ },
8
+ emit(event) {
9
+ state = config.reduce(state, event);
10
+ listeners.forEach((listener) => listener());
11
+ },
12
+ subscribe(listener) {
13
+ listeners.add(listener);
14
+ return () => {
15
+ listeners.delete(listener);
16
+ };
17
+ },
18
+ };
19
+ }
@@ -0,0 +1,30 @@
1
+ export type StateReducer<State, Event> = (state: State, event: Event) => State;
2
+ export type Store<State, Event> = {
3
+ getState(): State;
4
+ emit(event: Event): void;
5
+ subscribe(listener: () => void): () => void;
6
+ };
7
+ export type RefIndexEntry = {
8
+ type: string;
9
+ value: unknown;
10
+ };
11
+ export type ToolDefinition = {
12
+ name: string;
13
+ description: string;
14
+ };
15
+ export type SnapshotBundle = {
16
+ snapshotId: string;
17
+ generatedAt: number;
18
+ tui: string;
19
+ refIndex: Record<string, RefIndexEntry>;
20
+ visibleTools: ToolDefinition[];
21
+ };
22
+ export type ActionResult<T = unknown> = {
23
+ success: boolean;
24
+ message?: string;
25
+ data?: T;
26
+ error?: {
27
+ code: string;
28
+ message: string;
29
+ };
30
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function InboxGUI(): import("preact").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ /** @jsxImportSource preact */
3
+ import { useAppState } from "../../projection/gui/hooks";
4
+ export function InboxGUI() {
5
+ const { state } = useAppState();
6
+ return (_jsxs("gui-screen", { children: [_jsx("gui-trace", { children: state.shell.recentTrace ?? "" }), _jsx("gui-opened", { children: state.inbox.openedMessageId ?? "" }), state.inbox.items.map((item) => (_jsx("gui-item", { children: item.subject }, item.id)))] }));
7
+ }
@@ -0,0 +1 @@
1
+ export declare function InboxTUI(): import("preact").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ /** @jsxImportSource preact */
3
+ import { useDataRef } from "../../ref/useDataRef";
4
+ import { useArrayRef } from "../../ref/useArrayRef";
5
+ import { useAppState } from "../../projection/gui/hooks";
6
+ export function InboxTUI() {
7
+ const { state } = useAppState();
8
+ const firstMessage = state.inbox.items[0] ?? { id: "empty", subject: "Empty", opened: false };
9
+ const openedRef = useDataRef("message", firstMessage, "opened_message");
10
+ const [listRef, itemRef] = useArrayRef("message", state.inbox.items, "messages");
11
+ return (_jsxs("screen", { name: "Inbox", children: [_jsx("text", { children: listRef("Inbox messages") }), _jsxs("text", { children: ["Query: ", state.inbox.query || "(empty)"] }), _jsxs("text", { children: ["Opened: ", String(Boolean(state.inbox.openedMessageId))] }), state.inbox.items.map((item, index) => (_jsx("item", { children: itemRef(index, item.subject) }, item.id))), _jsx("text", { children: openedRef("Opened message") })] }));
12
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import type { InboxEvent, InboxState } from "./state";
3
+ declare const inboxMessageSchema: z.ZodObject<{
4
+ id: z.ZodString;
5
+ subject: z.ZodString;
6
+ opened: z.ZodBoolean;
7
+ }, z.core.$strip>;
8
+ export declare function createInboxActions(): {
9
+ openMessage: import("../../core/action/defineAction").ActionDefinition<InboxState, InboxEvent, {
10
+ message: z.infer<typeof inboxMessageSchema>;
11
+ }>;
12
+ searchMessages: import("../../core/action/defineAction").ActionDefinition<InboxState, InboxEvent, {
13
+ query: string;
14
+ }>;
15
+ };
16
+ export {};
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../core/action/defineAction";
3
+ const inboxMessageSchema = z.object({
4
+ id: z.string(),
5
+ subject: z.string(),
6
+ opened: z.boolean(),
7
+ });
8
+ export function createInboxActions() {
9
+ const openMessage = defineAction({
10
+ name: "openMessage",
11
+ description: "Open a message from the inbox.",
12
+ schema: z.object({
13
+ message: inboxMessageSchema,
14
+ }),
15
+ visibility(state) {
16
+ return state.shell.currentTab === "inbox";
17
+ },
18
+ handler(ctx, input) {
19
+ ctx.emit({ type: "MessageOpened", messageId: input.message.id });
20
+ ctx.emit({
21
+ type: "TraceUpdated",
22
+ summary: `Opened message ${input.message.subject}`,
23
+ });
24
+ return {
25
+ success: true,
26
+ data: { openedMessageId: input.message.id },
27
+ };
28
+ },
29
+ });
30
+ const searchMessages = defineAction({
31
+ name: "searchMessages",
32
+ description: "Search inbox messages.",
33
+ schema: z.object({
34
+ query: z.string().min(1),
35
+ }),
36
+ visibility(state) {
37
+ return state.shell.currentTab === "inbox";
38
+ },
39
+ async handler(ctx, input) {
40
+ ctx.emit({ type: "SearchStarted", query: input.query });
41
+ ctx.emit({
42
+ type: "TraceUpdated",
43
+ summary: `Started search for ${input.query}`,
44
+ });
45
+ await ctx.runEffect("searchMessages", input);
46
+ return {
47
+ success: true,
48
+ message: `Started search for ${input.query}`,
49
+ };
50
+ },
51
+ });
52
+ return {
53
+ openMessage,
54
+ searchMessages,
55
+ };
56
+ }
@@ -0,0 +1,20 @@
1
+ import { type InboxMessage } from "./state";
2
+ export declare function createInboxApp(config: {
3
+ initialMessages: InboxMessage[];
4
+ }): {
5
+ store: import("../../core/types").Store<import("./state").InboxState, import("./state").InboxEvent>;
6
+ bridge: {
7
+ listTools(): {
8
+ name: string;
9
+ description: string;
10
+ }[];
11
+ getSnapshotBundle(): import("../../core/types").SnapshotBundle;
12
+ executeTool(name: string, input: Record<string, unknown>, snapshotId: string): Promise<import("../../core/types").ActionResult>;
13
+ };
14
+ gui: {
15
+ render: () => string;
16
+ getVisibleSubjects(): string[];
17
+ getOpenedMessageId(): string | null;
18
+ getRecentTrace(): string;
19
+ };
20
+ };
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ /** @jsxImportSource preact */
3
+ import renderToString from "preact-render-to-string";
4
+ import { createActionRuntime } from "../../core/action/createActionRuntime";
5
+ import { createStore } from "../../core/state/createStore";
6
+ import { renderTUI } from "../../projection/tui/renderTUI";
7
+ import { AppProvider } from "../../projection/gui/AppProvider";
8
+ import { createToolBridge } from "../../tool/createToolBridge";
9
+ import { createInboxActions } from "./actions";
10
+ import { createInboxEffects } from "./effects";
11
+ import { InboxGUI } from "./InboxGUI";
12
+ import { InboxTUI } from "./InboxTUI";
13
+ import { createInitialInboxState, reduceInboxState, } from "./state";
14
+ export function createInboxApp(config) {
15
+ const store = createStore({
16
+ initialState: createInitialInboxState(config.initialMessages),
17
+ reduce: reduceInboxState,
18
+ });
19
+ const actions = createInboxActions();
20
+ const actionRuntime = createActionRuntime({
21
+ store,
22
+ actions: [actions.openMessage, actions.searchMessages],
23
+ effects: createInboxEffects(config.initialMessages),
24
+ });
25
+ const bridge = createToolBridge({
26
+ actionRuntime,
27
+ renderCurrentSnapshot() {
28
+ return renderTUI(_jsx(AppProvider, { store: store, actionRuntime: actionRuntime, children: _jsx(InboxTUI, {}) }), { visibleTools: actionRuntime.listVisibleTools() });
29
+ },
30
+ });
31
+ function renderGUI() {
32
+ return renderToString(_jsx(AppProvider, { store: store, actionRuntime: actionRuntime, children: _jsx(InboxGUI, {}) }));
33
+ }
34
+ return {
35
+ store,
36
+ bridge,
37
+ gui: {
38
+ render: renderGUI,
39
+ getVisibleSubjects() {
40
+ return store.getState().inbox.items.map((item) => item.subject);
41
+ },
42
+ getOpenedMessageId() {
43
+ return store.getState().inbox.openedMessageId;
44
+ },
45
+ getRecentTrace() {
46
+ return store.getState().shell.recentTrace ?? "";
47
+ },
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,9 @@
1
+ import type { InboxEvent, InboxMessage, InboxState } from "./state";
2
+ export declare function createInboxEffects(allMessages: InboxMessage[]): {
3
+ searchMessages(ctx: {
4
+ getState(): InboxState;
5
+ emit(event: InboxEvent): void;
6
+ }, input: {
7
+ query: string;
8
+ }): Promise<void>;
9
+ };
@@ -0,0 +1,12 @@
1
+ export function createInboxEffects(allMessages) {
2
+ return {
3
+ async searchMessages(ctx, input) {
4
+ const items = allMessages.filter((item) => item.subject.toLowerCase().includes(input.query.toLowerCase()));
5
+ ctx.emit({
6
+ type: "SearchSucceeded",
7
+ query: input.query,
8
+ items,
9
+ });
10
+ },
11
+ };
12
+ }
@@ -0,0 +1,33 @@
1
+ export type InboxMessage = {
2
+ id: string;
3
+ subject: string;
4
+ opened: boolean;
5
+ };
6
+ export type InboxState = {
7
+ shell: {
8
+ currentTab: "inbox" | "settings";
9
+ recentTrace: string | null;
10
+ };
11
+ inbox: {
12
+ query: string;
13
+ isLoading: boolean;
14
+ items: InboxMessage[];
15
+ openedMessageId: string | null;
16
+ };
17
+ };
18
+ export type InboxEvent = {
19
+ type: "MessageOpened";
20
+ messageId: string;
21
+ } | {
22
+ type: "SearchStarted";
23
+ query: string;
24
+ } | {
25
+ type: "SearchSucceeded";
26
+ query: string;
27
+ items: InboxMessage[];
28
+ } | {
29
+ type: "TraceUpdated";
30
+ summary: string;
31
+ };
32
+ export declare function createInitialInboxState(messages: InboxMessage[]): InboxState;
33
+ export declare function reduceInboxState(state: InboxState, event: InboxEvent): InboxState;
@@ -0,0 +1,56 @@
1
+ export function createInitialInboxState(messages) {
2
+ return {
3
+ shell: {
4
+ currentTab: "inbox",
5
+ recentTrace: null,
6
+ },
7
+ inbox: {
8
+ query: "",
9
+ isLoading: false,
10
+ items: messages,
11
+ openedMessageId: null,
12
+ },
13
+ };
14
+ }
15
+ export function reduceInboxState(state, event) {
16
+ switch (event.type) {
17
+ case "MessageOpened":
18
+ return {
19
+ ...state,
20
+ inbox: {
21
+ ...state.inbox,
22
+ openedMessageId: event.messageId,
23
+ items: state.inbox.items.map((item) => item.id === event.messageId ? { ...item, opened: true } : item),
24
+ },
25
+ };
26
+ case "SearchStarted":
27
+ return {
28
+ ...state,
29
+ inbox: {
30
+ ...state.inbox,
31
+ query: event.query,
32
+ isLoading: true,
33
+ },
34
+ };
35
+ case "SearchSucceeded":
36
+ return {
37
+ ...state,
38
+ inbox: {
39
+ ...state.inbox,
40
+ query: event.query,
41
+ isLoading: false,
42
+ items: event.items,
43
+ },
44
+ };
45
+ case "TraceUpdated":
46
+ return {
47
+ ...state,
48
+ shell: {
49
+ ...state.shell,
50
+ recentTrace: event.summary,
51
+ },
52
+ };
53
+ default:
54
+ return state;
55
+ }
56
+ }
@@ -0,0 +1,10 @@
1
+ export declare const VERSION = "0.0.0";
2
+ export { createStore } from "./core/state/createStore";
3
+ export { createSnapshotBundle } from "./core/snapshot/createSnapshotBundle";
4
+ export { defineAction } from "./core/action/defineAction";
5
+ export { createActionRuntime } from "./core/action/createActionRuntime";
6
+ export { createToolBridge } from "./tool/createToolBridge";
7
+ export { useDataRef } from "./ref/useDataRef";
8
+ export { useArrayRef } from "./ref/useArrayRef";
9
+ export { renderTUI } from "./projection/tui/renderTUI";
10
+ export { createInboxApp } from "./demo/inbox/createInboxApp";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export const VERSION = "0.0.0";
2
+ export { createStore } from "./core/state/createStore";
3
+ export { createSnapshotBundle } from "./core/snapshot/createSnapshotBundle";
4
+ export { defineAction } from "./core/action/defineAction";
5
+ export { createActionRuntime } from "./core/action/createActionRuntime";
6
+ export { createToolBridge } from "./tool/createToolBridge";
7
+ export { useDataRef } from "./ref/useDataRef";
8
+ export { useArrayRef } from "./ref/useArrayRef";
9
+ export { renderTUI } from "./projection/tui/renderTUI";
10
+ export { createInboxApp } from "./demo/inbox/createInboxApp";
@@ -0,0 +1,20 @@
1
+ import type { ComponentChildren } from "preact";
2
+ type AppContextValue = {
3
+ store: {
4
+ getState(): unknown;
5
+ };
6
+ actionRuntime: {
7
+ executeAction(name: string, input: Record<string, unknown>): Promise<unknown>;
8
+ listVisibleTools(): Array<{
9
+ name: string;
10
+ description: string;
11
+ }>;
12
+ };
13
+ };
14
+ export declare function AppProvider(props: {
15
+ store: AppContextValue["store"];
16
+ actionRuntime: AppContextValue["actionRuntime"];
17
+ children: ComponentChildren;
18
+ }): import("preact").JSX.Element;
19
+ export declare function useAppContext(): AppContextValue;
20
+ export {};
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ /** @jsxImportSource preact */
3
+ import { createContext } from "preact";
4
+ import { useContext } from "preact/hooks";
5
+ const AppContext = createContext(null);
6
+ export function AppProvider(props) {
7
+ return (_jsx(AppContext.Provider, { value: { store: props.store, actionRuntime: props.actionRuntime }, children: props.children }));
8
+ }
9
+ export function useAppContext() {
10
+ const context = useContext(AppContext);
11
+ if (!context) {
12
+ throw new Error("useAppContext must be used within AppProvider");
13
+ }
14
+ return context;
15
+ }
@@ -0,0 +1,10 @@
1
+ export declare function useAppState<State>(): {
2
+ state: State;
3
+ };
4
+ export declare function useActions(): {
5
+ callAction(name: string, input: Record<string, unknown>): Promise<unknown>;
6
+ getVisibleTools(): {
7
+ name: string;
8
+ description: string;
9
+ }[];
10
+ };
@@ -0,0 +1,18 @@
1
+ import { useAppContext } from "./AppProvider";
2
+ export function useAppState() {
3
+ const { store } = useAppContext();
4
+ return {
5
+ state: store.getState(),
6
+ };
7
+ }
8
+ export function useActions() {
9
+ const { actionRuntime } = useAppContext();
10
+ return {
11
+ callAction(name, input) {
12
+ return actionRuntime.executeAction(name, input);
13
+ },
14
+ getVisibleTools() {
15
+ return actionRuntime.listVisibleTools();
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,6 @@
1
+ /** @jsxImportSource preact */
2
+ import type { ComponentChild } from "preact";
3
+ import type { ToolDefinition } from "../../core/types";
4
+ export declare function renderTUI(node: ComponentChild, options: {
5
+ visibleTools: ToolDefinition[];
6
+ }): import("../../core/types").SnapshotBundle;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ import renderToString from "preact-render-to-string";
3
+ import { createRefCollector } from "../../core/ref/ref-index";
4
+ import { createSnapshotBundle } from "../../core/snapshot/createSnapshotBundle";
5
+ import { RefProvider } from "../../ref/RefContext";
6
+ export function renderTUI(node, options) {
7
+ const collector = createRefCollector();
8
+ const tui = renderToString(_jsx(RefProvider, { registry: collector, children: node }));
9
+ return createSnapshotBundle({
10
+ tui,
11
+ refIndex: collector.snapshot(),
12
+ visibleTools: options.visibleTools,
13
+ });
14
+ }
@@ -0,0 +1,11 @@
1
+ import type { ComponentChildren } from "preact";
2
+ import type { RefIndexEntry } from "../core/types";
3
+ type RefRegistry = {
4
+ register(refId: string, entry: RefIndexEntry): void;
5
+ };
6
+ export declare function RefProvider(props: {
7
+ registry: RefRegistry;
8
+ children: ComponentChildren;
9
+ }): import("preact").JSX.Element;
10
+ export declare function useRefRegistry(): RefRegistry;
11
+ export {};
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ /** @jsxImportSource preact */
3
+ import { createContext } from "preact";
4
+ import { useContext } from "preact/hooks";
5
+ const RefContext = createContext(null);
6
+ export function RefProvider(props) {
7
+ return (_jsx(RefContext.Provider, { value: props.registry, children: props.children }));
8
+ }
9
+ export function useRefRegistry() {
10
+ const registry = useContext(RefContext);
11
+ if (!registry) {
12
+ throw new Error("useRefRegistry must be used within RefProvider");
13
+ }
14
+ return registry;
15
+ }
@@ -0,0 +1 @@
1
+ export declare function useArrayRef(type: string, data: object[], refId: string): readonly [(content: string) => string, (index: number, content: string) => string];
@@ -0,0 +1,19 @@
1
+ import { useRefRegistry } from "./RefContext";
2
+ export function useArrayRef(type, data, refId) {
3
+ const registry = useRefRegistry();
4
+ const listRef = (content) => {
5
+ registry.register(refId, {
6
+ type: `${type}[]`,
7
+ value: data,
8
+ });
9
+ return `(${content})[${type}[]:${refId}]`;
10
+ };
11
+ const itemRef = (index, content) => {
12
+ registry.register(`${refId}[${index}]`, {
13
+ type,
14
+ value: data[index],
15
+ });
16
+ return `(${content})[${type}:${refId}[${index}]]`;
17
+ };
18
+ return [listRef, itemRef];
19
+ }
@@ -0,0 +1 @@
1
+ export declare function useDataRef(type: string, data: object, refId: string): (content: string) => string;
@@ -0,0 +1,8 @@
1
+ import { useRefRegistry } from "./RefContext";
2
+ export function useDataRef(type, data, refId) {
3
+ const registry = useRefRegistry();
4
+ return (content) => {
5
+ registry.register(refId, { type, value: data });
6
+ return `(${content})[${type}:${refId}]`;
7
+ };
8
+ }
@@ -0,0 +1,18 @@
1
+ import type { SnapshotBundle, ActionResult } from "../core/types";
2
+ export declare function createToolBridge(config: {
3
+ actionRuntime: {
4
+ listVisibleTools(): Array<{
5
+ name: string;
6
+ description: string;
7
+ }>;
8
+ executeAction(name: string, input: Record<string, unknown>): Promise<ActionResult>;
9
+ };
10
+ renderCurrentSnapshot(): SnapshotBundle;
11
+ }): {
12
+ listTools(): {
13
+ name: string;
14
+ description: string;
15
+ }[];
16
+ getSnapshotBundle(): SnapshotBundle;
17
+ executeTool(name: string, input: Record<string, unknown>, snapshotId: string): Promise<ActionResult>;
18
+ };
@@ -0,0 +1,57 @@
1
+ function resolveRefArgs(input, refIndex) {
2
+ const resolved = {};
3
+ for (const [key, value] of Object.entries(input)) {
4
+ if (typeof value === "string" && value in refIndex) {
5
+ resolved[key] = refIndex[value].value;
6
+ continue;
7
+ }
8
+ if (typeof value === "string" &&
9
+ (value.includes("[") || value.includes("]"))) {
10
+ return {
11
+ success: false,
12
+ refId: value,
13
+ };
14
+ }
15
+ resolved[key] = value;
16
+ }
17
+ return {
18
+ success: true,
19
+ data: resolved,
20
+ };
21
+ }
22
+ export function createToolBridge(config) {
23
+ const snapshots = new Map();
24
+ return {
25
+ listTools() {
26
+ return config.actionRuntime.listVisibleTools();
27
+ },
28
+ getSnapshotBundle() {
29
+ const snapshot = config.renderCurrentSnapshot();
30
+ snapshots.set(snapshot.snapshotId, snapshot);
31
+ return snapshot;
32
+ },
33
+ async executeTool(name, input, snapshotId) {
34
+ const snapshot = snapshots.get(snapshotId);
35
+ if (!snapshot) {
36
+ return {
37
+ success: false,
38
+ error: {
39
+ code: "SNAPSHOT_NOT_FOUND",
40
+ message: `Snapshot ${snapshotId} was not found`,
41
+ },
42
+ };
43
+ }
44
+ const resolved = resolveRefArgs(input, snapshot.refIndex);
45
+ if (!resolved.success) {
46
+ return {
47
+ success: false,
48
+ error: {
49
+ code: "REF_NOT_FOUND",
50
+ message: `Reference ${resolved.refId} was not found in snapshot ${snapshotId}`,
51
+ },
52
+ };
53
+ }
54
+ return config.actionRuntime.executeAction(name, resolved.data);
55
+ },
56
+ };
57
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@aotui/mobile-ai-native",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Alpha host-agnostic core for building Agent Native mobile apps with shared GUI/TUI state and snapshot-scoped tool execution.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "clean": "rm -rf dist",
17
+ "build": "pnpm run clean && tsc -p tsconfig.json",
18
+ "test": "vitest",
19
+ "test:run": "vitest --run",
20
+ "prepublishOnly": "pnpm build"
21
+ },
22
+ "dependencies": {
23
+ "preact": "^10.28.1",
24
+ "preact-render-to-string": "^6.6.6",
25
+ "zod": "^4.3.6"
26
+ },
27
+ "devDependencies": {
28
+ "happy-dom": "^20.5.3",
29
+ "typescript": "^5.9.3",
30
+ "vitest": "^3.2.4"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "GUIDE.md"
36
+ ],
37
+ "keywords": [
38
+ "agent-native",
39
+ "ios",
40
+ "mobile",
41
+ "llm",
42
+ "tui",
43
+ "state-machine",
44
+ "preact"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }