@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.
- package/GUIDE.md +19 -9
- package/README.md +11 -9
- package/dist/core/action/createActionRuntime.d.ts +7 -2
- package/dist/core/action/createActionRuntime.js +46 -27
- package/dist/core/action/defineAction.d.ts +5 -2
- package/dist/core/action/defineViewTypeTool.d.ts +12 -1
- package/dist/core/action/defineViewTypeTool.js +4 -1
- package/dist/core/action/materializeToolSurface.d.ts +10 -0
- package/dist/core/action/materializeToolSurface.js +25 -0
- package/dist/core/snapshot/createSnapshotBundle.d.ts +3 -2
- package/dist/core/snapshot/createSnapshotBundle.js +5 -67
- package/dist/core/tool/hardenToolSurface.d.ts +4 -0
- package/dist/core/tool/hardenToolSurface.js +85 -0
- package/dist/core/types.d.ts +18 -2
- package/dist/demo/inbox/InboxTUI.d.ts +2 -2
- package/dist/demo/inbox/InboxTUI.js +25 -3
- package/dist/demo/inbox/actions.d.ts +2 -2
- package/dist/demo/inbox/actions.js +50 -11
- package/dist/demo/inbox/createInboxApp.d.ts +1 -1
- package/dist/demo/inbox/createInboxApp.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/projection/gui/AppProvider.d.ts +3 -2
- package/dist/projection/gui/AppProvider.js +24 -5
- package/dist/projection/gui/hooks.d.ts +3 -2
- package/dist/projection/react/createReactAppRuntime.d.ts +6 -4
- package/dist/projection/react/createReactAppRuntime.js +24 -15
- package/dist/projection/react/hooks.d.ts +3 -2
- package/dist/projection/tui/createSnapshotAssembler.js +2 -1
- package/dist/projection/tui/renderTUI.d.ts +3 -2
- package/dist/projection/tui/renderTUI.js +2 -1
- package/dist/tool/createToolBridge.d.ts +4 -3
- package/dist/tool/createToolBridge.js +11 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/LICENSE +0 -201
package/GUIDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# GUIDE: Building an Agent Native
|
|
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
|
|
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
|
|
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
|
-
- `
|
|
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
|
|
140
|
+
The tool surface should be stable by default.
|
|
133
141
|
|
|
134
|
-
- register tools
|
|
135
|
-
-
|
|
136
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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
|
-
- `
|
|
57
|
+
- `tools`
|
|
58
|
+
- `toolAvailability`
|
|
57
59
|
|
|
58
|
-
Today the runtime hard-validates `markup` against `views`, then freezes the associated `tui`, `refIndex`, and `
|
|
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`,
|
|
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
|
-
`
|
|
90
|
+
`tools = stable tools + currently exposed dynamic tools`
|
|
89
91
|
|
|
90
|
-
|
|
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 `
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
32
|
-
!action.visibility(config.store.getState())) {
|
|
45
|
+
if (!isActionExposed(action)) {
|
|
33
46
|
return {
|
|
34
47
|
success: false,
|
|
35
48
|
error: {
|
|
36
|
-
code: "
|
|
37
|
-
message: `Action ${name} is not currently
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>):
|
|
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
|
+
};
|
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
97
|
+
tools,
|
|
98
|
+
toolAvailability,
|
|
77
99
|
});
|
|
78
100
|
}
|