@ateam-ai-mobile/plugin-runtime 0.1.8 → 0.1.10
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/dist/{chunk-RXR6M623.mjs → chunk-SICFDA5K.mjs} +121 -15
- package/dist/chunk-SICFDA5K.mjs.map +1 -0
- package/dist/index.d.mts +114 -18
- package/dist/index.d.ts +114 -18
- package/dist/index.js +142 -70
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10 -59
- package/dist/index.mjs.map +1 -1
- package/dist/{loader-UUOPZQSI.mjs → loader-MSZ4PVTO.mjs} +2 -2
- package/package.json +2 -2
- package/src/api/plugins.ts +15 -9
- package/src/bundles/bundleManager.ts +28 -9
- package/src/bundles/bundleModuleNames.ts +52 -0
- package/src/bundles/bundleUrl.ts +41 -0
- package/src/bundles/storage.ts +52 -0
- package/src/hooks/usePlugin.ts +2 -1
- package/src/index.ts +10 -36
- package/src/sdk/CameraPreview.tsx +7 -0
- package/src/sdk/device-state.ts +20 -7
- package/src/sdk/index.ts +34 -0
- package/src/sdk/types.ts +13 -0
- package/src/sdk/voice.ts +48 -4
- package/src/tools/loader.ts +4 -2
- package/src/tools/registry.ts +2 -2
- package/dist/chunk-RXR6M623.mjs.map +0 -1
- /package/dist/{loader-UUOPZQSI.mjs.map → loader-MSZ4PVTO.mjs.map} +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE bundle URL of a plugin: the one place it is spelled.
|
|
3
|
+
*
|
|
4
|
+
* Core serves a plugin's React Native bundle at
|
|
5
|
+
* `/api/ui-plugins/<pluginId>/bundle.js`, where the id is URL-encoded because
|
|
6
|
+
* plugin ids contain ':' (`mcp:<connector>:<plugin>`). The bundle manager keys
|
|
7
|
+
* everything it holds by this string: evaluated modules, in-flight downloads and
|
|
8
|
+
* the disk index. To it, two spellings of one plugin's URL are two plugins, which
|
|
9
|
+
* means two downloads, two evaluations and two separate copies of module state.
|
|
10
|
+
*
|
|
11
|
+
* That happened (MGAP-C8). The rendered surface derived the encoded route, while
|
|
12
|
+
* the device-tool loader (6294cc3) used the manifest's `bundleUrl`, which Core
|
|
13
|
+
* fills in UNENCODED. A plugin that was on screen and also had device tools ran
|
|
14
|
+
* as two module instances, and its tools never saw its UI's state.
|
|
15
|
+
*
|
|
16
|
+
* So the URL is DERIVED from the plugin id and never read from a manifest (a
|
|
17
|
+
* connector-supplied URL is ignored, as in ea435149). Every path that builds a
|
|
18
|
+
* plugin's bundle URL, or looks one up, calls this function.
|
|
19
|
+
*/
|
|
20
|
+
export function pluginBundleUrl(pluginId: string): string {
|
|
21
|
+
return `/api/ui-plugins/${encodeURIComponent(pluginId)}/bundle.js`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Is `url` this plugin's bundle URL, however the id is spelled in it?
|
|
26
|
+
*
|
|
27
|
+
* Core's manifest writes the id unencoded (`/api/ui-plugins/mcp:c:p/bundle.js`).
|
|
28
|
+
* That is the same route as `pluginBundleUrl(pluginId)`, but it is not a key the
|
|
29
|
+
* SDK may use. The check compares decoded forms, so it asks "same route?" without
|
|
30
|
+
* parsing the URL. A URL that cannot be decoded is not a match (fail closed).
|
|
31
|
+
*/
|
|
32
|
+
export function isPluginBundleUrl(url: unknown, pluginId: string): boolean {
|
|
33
|
+
if (typeof url !== 'string') return false;
|
|
34
|
+
const canonical = pluginBundleUrl(pluginId);
|
|
35
|
+
if (url === canonical) return true;
|
|
36
|
+
try {
|
|
37
|
+
return decodeURIComponent(url) === decodeURIComponent(canonical);
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/bundles/storage.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* just slower on cold start).
|
|
7
7
|
*/
|
|
8
8
|
import type { PluginGrants } from '../pluginRuntime';
|
|
9
|
+
import { pluginBundleUrl, isPluginBundleUrl } from './bundleUrl';
|
|
9
10
|
|
|
10
11
|
export interface BundleCacheEntry {
|
|
11
12
|
localPath: string;
|
|
@@ -74,6 +75,57 @@ export function selectOwnedCacheEntries<T extends { tenant?: string }>(
|
|
|
74
75
|
return (entries || []).filter((e) => !!e && e.tenant === tenant);
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Read the disk index with every entry filed under its plugin's ONE bundle URL
|
|
80
|
+
* (pluginBundleUrl). The SDK reads the index only through this function.
|
|
81
|
+
*
|
|
82
|
+
* Before MGAP-C8, the device-tool loader recorded entries under the manifest's
|
|
83
|
+
* UNENCODED spelling (`/api/ui-plugins/mcp:c:p/bundle.js`), while the rendered
|
|
84
|
+
* surface used the encoded one. A device that ran both can therefore hold one
|
|
85
|
+
* plugin under two keys. Every load now asks for the canonical URL, so an entry
|
|
86
|
+
* stored under the other spelling would never be read again.
|
|
87
|
+
*
|
|
88
|
+
* The decision is to RECOVER such an entry rather than discard it, but only when
|
|
89
|
+
* it provably is the same bundle. An entry moves to the canonical key only when
|
|
90
|
+
* both of these hold:
|
|
91
|
+
* (a) it carries the trusted declaration that names its plugin. That record
|
|
92
|
+
* comes from the manifest, never from the bundle.
|
|
93
|
+
* (b) its URL is that plugin's route in another spelling (isPluginBundleUrl).
|
|
94
|
+
* Its tenant stamp, hash and trusted declaration move with it unchanged. So its
|
|
95
|
+
* ownership (selectOwnedCacheEntries) and its integrity (the hash is re-checked
|
|
96
|
+
* at eval) stay exactly as they were. When both spellings hold an entry, the
|
|
97
|
+
* NEWER one is kept. That is the same last-writer-wins rule one key always had.
|
|
98
|
+
*
|
|
99
|
+
* Everything else stays where it is and is never adopted:
|
|
100
|
+
* - an entry with no trusted declaration (its plugin is unknown without parsing
|
|
101
|
+
* its URL);
|
|
102
|
+
* - a URL that is not the plugin's route at all, such as a connector-declared
|
|
103
|
+
* URL the old loader followed.
|
|
104
|
+
* Such an entry is simply never matched. Its plugin downloads once more under
|
|
105
|
+
* the canonical key, and the hash gate still applies.
|
|
106
|
+
*
|
|
107
|
+
* The index is mutated in place (the manager does read-modify-write on it), and
|
|
108
|
+
* the move reaches disk with the next index write.
|
|
109
|
+
*/
|
|
110
|
+
export async function loadCacheIndex(
|
|
111
|
+
storage: BundleStorage = getBundleStorage(),
|
|
112
|
+
): Promise<Record<string, BundleCacheEntry>> {
|
|
113
|
+
const index = (await storage.loadIndex()) || {};
|
|
114
|
+
for (const [key, entry] of Object.entries(index)) {
|
|
115
|
+
const pluginId = entry?.trusted?.pluginId;
|
|
116
|
+
if (!pluginId) continue;
|
|
117
|
+
const canonical = pluginBundleUrl(pluginId);
|
|
118
|
+
if (key === canonical) continue;
|
|
119
|
+
if (!isPluginBundleUrl(key, pluginId) || !isPluginBundleUrl(entry.bundleUrl, pluginId)) continue;
|
|
120
|
+
delete index[key];
|
|
121
|
+
const held = index[canonical];
|
|
122
|
+
if (!held || (entry.timestamp ?? 0) > (held.timestamp ?? 0)) {
|
|
123
|
+
index[canonical] = { ...entry, bundleUrl: canonical };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return index;
|
|
127
|
+
}
|
|
128
|
+
|
|
77
129
|
export interface BundleStorage {
|
|
78
130
|
loadIndex(): Promise<Record<string, BundleCacheEntry>>;
|
|
79
131
|
saveIndex(index: Record<string, BundleCacheEntry>): Promise<void>;
|
package/src/hooks/usePlugin.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { nativePluginRegistry } from '../host';
|
|
|
16
16
|
import { resolvePluginGrants } from '../pluginRuntime';
|
|
17
17
|
import type { NativeCapabilities, RegisteredUIPlugin } from '../sdk/types';
|
|
18
18
|
import type { DeviceBridgeDomain } from '../sdk/device-state';
|
|
19
|
+
import { pluginBundleUrl } from '../bundles/bundleUrl';
|
|
19
20
|
|
|
20
21
|
interface PluginManifestLike {
|
|
21
22
|
version?: string;
|
|
@@ -64,7 +65,7 @@ function extractBundleUrl(pluginId: string, manifest: PluginManifestLike): { url
|
|
|
64
65
|
const hasNativeComponent = !!(render.reactNative?.component || render.component);
|
|
65
66
|
if (hasNativeComponent) {
|
|
66
67
|
return {
|
|
67
|
-
url:
|
|
68
|
+
url: pluginBundleUrl(pluginId),
|
|
68
69
|
source: injected ? 'ignored-explicit' : 'derived-default',
|
|
69
70
|
...(injected && { ignored: injected }),
|
|
70
71
|
};
|
package/src/index.ts
CHANGED
|
@@ -19,43 +19,14 @@ export * from './sdk';
|
|
|
19
19
|
|
|
20
20
|
// Device Bridge v2 — the SEMANTIC layer. `props.deviceState.*`: state names,
|
|
21
21
|
// snapshots carrying freshness and confidence, and reads that say `unknown`
|
|
22
|
-
// rather than throwing.
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// glance which one they are in, and merging both into one import undoes that.
|
|
22
|
+
// rather than throwing. Its plugin-facing types and the isActionable /
|
|
23
|
+
// explainCapability helpers come through `export * from './sdk'` above — the
|
|
24
|
+
// plugin SDK is the one place they are listed, because it is the only module a
|
|
25
|
+
// bundle can require. The whole layer is also available under its own path.
|
|
27
26
|
export * as deviceStateV2 from './sdk/device-state';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
StateQuality,
|
|
32
|
-
Freshness,
|
|
33
|
-
Confidence,
|
|
34
|
-
VisualSnapshot,
|
|
35
|
-
SpatialSnapshot,
|
|
36
|
-
SpatialPoint,
|
|
37
|
-
SpatialIntent,
|
|
38
|
-
SpatialEvent,
|
|
39
|
-
SpatialStateEvent,
|
|
40
|
-
StateEvent,
|
|
41
|
-
StateListener,
|
|
42
|
-
Subscription,
|
|
43
|
-
ActivitySnapshot,
|
|
44
|
-
EnvironmentSnapshot,
|
|
45
|
-
InteractionSnapshot,
|
|
46
|
-
CapabilitySnapshot,
|
|
47
|
-
CapabilityStatus,
|
|
48
|
-
DeviceContextSnapshot,
|
|
49
|
-
ConnectivitySnapshot,
|
|
50
|
-
} from './sdk/device-state';
|
|
51
|
-
// Device Bridge v2 runtime helpers (VALUES). `neverObserved` is the canonical
|
|
52
|
-
// `unknown` freshness the engine's quality module derives from; the others were
|
|
53
|
-
// already in the built types but missing from the source root export.
|
|
54
|
-
export {
|
|
55
|
-
neverObserved,
|
|
56
|
-
isActionable,
|
|
57
|
-
explainCapability,
|
|
58
|
-
} from './sdk/device-state';
|
|
27
|
+
// The canonical `unknown` freshness the engine's quality module derives from —
|
|
28
|
+
// an ENGINE value (the host's device-state domains), not a plugin one.
|
|
29
|
+
export { neverObserved } from './sdk/device-state';
|
|
59
30
|
|
|
60
31
|
// Device Bridge v2 host DI — the semantic counterpart of
|
|
61
32
|
// setPluginNativeProvider. Separate module on purpose: the moment the two
|
|
@@ -148,6 +119,9 @@ export {
|
|
|
148
119
|
getNativeBundleUrl,
|
|
149
120
|
getNativeBundleHash,
|
|
150
121
|
} from './api/plugins';
|
|
122
|
+
// The one spelling of a plugin's bundle URL (MGAP-C8) — hosts that build or
|
|
123
|
+
// compare one use this, never their own template.
|
|
124
|
+
export { pluginBundleUrl } from './bundles/bundleUrl';
|
|
151
125
|
export type { PluginSummary, PluginSurfaceMeta } from './api/plugins';
|
|
152
126
|
export type { PluginManifest as PlatformPluginManifest } from './api/plugins';
|
|
153
127
|
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CameraPreview — the plugin-facing way to SHOW the user what the camera sees.
|
|
3
3
|
*
|
|
4
|
+
* import { CameraPreview } from '@adas/plugin-sdk';
|
|
5
|
+
* <CameraPreview native={native} /> // native = the plugin's own props.native
|
|
6
|
+
*
|
|
7
|
+
* '@adas/plugin-sdk' is the only SDK module a bundle can require. The same code
|
|
8
|
+
* is published as '@ateam-ai-mobile/plugin-runtime', but that is the host app's
|
|
9
|
+
* package: a bundle that requires it fails at evaluation, by name.
|
|
10
|
+
*
|
|
4
11
|
* WHY THIS EXISTS RATHER THAN HANDING PLUGINS `expo-camera`.
|
|
5
12
|
*
|
|
6
13
|
* The design has exactly two doors to the device: `props.native.*` (mechanical
|
package/src/sdk/device-state.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Device state — Device Bridge v2, the SEMANTIC layer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Repo path:
|
|
5
5
|
* packages/plugin-runtime/src/sdk/device-state.ts
|
|
6
|
-
*
|
|
7
|
-
* '@adas/
|
|
6
|
+
* Module specifier for plugin bundles:
|
|
7
|
+
* '@adas/plugin-sdk' — the only SDK module a bundle can require. This file
|
|
8
|
+
* once named '@adas/device-state' here (4110943); the require shim never
|
|
9
|
+
* resolved it, so a bundle that imported it got `{}` and failed later. It is
|
|
10
|
+
* now refused by name at evaluation (bundles/bundleModuleNames.ts).
|
|
8
11
|
*
|
|
9
12
|
* ─────────────────────────────────────────────────────────────────────────
|
|
10
13
|
* WHICH LAYER AM I IN?
|
|
@@ -15,7 +18,7 @@
|
|
|
15
18
|
* Returns a BARE value. THROWS when it cannot.
|
|
16
19
|
* Declared in the manifest as `capabilities: { camera: true }`
|
|
17
20
|
* (the legacy `native: {}` field is still honoured).
|
|
18
|
-
*
|
|
21
|
+
* Its types are imported from '@adas/plugin-sdk'.
|
|
19
22
|
*
|
|
20
23
|
* props.deviceState.* SEMANTIC. "What is true right now, and how well do
|
|
21
24
|
* we know it?"
|
|
@@ -24,7 +27,8 @@
|
|
|
24
27
|
* and able to say `unknown` in every field.
|
|
25
28
|
* A read NEVER throws for absence.
|
|
26
29
|
* Declared in the manifest as `stateDomains: ['visual']`.
|
|
27
|
-
*
|
|
30
|
+
* Defined in this file; its types and helpers are
|
|
31
|
+
* imported from '@adas/plugin-sdk' too.
|
|
28
32
|
*
|
|
29
33
|
* The two containers are the discriminator. Every semantic call in a plugin
|
|
30
34
|
* begins with the eight characters `deviceState.`; every mechanical call
|
|
@@ -655,11 +659,20 @@ export type HumanActivity =
|
|
|
655
659
|
| 'driving'
|
|
656
660
|
| 'unknown';
|
|
657
661
|
|
|
658
|
-
/**
|
|
662
|
+
/**
|
|
663
|
+
* FROZEN. Independent of HumanActivity — walking+pocket, driving+mounted.
|
|
664
|
+
*
|
|
665
|
+
* What is returned TODAY: `handheld`, `table` and `unknown` only. Do not write
|
|
666
|
+
* a branch that waits for `pocket` or `mounted`; it will never run. A pocketed
|
|
667
|
+
* phone shows up in the Visual domain's frame quality (`obstructed: true` /
|
|
668
|
+
* `exposure: 'dark'`), not here.
|
|
669
|
+
*/
|
|
659
670
|
export type DevicePlacement =
|
|
660
671
|
| 'handheld'
|
|
672
|
+
/** RESERVED — never returned until the proximity/light sensor is wired. */
|
|
661
673
|
| 'pocket'
|
|
662
674
|
| 'table'
|
|
675
|
+
/** RESERVED — never returned until Bluetooth/CarPlay mount context is wired. */
|
|
663
676
|
| 'mounted'
|
|
664
677
|
| 'unknown';
|
|
665
678
|
|
package/src/sdk/index.ts
CHANGED
|
@@ -54,6 +54,7 @@ export type {
|
|
|
54
54
|
DeviceToolContext,
|
|
55
55
|
DeviceToolDefinition,
|
|
56
56
|
DeviceToolFn,
|
|
57
|
+
DeviceToolFailureReason,
|
|
57
58
|
} from './types';
|
|
58
59
|
|
|
59
60
|
// Voice — the live conversational SESSION surface (props.voice).
|
|
@@ -61,6 +62,8 @@ export type {
|
|
|
61
62
|
VoiceAPI,
|
|
62
63
|
VoiceState,
|
|
63
64
|
VoiceSnapshot,
|
|
65
|
+
VoiceDeviceToolCall,
|
|
66
|
+
VoiceDeviceToolReason,
|
|
64
67
|
VoiceTranscriptEntry,
|
|
65
68
|
VoiceStartOptions,
|
|
66
69
|
VoicePersona,
|
|
@@ -73,6 +76,37 @@ export type {
|
|
|
73
76
|
// provider and its own surfaces call it as this caller. See VoiceSessionProvider.
|
|
74
77
|
export { PLATFORM_VOICE_CALLER } from './voice';
|
|
75
78
|
|
|
79
|
+
// Device state — the SEMANTIC layer: the types of what `props.deviceState`
|
|
80
|
+
// returns, and the two pure helpers for reading a snapshot. They are here, in
|
|
81
|
+
// the one module a bundle can require, because a bundle has no other SDK name
|
|
82
|
+
// ('@adas/device-state' never resolved, and is refused by name). What keeps the
|
|
83
|
+
// two layers apart is the CONTAINER a call starts with — `deviceState.` vs
|
|
84
|
+
// `device.` — not the import line.
|
|
85
|
+
export type {
|
|
86
|
+
DeviceState,
|
|
87
|
+
DeviceBridgeDomain,
|
|
88
|
+
StateQuality,
|
|
89
|
+
Freshness,
|
|
90
|
+
Confidence,
|
|
91
|
+
VisualSnapshot,
|
|
92
|
+
SpatialSnapshot,
|
|
93
|
+
SpatialPoint,
|
|
94
|
+
SpatialIntent,
|
|
95
|
+
SpatialEvent,
|
|
96
|
+
SpatialStateEvent,
|
|
97
|
+
StateEvent,
|
|
98
|
+
StateListener,
|
|
99
|
+
Subscription,
|
|
100
|
+
ActivitySnapshot,
|
|
101
|
+
EnvironmentSnapshot,
|
|
102
|
+
InteractionSnapshot,
|
|
103
|
+
CapabilitySnapshot,
|
|
104
|
+
CapabilityStatus,
|
|
105
|
+
DeviceContextSnapshot,
|
|
106
|
+
ConnectivitySnapshot,
|
|
107
|
+
} from './device-state';
|
|
108
|
+
export { isActionable, explainCapability } from './device-state';
|
|
109
|
+
|
|
76
110
|
// RN primitives (plugins import from the SDK, not from react-native)
|
|
77
111
|
export {
|
|
78
112
|
View,
|
package/src/sdk/types.ts
CHANGED
|
@@ -767,6 +767,19 @@ export interface DeviceToolContext {
|
|
|
767
767
|
|
|
768
768
|
export type DeviceToolFn = (args: any, ctx: DeviceToolContext) => Promise<any> | any;
|
|
769
769
|
|
|
770
|
+
/**
|
|
771
|
+
* Why running a device tool produced no result (`callDeviceTool`'s
|
|
772
|
+
* `{ ok: false, reason }`). The one list: the registry returns these, and the
|
|
773
|
+
* voice snapshot's `lastDeviceTool.reason` carries them unchanged.
|
|
774
|
+
* - `not_found` — no tool is registered under that name.
|
|
775
|
+
* - `no_context` — the tool's device context could not be built.
|
|
776
|
+
* - `timeout` — the tool did not finish within the call's timeout.
|
|
777
|
+
* - `failed` — the tool threw; the error is its message.
|
|
778
|
+
* A tool that RETURNS a value has succeeded as far as the platform is concerned,
|
|
779
|
+
* even if the value describes a failure — throw to report one.
|
|
780
|
+
*/
|
|
781
|
+
export type DeviceToolFailureReason = 'not_found' | 'no_context' | 'timeout' | 'failed';
|
|
782
|
+
|
|
770
783
|
export interface DeviceToolDefinition {
|
|
771
784
|
/** Domain name, as the catalog and the caller see it. e.g. "guide.observe" */
|
|
772
785
|
name: string;
|
package/src/sdk/voice.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* See docs/handoff/SPEC_voice_session_without_the_voice_screen_2026-09-12.md.
|
|
20
20
|
*/
|
|
21
|
+
import type { DeviceToolFailureReason } from './types';
|
|
21
22
|
|
|
22
23
|
/** The session lifecycle — identical vocabulary to the built-in `CallState`. */
|
|
23
24
|
export type VoiceState = 'idle' | 'connecting' | 'connected' | 'error';
|
|
@@ -29,6 +30,41 @@ export interface VoiceTranscriptEntry {
|
|
|
29
30
|
ts: number;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Why a device-tool call the session received did not produce a result.
|
|
35
|
+
* - the tool RAN and did not return: `not_found` | `no_context` | `timeout` |
|
|
36
|
+
* `failed` (it threw) — see DeviceToolFailureReason;
|
|
37
|
+
* - the phone REFUSED it before running: `not_ready` (Core's tool allowset for
|
|
38
|
+
* this session had not arrived yet — transient, a retry can succeed) or
|
|
39
|
+
* `not_authorized` (not allowed in this session — final);
|
|
40
|
+
* - `invalid`: the call itself was malformed (it named no tool, or carried no
|
|
41
|
+
* correlationId to answer on) — not run.
|
|
42
|
+
*/
|
|
43
|
+
export type VoiceDeviceToolReason = DeviceToolFailureReason | 'not_ready' | 'not_authorized' | 'invalid';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One finished device-tool call, as `VoiceSnapshot.lastDeviceTool` reports it.
|
|
47
|
+
* `name` and `at` keep their original meaning, so a reader of just those two is
|
|
48
|
+
* unaffected.
|
|
49
|
+
*/
|
|
50
|
+
export type VoiceDeviceToolCall = {
|
|
51
|
+
/** The tool the model called ('' if the call named none — see `invalid`). */
|
|
52
|
+
name: string;
|
|
53
|
+
/** Epoch ms the call REACHED the phone (not when it finished — that is at + ms). */
|
|
54
|
+
at: number;
|
|
55
|
+
/** Milliseconds from arrival to outcome (≈0 for a refusal). */
|
|
56
|
+
ms: number;
|
|
57
|
+
} & (
|
|
58
|
+
| { ok: true; reason: null; error: null }
|
|
59
|
+
| {
|
|
60
|
+
ok: false;
|
|
61
|
+
reason: VoiceDeviceToolReason;
|
|
62
|
+
/** What went wrong, clipped to 200 characters: tool errors can echo the
|
|
63
|
+
* call's arguments, which may carry what the user said. */
|
|
64
|
+
error: string;
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
32
68
|
/** A point-in-time read of the session — what a reasoning loop polls. */
|
|
33
69
|
export interface VoiceSnapshot {
|
|
34
70
|
state: VoiceState;
|
|
@@ -52,10 +88,18 @@ export interface VoiceSnapshot {
|
|
|
52
88
|
/** The last error, if `state === 'error'`; also carries a refusal reason when
|
|
53
89
|
* this surface was handed to a plugin that did not declare `voice`. */
|
|
54
90
|
lastError: string | null;
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
91
|
+
/**
|
|
92
|
+
* The most recently FINISHED device-tool call the model made through this
|
|
93
|
+
* session, with its outcome — or null if none has finished yet. Set once the
|
|
94
|
+
* outcome is known (ran, refused, malformed, threw, timed out), never on
|
|
95
|
+
* arrival, so a refused call can no longer look like one that ran.
|
|
96
|
+
*
|
|
97
|
+
* SCOPED to the caller: a plugin sees only calls to tools IT registered (never
|
|
98
|
+
* another solution's tool names or errors, even on its own call); the platform
|
|
99
|
+
* sees every call. Cleared when the session ends or a new one starts.
|
|
100
|
+
* Diagnostic — every solution's debug view gets "why did that call fail" free.
|
|
101
|
+
*/
|
|
102
|
+
lastDeviceTool: VoiceDeviceToolCall | null;
|
|
59
103
|
/** The sessionId the Core allowset is bound to. Diagnostic: authorization
|
|
60
104
|
* requires it to equal `sessionId`; a mismatch is why tools refuse. */
|
|
61
105
|
authorizedSessionId: string | null;
|
package/src/tools/loader.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { listPlugins, getPlugin, getNativeBundleUrl, getNativeBundleHash } from '../api/plugins';
|
|
14
14
|
import { bundleManager } from '../bundles/bundleManager';
|
|
15
15
|
import { listDeviceTools } from './registry';
|
|
16
|
-
import { getBundleStorage, selectOwnedCacheEntries } from '../bundles/storage';
|
|
16
|
+
import { getBundleStorage, loadCacheIndex, selectOwnedCacheEntries } from '../bundles/storage';
|
|
17
17
|
import { resolvePluginGrants } from '../pluginRuntime';
|
|
18
18
|
import { getTenant } from '@ateam-ai-mobile/core';
|
|
19
19
|
|
|
@@ -142,7 +142,9 @@ export async function ensureDeviceToolsLoaded({ force = false } = {}): Promise<s
|
|
|
142
142
|
*/
|
|
143
143
|
async function loadFromDiskCache(): Promise<string[]> {
|
|
144
144
|
try {
|
|
145
|
-
|
|
145
|
+
// Canonical keys only (loadCacheIndex): an entry an older build filed under
|
|
146
|
+
// the manifest's unencoded spelling is recovered under pluginBundleUrl.
|
|
147
|
+
const index = await loadCacheIndex(getBundleStorage());
|
|
146
148
|
const entries = Object.values(index || {});
|
|
147
149
|
if (entries.length === 0) return [];
|
|
148
150
|
|
package/src/tools/registry.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { createNativeBridge } from '../bridge/native';
|
|
16
16
|
import { getPluginNative, gatePluginNative } from '../host';
|
|
17
17
|
import { gateDeviceState, withoutSubscribe } from '../deviceStateHost';
|
|
18
|
-
import type { DeviceToolContext, DeviceToolDefinition, NativeCapabilities } from '../sdk/types';
|
|
18
|
+
import type { DeviceToolContext, DeviceToolDefinition, DeviceToolFailureReason, NativeCapabilities } from '../sdk/types';
|
|
19
19
|
import type { DeviceBridgeDomain } from '../sdk/device-state';
|
|
20
20
|
|
|
21
21
|
export interface DeviceToolSummary {
|
|
@@ -27,7 +27,7 @@ export interface DeviceToolSummary {
|
|
|
27
27
|
|
|
28
28
|
export type DeviceToolOutcome =
|
|
29
29
|
| { ok: true; result: any }
|
|
30
|
-
| { ok: false; error: string; reason:
|
|
30
|
+
| { ok: false; error: string; reason: DeviceToolFailureReason };
|
|
31
31
|
|
|
32
32
|
interface Entry {
|
|
33
33
|
pluginId: string;
|