@aotui/mobile-ai-native 0.1.0-alpha.2 → 0.1.0-alpha.3

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 (36) hide show
  1. package/GUIDE.md +19 -9
  2. package/README.md +11 -9
  3. package/dist/core/action/createActionRuntime.d.ts +7 -2
  4. package/dist/core/action/createActionRuntime.js +46 -27
  5. package/dist/core/action/defineAction.d.ts +5 -2
  6. package/dist/core/action/defineViewTypeTool.d.ts +12 -1
  7. package/dist/core/action/defineViewTypeTool.js +4 -1
  8. package/dist/core/action/materializeToolSurface.d.ts +10 -0
  9. package/dist/core/action/materializeToolSurface.js +25 -0
  10. package/dist/core/snapshot/createSnapshotBundle.d.ts +3 -2
  11. package/dist/core/snapshot/createSnapshotBundle.js +5 -67
  12. package/dist/core/tool/hardenToolSurface.d.ts +4 -0
  13. package/dist/core/tool/hardenToolSurface.js +85 -0
  14. package/dist/core/types.d.ts +18 -2
  15. package/dist/demo/inbox/InboxTUI.d.ts +2 -2
  16. package/dist/demo/inbox/InboxTUI.js +25 -3
  17. package/dist/demo/inbox/actions.d.ts +2 -2
  18. package/dist/demo/inbox/actions.js +50 -11
  19. package/dist/demo/inbox/createInboxApp.d.ts +1 -1
  20. package/dist/demo/inbox/createInboxApp.js +2 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/projection/gui/AppProvider.d.ts +3 -2
  23. package/dist/projection/gui/AppProvider.js +24 -5
  24. package/dist/projection/gui/hooks.d.ts +3 -2
  25. package/dist/projection/react/createReactAppRuntime.d.ts +6 -4
  26. package/dist/projection/react/createReactAppRuntime.js +24 -15
  27. package/dist/projection/react/hooks.d.ts +3 -2
  28. package/dist/projection/tui/createSnapshotAssembler.js +2 -1
  29. package/dist/projection/tui/renderTUI.d.ts +3 -2
  30. package/dist/projection/tui/renderTUI.js +2 -1
  31. package/dist/tool/createToolBridge.d.ts +4 -3
  32. package/dist/tool/createToolBridge.js +11 -3
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +1 -1
  36. package/LICENSE +0 -201
package/GUIDE.md CHANGED
@@ -1,6 +1,6 @@
1
- # GUIDE: Building an Agent Native iOS Calendar App
1
+ # GUIDE: Building an Agent Native Mobile App
2
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`.
3
+ This guide is for a developer who wants to build a high-quality agent-native mobile app using `@aotui/mobile-ai-native`.
4
4
 
5
5
  Important boundary:
6
6
 
@@ -10,7 +10,7 @@ Important boundary:
10
10
  The goal is simple:
11
11
 
12
12
  - humans use a normal GUI calendar
13
- - the LLM uses tools through a snapshot
13
+ - the LLM uses a stable tool manifest through a snapshot
14
14
  - both operate on the same app state
15
15
  - the human can see the result of the LLM's actions
16
16
 
@@ -48,7 +48,15 @@ In the current runtime, the snapshot is assembled from ordered `<View>` fragment
48
48
  - `markup` is the composed xml+markdown snapshot
49
49
  - `views` preserves the fragment order
50
50
  - `refIndex` resolves semantic refs by exact key lookup
51
- - `visibleTools` is the tool list for that same render tick
51
+ - `tools` is the stable tool manifest for that same render tick
52
+ - `toolAvailability` tells the model which tools are currently executable
53
+ - `preconditions` describe why a tool exists and what state it assumes
54
+
55
+ The important split is:
56
+
57
+ - `tools` stay stable across ticks unless the app protocol itself changes
58
+ - `toolAvailability` can change as state changes
59
+ - `PRECONDITION_NOT_MET` is the structured failure when a tool exists but cannot run yet
52
60
 
53
61
  `snapshotId` is non-negotiable.
54
62
  If the app changes after the LLM reads a snapshot, the tool call must still be tied to the exact snapshot the model saw.
@@ -129,13 +137,14 @@ Model the app as a static root plus mounted runtime views:
129
137
  - `EventDetailView` can mount only when an event is selected
130
138
  - `SearchResultView` can mount only while search results are present
131
139
 
132
- The tool surface should follow the same semantic grouping.
140
+ The tool surface should be stable by default.
133
141
 
134
- - register tools against a `viewType`
135
- - filter them with `visibility(state)`
136
- - only expose tools for currently relevant view types
142
+ - register protocol tools with `exposure: "static"`
143
+ - use `getAvailability(state)` to report whether the tool can run now
144
+ - use `preconditions` to explain the contract in human-readable form
145
+ - reserve `exposure: "dynamic"` for the few tools that are truly view-scoped
137
146
 
138
- That keeps the calendar agent surface aligned with the current screen reality without pretending the runtime is a desktop tree inspector.
147
+ That keeps the calendar agent surface aligned with the current screen reality without forcing every temporary GUI state into the visible tool list.
139
148
 
140
149
  ## 7. Why Refs Matter In Calendar Apps
141
150
 
@@ -215,6 +224,7 @@ The React Native app should do only host work:
215
224
  - host the agent session
216
225
  - ask the framework for `SnapshotBundle`
217
226
  - pass tool calls into `executeTool`
227
+ - read `toolAvailability` when it wants to surface current precondition state in the UI
218
228
  - subscribe to state and trace through the runtime hooks instead of copying framework state into local component state
219
229
 
220
230
  In practice, that means mounting the core through `createReactNativeAppRuntime()` and `AppRuntimeProvider` from `@aotui/mobile-ai-native-react-native`.
package/README.md CHANGED
@@ -16,7 +16,7 @@ The current snapshot model is view-based:
16
16
  - one shared state system drives both GUI and TUI
17
17
  - the root view is static navigation knowledge, not runtime state
18
18
  - business views are mounted from current state and represent runtime reality
19
- - tools are scoped by `viewType` and then filtered by `visibility(state)`
19
+ - tools are exposed as a stable manifest, with runtime availability tracked separately
20
20
  - the framework builds one atomic `SnapshotBundle`
21
21
  - snapshot markup is composed from ordered `<View>` fragments
22
22
  - tools execute against the exact `snapshotId` the LLM saw
@@ -36,7 +36,8 @@ type SnapshotBundle = {
36
36
  views: readonly ViewFragment[];
37
37
  tui: string;
38
38
  refIndex: Record<string, RefIndexEntry>;
39
- visibleTools: readonly ToolDefinition[];
39
+ tools: readonly ToolDefinition[];
40
+ toolAvailability: Record<string, ToolAvailability>;
40
41
  };
41
42
  ```
42
43
 
@@ -45,7 +46,7 @@ Current behavior to keep in mind:
45
46
  - `markup` is the composed xml+markdown snapshot built from ordered `<View>` fragments
46
47
  - `views` preserves the ordered fragment list, with the `Root` fragment first
47
48
  - `tui` is retained as a compatibility readout and may differ from `markup`, but it should be produced from the same snapshot generation pass
48
- - `refIndex` and `visibleTools` are produced and frozen alongside snapshot creation
49
+ - `refIndex`, `tools`, and `toolAvailability` are produced and frozen alongside snapshot creation
49
50
 
50
51
  The bundle is intended to be atomic:
51
52
 
@@ -53,9 +54,10 @@ The bundle is intended to be atomic:
53
54
  - `views`
54
55
  - `tui`
55
56
  - `refIndex`
56
- - `visibleTools`
57
+ - `tools`
58
+ - `toolAvailability`
57
59
 
58
- Today the runtime hard-validates `markup` against `views`, then freezes the associated `tui`, `refIndex`, and `visibleTools` outputs generated on that same snapshot path.
60
+ Today the runtime hard-validates `markup` against `views`, then freezes the associated `tui`, `refIndex`, `tools`, and `toolAvailability` outputs generated on that same snapshot path.
59
61
 
60
62
  ## RootView And Mounted Views
61
63
 
@@ -81,13 +83,13 @@ That means the LLM should read the static root first, then read the mounted busi
81
83
 
82
84
  ## Tool Model
83
85
 
84
- Tools are defined against a semantic `viewType`, then filtered by current state.
86
+ Tools are defined against a semantic `viewType`, exposure mode, and runtime availability.
85
87
 
86
88
  That gives the runtime this rule:
87
89
 
88
- `visibleTools = tools for currently relevant viewTypes + visibility(state)`
90
+ `tools = stable tools + currently exposed dynamic tools`
89
91
 
90
- So a tool can exist in the app, be mounted to a view type, and still be hidden when the current state does not allow it.
92
+ Then `toolAvailability[name]` tells the model whether a listed tool can execute right now. A tool can remain in the manifest and still fail with `PRECONDITION_NOT_MET` when current state does not satisfy its runtime guard.
91
93
 
92
94
  ## Core Contract
93
95
 
@@ -177,4 +179,4 @@ Keep the framework and app responsibilities separated:
177
179
  - business apps own state shape, domain actions, handwritten TUI, and app-specific effect behavior
178
180
  - actions can read state, emit events, run effects, and update trace summaries
179
181
  - effects can read state, emit events, and report structured success or failure, but they do not write state directly
180
- - snapshot coherence is enforced for the textual view representation, so `markup`, `views`, and `tui` must agree for the same render tick; `refIndex` and `visibleTools` are built and frozen alongside that snapshot path but are not yet cross-validated field-by-field
182
+ - snapshot coherence is enforced for the textual view representation, so `markup`, `views`, and `tui` must agree for the same render tick; `refIndex`, `tools`, and `toolAvailability` are built and frozen alongside that snapshot path but are not yet cross-validated field-by-field
@@ -1,6 +1,6 @@
1
1
  import type { ActionDefinition } from "./defineAction.js";
2
2
  import type { EffectMap } from "../effect/types.js";
3
- import type { ActionResult, Store, ToolDefinition, TraceStore } from "../types.js";
3
+ import type { ActionResult, Store, ToolAvailability, TraceStore } from "../types.js";
4
4
  export declare function createActionRuntime<State, Event>(config: {
5
5
  store: Store<State, Event>;
6
6
  actions: Array<ActionDefinition<State, Event, any>>;
@@ -9,5 +9,10 @@ export declare function createActionRuntime<State, Event>(config: {
9
9
  getRelevantViewTypes?: () => readonly string[];
10
10
  }): {
11
11
  executeAction: (name: string, input: Record<string, unknown>) => Promise<ActionResult>;
12
- listVisibleTools: (relevantViewTypes?: readonly string[]) => ToolDefinition[];
12
+ listTools: () => readonly import("../types").ToolDefinition[];
13
+ getToolAvailability: () => Readonly<Record<string, ToolAvailability>>;
14
+ getToolSurface: () => {
15
+ tools: readonly import("../types").ToolDefinition[];
16
+ toolAvailability: Readonly<Record<string, ToolAvailability>>;
17
+ };
13
18
  };
@@ -1,8 +1,10 @@
1
+ import { materializeToolSurface } from "./materializeToolSurface.js";
1
2
  import { createTraceStore } from "../trace/createTraceStore.js";
2
3
  export function createActionRuntime(config) {
3
4
  const actionsByName = new Map(config.actions.map((action) => [action.name, action]));
4
5
  const traceStore = config.traceStore ?? createTraceStore();
5
- function isActionRelevant(action) {
6
+ const AVAILABLE = Object.freeze({ ok: true });
7
+ function isActionViewTypeRelevant(action) {
6
8
  const scopedAction = action;
7
9
  const relevantViewTypes = config.getRelevantViewTypes?.() ?? [];
8
10
  if (!scopedAction.viewType) {
@@ -10,6 +12,18 @@ export function createActionRuntime(config) {
10
12
  }
11
13
  return relevantViewTypes.includes(scopedAction.viewType);
12
14
  }
15
+ function isActionExposed(action) {
16
+ if (action.exposure !== "dynamic") {
17
+ return true;
18
+ }
19
+ if (!isActionViewTypeRelevant(action)) {
20
+ return false;
21
+ }
22
+ return action.isExposed?.(config.store.getState()) ?? true;
23
+ }
24
+ function getActionAvailability(action) {
25
+ return action.getAvailability?.(config.store.getState()) ?? AVAILABLE;
26
+ }
13
27
  function recordTrace(actionName, status, summary) {
14
28
  traceStore.record({
15
29
  actionName,
@@ -28,13 +42,12 @@ export function createActionRuntime(config) {
28
42
  },
29
43
  };
30
44
  }
31
- if (!isActionRelevant(action) ||
32
- !action.visibility(config.store.getState())) {
45
+ if (!isActionExposed(action)) {
33
46
  return {
34
47
  success: false,
35
48
  error: {
36
- code: "ACTION_NOT_VISIBLE",
37
- message: `Action ${name} is not currently visible`,
49
+ code: "ACTION_NOT_EXPOSED",
50
+ message: `Action ${name} is not currently exposed`,
38
51
  },
39
52
  };
40
53
  }
@@ -48,6 +61,20 @@ export function createActionRuntime(config) {
48
61
  },
49
62
  };
50
63
  }
64
+ const availability = getActionAvailability(action);
65
+ if (!availability.ok) {
66
+ return {
67
+ success: false,
68
+ error: {
69
+ code: availability.code,
70
+ message: availability.message,
71
+ ...(availability.reason ? { reason: availability.reason } : {}),
72
+ ...(availability.requiredState
73
+ ? { requiredState: availability.requiredState }
74
+ : {}),
75
+ },
76
+ };
77
+ }
51
78
  let latestSummary = `Started action ${name}`;
52
79
  recordTrace(name, "started", latestSummary);
53
80
  const trace = {
@@ -92,31 +119,23 @@ export function createActionRuntime(config) {
92
119
  throw error;
93
120
  }
94
121
  }
95
- function listVisibleTools(relevantViewTypes = config.getRelevantViewTypes?.() ?? []) {
96
- const state = config.store.getState();
97
- const relevantViewTypeSet = new Set(relevantViewTypes);
98
- return config.actions
99
- .filter((action) => {
100
- const scopedAction = action;
101
- const isViewTypeRelevant = !scopedAction.viewType ||
102
- relevantViewTypeSet.has(scopedAction.viewType);
103
- return isViewTypeRelevant && action.visibility(state);
104
- })
105
- .map((action) => {
106
- const scopedAction = action;
107
- return {
108
- name: action.name,
109
- description: action.description,
110
- inputSchema: action.schema,
111
- meta: action.meta ?? {},
112
- ...(scopedAction.viewType
113
- ? { viewType: scopedAction.viewType }
114
- : {}),
115
- };
122
+ function getToolSurface() {
123
+ return materializeToolSurface({
124
+ actions: config.actions,
125
+ isActionExposed,
126
+ getActionAvailability,
116
127
  });
117
128
  }
129
+ function listTools() {
130
+ return getToolSurface().tools;
131
+ }
132
+ function getToolAvailability() {
133
+ return getToolSurface().toolAvailability;
134
+ }
118
135
  return {
119
136
  executeAction,
120
- listVisibleTools,
137
+ listTools,
138
+ getToolAvailability,
139
+ getToolSurface,
121
140
  };
122
141
  }
@@ -1,6 +1,6 @@
1
1
  import type { ZodType } from "zod";
2
2
  import type { EffectResult } from "../effect/types.js";
3
- import type { ActionResult, ToolMetadata } from "../types.js";
3
+ import type { ActionResult, ToolAvailability, ToolExposure, ToolMetadata } from "../types.js";
4
4
  export type ActionContext<State, Event> = {
5
5
  getState(): State;
6
6
  emit(event: Event): void;
@@ -14,7 +14,10 @@ export type ActionDefinition<State, Event, Input> = {
14
14
  description: string;
15
15
  schema: ZodType<Input>;
16
16
  meta?: ToolMetadata;
17
- visibility(state: State): boolean;
17
+ exposure?: ToolExposure;
18
+ preconditions?: readonly string[];
19
+ isExposed?(state: State): boolean;
20
+ getAvailability?(state: State): ToolAvailability;
18
21
  handler(ctx: ActionContext<State, Event>, input: Input): Promise<ActionResult> | ActionResult;
19
22
  };
20
23
  export declare function defineAction<State, Event, Input>(config: ActionDefinition<State, Event, Input>): ActionDefinition<State, Event, Input>;
@@ -2,4 +2,15 @@ import type { ActionDefinition } from "./defineAction.js";
2
2
  export type ViewTypeToolActionDefinition<State, Event, Input> = ActionDefinition<State, Event, Input> & {
3
3
  readonly viewType: string;
4
4
  };
5
- export declare function defineViewTypeTool<State, Event, Input>(tool: ViewTypeToolActionDefinition<State, Event, Input>): ViewTypeToolActionDefinition<State, Event, Input>;
5
+ export declare function defineViewTypeTool<State, Event, Input>(tool: ViewTypeToolActionDefinition<State, Event, Input>): {
6
+ name: string;
7
+ description: string;
8
+ schema: import("zod").ZodType<Input, unknown, import("zod/v4/core").$ZodTypeInternals<Input, unknown>>;
9
+ meta?: import("../types").ToolMetadata;
10
+ exposure: string;
11
+ preconditions?: readonly string[];
12
+ isExposed?(state: State): boolean;
13
+ getAvailability?(state: State): import("../types").ToolAvailability;
14
+ handler(ctx: import("./defineAction").ActionContext<State, Event>, input: Input): Promise<import("../types").ActionResult> | import("../types").ActionResult;
15
+ viewType: string;
16
+ };
@@ -1,3 +1,6 @@
1
1
  export function defineViewTypeTool(tool) {
2
- return tool;
2
+ return {
3
+ exposure: "dynamic",
4
+ ...tool,
5
+ };
3
6
  }
@@ -0,0 +1,10 @@
1
+ import type { ActionDefinition } from "./defineAction.js";
2
+ import type { ToolAvailability, ToolDefinition } from "../types.js";
3
+ export declare function materializeToolSurface<State, Event>(config: {
4
+ actions: Array<ActionDefinition<State, Event, any>>;
5
+ isActionExposed: (action: ActionDefinition<State, Event, any>) => boolean;
6
+ getActionAvailability: (action: ActionDefinition<State, Event, any>) => ToolAvailability;
7
+ }): {
8
+ tools: readonly ToolDefinition[];
9
+ toolAvailability: Readonly<Record<string, ToolAvailability>>;
10
+ };
@@ -0,0 +1,25 @@
1
+ import { hardenToolAvailability, hardenTools, } from "../tool/hardenToolSurface.js";
2
+ export function materializeToolSurface(config) {
3
+ const tools = [];
4
+ const toolAvailability = Object.create(null);
5
+ for (const action of config.actions) {
6
+ if (!config.isActionExposed(action)) {
7
+ continue;
8
+ }
9
+ const scopedAction = action;
10
+ tools.push({
11
+ name: action.name,
12
+ description: action.description,
13
+ inputSchema: action.schema,
14
+ meta: action.meta ?? {},
15
+ exposure: action.exposure ?? "static",
16
+ preconditions: action.preconditions ?? [],
17
+ ...(scopedAction.viewType ? { viewType: scopedAction.viewType } : {}),
18
+ });
19
+ toolAvailability[action.name] = config.getActionAvailability(action);
20
+ }
21
+ return {
22
+ tools: hardenTools(tools),
23
+ toolAvailability: hardenToolAvailability(toolAvailability),
24
+ };
25
+ }
@@ -1,9 +1,10 @@
1
- import type { SnapshotBundle, ToolDefinition, RefIndexEntry, ViewFragment } from "../types.js";
1
+ import type { SnapshotBundle, RefIndexEntry, ToolAvailability, ToolDefinition, ViewFragment } from "../types.js";
2
2
  export declare function hardenSnapshotBundle(snapshot: SnapshotBundle): SnapshotBundle;
3
3
  export declare function createSnapshotBundle(input: {
4
4
  markup?: string;
5
5
  views?: readonly ViewFragment[];
6
6
  tui?: string;
7
7
  refIndex: Readonly<Record<string, RefIndexEntry>>;
8
- visibleTools: readonly ToolDefinition[];
8
+ tools: readonly ToolDefinition[];
9
+ toolAvailability: Readonly<Record<string, ToolAvailability>>;
9
10
  }): SnapshotBundle;
@@ -1,59 +1,5 @@
1
+ import { cloneReadonlyValue, hardenToolAvailability, hardenTools, } from "../tool/hardenToolSurface.js";
1
2
  let snapshotCounter = 0;
2
- function isPlainObject(value) {
3
- if (typeof value !== "object" || value === null) {
4
- return false;
5
- }
6
- const prototype = Object.getPrototypeOf(value);
7
- return prototype === Object.prototype || prototype === null;
8
- }
9
- function cloneReadonlyValue(value, seen = new WeakMap()) {
10
- if (value === null) {
11
- return null;
12
- }
13
- const valueType = typeof value;
14
- if (valueType === "string" ||
15
- valueType === "boolean") {
16
- return value;
17
- }
18
- if (valueType === "number") {
19
- if (!Number.isFinite(value)) {
20
- throw new Error("Snapshot values must be plain JSON-like data; received a non-finite number.");
21
- }
22
- return value;
23
- }
24
- if (valueType === "undefined" ||
25
- valueType === "function" ||
26
- valueType === "symbol" ||
27
- valueType === "bigint") {
28
- throw new Error("Snapshot values must be plain JSON-like data; received an unsupported primitive value.");
29
- }
30
- if (Array.isArray(value)) {
31
- if (seen.has(value)) {
32
- return seen.get(value);
33
- }
34
- const clone = [];
35
- seen.set(value, clone);
36
- for (const item of value) {
37
- clone.push(cloneReadonlyValue(item, seen));
38
- }
39
- return Object.freeze(clone);
40
- }
41
- if (isPlainObject(value)) {
42
- if (seen.has(value)) {
43
- return seen.get(value);
44
- }
45
- const clone = Object.create(null);
46
- seen.set(value, clone);
47
- for (const [key, nestedValue] of Object.entries(value)) {
48
- clone[key] = cloneReadonlyValue(nestedValue, seen);
49
- }
50
- return Object.freeze(clone);
51
- }
52
- if (typeof value === "object" && value !== null) {
53
- throw new Error("Snapshot values must be plain JSON-like data; received a non-plain object.");
54
- }
55
- return value;
56
- }
57
3
  function hardenRefIndex(refIndex) {
58
4
  const clonedEntries = Object.create(null);
59
5
  for (const [refId, entry] of Object.entries(refIndex)) {
@@ -64,16 +10,6 @@ function hardenRefIndex(refIndex) {
64
10
  }
65
11
  return Object.freeze(clonedEntries);
66
12
  }
67
- function hardenVisibleTools(visibleTools) {
68
- const clonedTools = visibleTools.map((tool) => Object.freeze({
69
- name: tool.name,
70
- description: tool.description,
71
- inputSchema: tool.inputSchema,
72
- meta: cloneReadonlyValue(tool.meta),
73
- ...(tool.viewType ? { viewType: tool.viewType } : {}),
74
- }));
75
- return Object.freeze(clonedTools);
76
- }
77
13
  function validateSnapshotCoherence(snapshot) {
78
14
  if (snapshot.views.length === 0) {
79
15
  return;
@@ -97,7 +33,8 @@ export function hardenSnapshotBundle(snapshot) {
97
33
  }))),
98
34
  tui: snapshot.tui,
99
35
  refIndex: hardenRefIndex(snapshot.refIndex),
100
- visibleTools: hardenVisibleTools(snapshot.visibleTools),
36
+ tools: hardenTools(snapshot.tools),
37
+ toolAvailability: hardenToolAvailability(snapshot.toolAvailability),
101
38
  });
102
39
  }
103
40
  export function createSnapshotBundle(input) {
@@ -114,6 +51,7 @@ export function createSnapshotBundle(input) {
114
51
  views,
115
52
  tui,
116
53
  refIndex: input.refIndex,
117
- visibleTools: input.visibleTools,
54
+ tools: input.tools,
55
+ toolAvailability: input.toolAvailability,
118
56
  });
119
57
  }
@@ -0,0 +1,4 @@
1
+ import type { ToolAvailability, ToolDefinition } from "../types.js";
2
+ export declare function cloneReadonlyValue(value: unknown, seen?: WeakMap<object, unknown>): unknown;
3
+ export declare function hardenTools(tools: readonly ToolDefinition[]): readonly ToolDefinition[];
4
+ export declare function hardenToolAvailability(toolAvailability: Readonly<Record<string, ToolAvailability>>): Readonly<Record<string, ToolAvailability>>;
@@ -0,0 +1,85 @@
1
+ function isPlainObject(value) {
2
+ if (typeof value !== "object" || value === null) {
3
+ return false;
4
+ }
5
+ const prototype = Object.getPrototypeOf(value);
6
+ return prototype === Object.prototype || prototype === null;
7
+ }
8
+ export function cloneReadonlyValue(value, seen = new WeakMap()) {
9
+ if (value === null) {
10
+ return null;
11
+ }
12
+ const valueType = typeof value;
13
+ if (valueType === "string" || valueType === "boolean") {
14
+ return value;
15
+ }
16
+ if (valueType === "number") {
17
+ if (!Number.isFinite(value)) {
18
+ throw new Error("Tool protocol values must be plain JSON-like data; received a non-finite number.");
19
+ }
20
+ return value;
21
+ }
22
+ if (valueType === "undefined" ||
23
+ valueType === "function" ||
24
+ valueType === "symbol" ||
25
+ valueType === "bigint") {
26
+ throw new Error("Tool protocol values must be plain JSON-like data; received an unsupported primitive value.");
27
+ }
28
+ if (Array.isArray(value)) {
29
+ if (seen.has(value)) {
30
+ return seen.get(value);
31
+ }
32
+ const clone = [];
33
+ seen.set(value, clone);
34
+ for (const item of value) {
35
+ clone.push(cloneReadonlyValue(item, seen));
36
+ }
37
+ return Object.freeze(clone);
38
+ }
39
+ if (isPlainObject(value)) {
40
+ if (seen.has(value)) {
41
+ return seen.get(value);
42
+ }
43
+ const clone = Object.create(null);
44
+ seen.set(value, clone);
45
+ for (const [key, nestedValue] of Object.entries(value)) {
46
+ clone[key] = cloneReadonlyValue(nestedValue, seen);
47
+ }
48
+ return Object.freeze(clone);
49
+ }
50
+ if (typeof value === "object" && value !== null) {
51
+ throw new Error("Tool protocol values must be plain JSON-like data; received a non-plain object.");
52
+ }
53
+ return value;
54
+ }
55
+ export function hardenTools(tools) {
56
+ const clonedTools = tools.map((tool) => Object.freeze({
57
+ name: tool.name,
58
+ description: tool.description,
59
+ inputSchema: tool.inputSchema,
60
+ meta: cloneReadonlyValue(tool.meta),
61
+ exposure: tool.exposure,
62
+ preconditions: Object.freeze([...tool.preconditions]),
63
+ ...(tool.viewType ? { viewType: tool.viewType } : {}),
64
+ }));
65
+ return Object.freeze(clonedTools);
66
+ }
67
+ export function hardenToolAvailability(toolAvailability) {
68
+ const clonedAvailability = Object.create(null);
69
+ for (const [toolName, availability] of Object.entries(toolAvailability)) {
70
+ clonedAvailability[toolName] = availability.ok
71
+ ? Object.freeze({ ok: true })
72
+ : Object.freeze({
73
+ ok: false,
74
+ code: availability.code,
75
+ message: availability.message,
76
+ ...(availability.reason ? { reason: availability.reason } : {}),
77
+ ...(availability.requiredState
78
+ ? {
79
+ requiredState: cloneReadonlyValue(availability.requiredState),
80
+ }
81
+ : {}),
82
+ });
83
+ }
84
+ return Object.freeze(clonedAvailability);
85
+ }
@@ -14,11 +14,23 @@ export type RefIndexEntry = {
14
14
  readonly value: unknown;
15
15
  };
16
16
  export type ToolMetadata = Readonly<Record<string, unknown>>;
17
+ export type ToolExposure = "static" | "dynamic";
18
+ export type ToolAvailability = {
19
+ readonly ok: true;
20
+ } | {
21
+ readonly ok: false;
22
+ readonly code: "PRECONDITION_NOT_MET";
23
+ readonly message: string;
24
+ readonly reason?: string;
25
+ readonly requiredState?: Readonly<Record<string, unknown>>;
26
+ };
17
27
  export type ToolDefinition = {
18
28
  readonly name: string;
19
29
  readonly description: string;
20
30
  readonly inputSchema: ZodTypeAny;
21
31
  readonly meta: ToolMetadata;
32
+ readonly exposure: ToolExposure;
33
+ readonly preconditions: readonly string[];
22
34
  readonly viewType?: string;
23
35
  };
24
36
  export type ViewTypeToolDefinition = ToolDefinition & {
@@ -46,7 +58,8 @@ export type SnapshotAssemblerInput<State = unknown> = {
46
58
  readonly rootView: ViewFragment;
47
59
  readonly mountedViews: readonly ViewFragment[];
48
60
  readonly refIndex: Readonly<Record<string, RefIndexEntry>>;
49
- readonly visibleTools: readonly ToolDefinition[];
61
+ readonly tools: readonly ToolDefinition[];
62
+ readonly toolAvailability: Readonly<Record<string, ToolAvailability>>;
50
63
  readonly tui?: string;
51
64
  };
52
65
  export type SnapshotBundle = {
@@ -56,7 +69,8 @@ export type SnapshotBundle = {
56
69
  readonly views: readonly ViewFragment[];
57
70
  readonly tui: string;
58
71
  readonly refIndex: Readonly<Record<string, RefIndexEntry>>;
59
- readonly visibleTools: readonly ToolDefinition[];
72
+ readonly tools: readonly ToolDefinition[];
73
+ readonly toolAvailability: Readonly<Record<string, ToolAvailability>>;
60
74
  };
61
75
  export type SnapshotStatus = "active" | "stale";
62
76
  export type SnapshotRegistryEntry = {
@@ -77,5 +91,7 @@ export type ActionResult<T = unknown> = {
77
91
  error?: {
78
92
  code: string;
79
93
  message: string;
94
+ reason?: string;
95
+ requiredState?: Readonly<Record<string, unknown>>;
80
96
  };
81
97
  };
@@ -1,3 +1,3 @@
1
- import type { ToolDefinition } from "../../core/types.js";
1
+ import type { ToolAvailability, ToolDefinition } from "../../core/types.js";
2
2
  import type { InboxState } from "./state.js";
3
- export declare function createInboxSnapshotBundle(state: InboxState, visibleTools: readonly ToolDefinition[]): import("../..").SnapshotBundle;
3
+ export declare function createInboxSnapshotBundle(state: InboxState, tools: readonly ToolDefinition[], toolAvailability: Readonly<Record<string, ToolAvailability>>): import("../..").SnapshotBundle;
@@ -8,6 +8,25 @@ import { RefProvider } from "../../ref/RefContext.js";
8
8
  import { useDataRef } from "../../ref/useDataRef.js";
9
9
  import { useArrayRef } from "../../ref/useArrayRef.js";
10
10
  import { isInboxSearchActive, } from "./state.js";
11
+ function describeToolAvailability(availability) {
12
+ if (!availability) {
13
+ return "unknown";
14
+ }
15
+ if (availability.ok) {
16
+ return "available";
17
+ }
18
+ return `${availability.code}: ${availability.message}`;
19
+ }
20
+ function renderToolManifest(tools, toolAvailability) {
21
+ return tools
22
+ .map((tool) => {
23
+ const preconditions = tool.preconditions.length
24
+ ? tool.preconditions.join("; ")
25
+ : "none";
26
+ return `<item>${tool.name}: ${tool.description} | exposure=${tool.exposure} | availability=${describeToolAvailability(toolAvailability[tool.name])} | preconditions=${preconditions}</item>`;
27
+ })
28
+ .join("");
29
+ }
11
30
  function InboxRootContent() {
12
31
  return (_jsxs(_Fragment, { children: [_jsx("text", { children: "App Navigation" }), _jsx("text", { children: "Semantic view graph" }), _jsx("item", { children: "Inbox: primary message list view." }), _jsx("item", { children: "Enter Inbox: mounted by default when the app opens." }), _jsx("item", { children: "Inbox actions: openMessage." }), _jsx("item", { children: "InboxSearch: focused search/results view for inbox queries." }), _jsx("item", { children: "Enter InboxSearch: use searchMessages when inbox search is relevant." }), _jsx("item", { children: "InboxSearch actions: searchMessages." }), _jsx("item", { children: "MessageDetail: opened message detail view." }), _jsx("item", { children: "Enter MessageDetail: use openMessage from Inbox." }), _jsx("item", { children: "MessageDetail actions: inspect the opened message state." })] }));
13
32
  }
@@ -33,14 +52,16 @@ function renderFragmentMarkup(children, collector) {
33
52
  markup: renderToString(_jsx(RefProvider, { registry: collector, children: children })),
34
53
  };
35
54
  }
36
- export function createInboxSnapshotBundle(state, visibleTools) {
55
+ export function createInboxSnapshotBundle(state, tools, toolAvailability) {
37
56
  const collector = createRefCollector();
38
57
  const rootMarkup = renderFragmentMarkup(_jsx(InboxRootContent, {}), collector);
39
58
  const rootView = renderViewFragment({
40
59
  id: "root",
41
60
  type: "Root",
42
61
  name: "Navigation",
43
- markup: rootMarkup.markup,
62
+ markup: rootMarkup.markup +
63
+ "<text>Tool manifest is stable; runtime validation decides whether a tool is currently executable.</text>" +
64
+ renderToolManifest(tools, toolAvailability),
44
65
  });
45
66
  const inboxMarkup = renderFragmentMarkup(_jsx(InboxListContent, { state: state }), collector);
46
67
  const mountedViews = [
@@ -73,6 +94,7 @@ export function createInboxSnapshotBundle(state, visibleTools) {
73
94
  rootView,
74
95
  mountedViews,
75
96
  refIndex: collector.snapshot(),
76
- visibleTools,
97
+ tools,
98
+ toolAvailability,
77
99
  });
78
100
  }