@ateam-ai-mobile/ui 0.1.8 → 0.1.9
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/index.d.mts +39 -11
- package/dist/index.d.ts +39 -11
- package/dist/index.js +92 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +92 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/voice/voiceSession.ts +150 -64
package/dist/index.mjs
CHANGED
|
@@ -2800,6 +2800,11 @@ var buildObserveScript = (text) => `
|
|
|
2800
2800
|
}
|
|
2801
2801
|
})(${JSON.stringify(text)}); true;
|
|
2802
2802
|
`;
|
|
2803
|
+
var DEVICE_TOOL_ERROR_MAX = 200;
|
|
2804
|
+
function clipError(message) {
|
|
2805
|
+
const chars = Array.from(message);
|
|
2806
|
+
return chars.length > DEVICE_TOOL_ERROR_MAX ? chars.slice(0, DEVICE_TOOL_ERROR_MAX - 1).join("") + "\u2026" : message;
|
|
2807
|
+
}
|
|
2803
2808
|
function originOf(url) {
|
|
2804
2809
|
if (!url) return null;
|
|
2805
2810
|
const m = /^([a-z][a-z0-9+.-]*:\/\/[^/?#]+)/i.exec(url);
|
|
@@ -2815,7 +2820,7 @@ var IDLE = {
|
|
|
2815
2820
|
muted: false,
|
|
2816
2821
|
userSpeaking: false,
|
|
2817
2822
|
lastUserSpeechEndedAt: null,
|
|
2818
|
-
|
|
2823
|
+
deviceToolCalls: [],
|
|
2819
2824
|
mounted: false,
|
|
2820
2825
|
webViewSource: null,
|
|
2821
2826
|
skillSlug: void 0
|
|
@@ -2871,6 +2876,11 @@ var VoiceSessionManager = class {
|
|
|
2871
2876
|
* and the bundleManager registers a plugin's tools under that same id (never
|
|
2872
2877
|
* the bundle's own short `id`), so exact equality is the whole check. */
|
|
2873
2878
|
this.toolScope = null;
|
|
2879
|
+
/** Bumped whenever a session begins or ends. A device-tool call remembers the
|
|
2880
|
+
* epoch it arrived in, and its outcome is recorded only if that session is
|
|
2881
|
+
* still the current one — a tool that finishes after hang-up (or after a new
|
|
2882
|
+
* call started) must not show up as the new session's last call. */
|
|
2883
|
+
this.sessionEpoch = 0;
|
|
2874
2884
|
this.injectedTrackHook = INJECT_TRACK_HOOK;
|
|
2875
2885
|
}
|
|
2876
2886
|
/** The platform (built-in sheet, mic indicator, session host) sees and
|
|
@@ -2900,11 +2910,11 @@ var VoiceSessionManager = class {
|
|
|
2900
2910
|
async start(opts, caller = PLATFORM_VOICE_CALLER) {
|
|
2901
2911
|
if (this.state.callState === "connected") {
|
|
2902
2912
|
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2903
|
-
return this.realSnapshot();
|
|
2913
|
+
return this.realSnapshot(caller);
|
|
2904
2914
|
}
|
|
2905
2915
|
if (this.state.callState === "connecting") {
|
|
2906
2916
|
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2907
|
-
return this.waitForConnect();
|
|
2917
|
+
return this.waitForConnect(caller);
|
|
2908
2918
|
}
|
|
2909
2919
|
if (opts?.skills === "none" && opts?.skillSlug) {
|
|
2910
2920
|
throw new Error("voice: skills:'none' (a clean call) cannot also hand the mic to a skill (skillSlug).");
|
|
@@ -2925,16 +2935,17 @@ var VoiceSessionManager = class {
|
|
|
2925
2935
|
const after = this.getInternal().callState;
|
|
2926
2936
|
if (after === "connected") {
|
|
2927
2937
|
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2928
|
-
return this.realSnapshot();
|
|
2938
|
+
return this.realSnapshot(caller);
|
|
2929
2939
|
}
|
|
2930
2940
|
if (after === "connecting") {
|
|
2931
2941
|
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2932
|
-
return this.waitForConnect();
|
|
2942
|
+
return this.waitForConnect(caller);
|
|
2933
2943
|
}
|
|
2934
2944
|
this.owner = caller;
|
|
2935
2945
|
this.toolScope = opts?.skills === "none" && typeof caller === "string" ? caller : null;
|
|
2936
2946
|
this.frozenTools = null;
|
|
2937
2947
|
this.frozenSessionId = null;
|
|
2948
|
+
this.sessionEpoch++;
|
|
2938
2949
|
const skillSlug = opts?.skillSlug;
|
|
2939
2950
|
this.setState({
|
|
2940
2951
|
callState: "connecting",
|
|
@@ -2943,12 +2954,15 @@ var VoiceSessionManager = class {
|
|
|
2943
2954
|
aiPartial: "",
|
|
2944
2955
|
userPartial: "",
|
|
2945
2956
|
muted: false,
|
|
2957
|
+
// A session that ended from inside the voice page (not via stop()) left
|
|
2958
|
+
// its calls here; the new session starts with none.
|
|
2959
|
+
deviceToolCalls: [],
|
|
2946
2960
|
mounted: true,
|
|
2947
2961
|
skillSlug,
|
|
2948
2962
|
persona: opts?.persona,
|
|
2949
2963
|
webViewSource: { uri: this.buildUri(skillSlug, opts?.persona, opts?.skills) }
|
|
2950
2964
|
});
|
|
2951
|
-
return this.waitForConnect();
|
|
2965
|
+
return this.waitForConnect(caller);
|
|
2952
2966
|
}
|
|
2953
2967
|
stop(caller = PLATFORM_VOICE_CALLER) {
|
|
2954
2968
|
if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
|
|
@@ -2962,6 +2976,7 @@ var VoiceSessionManager = class {
|
|
|
2962
2976
|
this.rejectPending(new Error("voice: session stopped before it connected"));
|
|
2963
2977
|
this.frozenTools = null;
|
|
2964
2978
|
this.frozenSessionId = null;
|
|
2979
|
+
this.sessionEpoch++;
|
|
2965
2980
|
this.setState({
|
|
2966
2981
|
...IDLE,
|
|
2967
2982
|
// keep the transcript of the call that just ended for any reader still up
|
|
@@ -2994,9 +3009,11 @@ var VoiceSessionManager = class {
|
|
|
2994
3009
|
this.webView?.injectJavaScript?.(buildObserveScript(text));
|
|
2995
3010
|
}
|
|
2996
3011
|
getSnapshot(caller = PLATFORM_VOICE_CALLER) {
|
|
2997
|
-
return this.canAccess(caller) ? this.realSnapshot() : this.foreignSnapshot();
|
|
3012
|
+
return this.canAccess(caller) ? this.realSnapshot(caller) : this.foreignSnapshot();
|
|
2998
3013
|
}
|
|
2999
|
-
|
|
3014
|
+
/** The snapshot for a caller that may access the session. Everything is the
|
|
3015
|
+
* session's, except `lastDeviceTool`, which is scoped to the caller's tools. */
|
|
3016
|
+
realSnapshot(caller) {
|
|
3000
3017
|
const s = this.state;
|
|
3001
3018
|
return {
|
|
3002
3019
|
state: s.callState,
|
|
@@ -3006,11 +3023,23 @@ var VoiceSessionManager = class {
|
|
|
3006
3023
|
listening: !!s.userPartial,
|
|
3007
3024
|
userSpeaking: s.userSpeaking,
|
|
3008
3025
|
lastUserSpeechEndedAt: s.lastUserSpeechEndedAt,
|
|
3009
|
-
lastDeviceTool:
|
|
3026
|
+
lastDeviceTool: this.lastDeviceToolFor(caller),
|
|
3010
3027
|
authorizedSessionId: this.frozenSessionId,
|
|
3011
3028
|
lastError: s.error
|
|
3012
3029
|
};
|
|
3013
3030
|
}
|
|
3031
|
+
/**
|
|
3032
|
+
* The last finished device-tool call THIS caller may see. The platform (the
|
|
3033
|
+
* user's own surface) sees every call. A plugin sees only calls to tools it
|
|
3034
|
+
* registered — even on a session it owns, the model can call another
|
|
3035
|
+
* solution's tool (a normal call offers every registered tool), and that
|
|
3036
|
+
* tool's name and error are not this plugin's to read.
|
|
3037
|
+
*/
|
|
3038
|
+
lastDeviceToolFor(caller) {
|
|
3039
|
+
const calls = this.state.deviceToolCalls;
|
|
3040
|
+
if (caller === PLATFORM_VOICE_CALLER) return calls.length ? calls[calls.length - 1].call : null;
|
|
3041
|
+
return calls.find((r) => r.owner === caller)?.call ?? null;
|
|
3042
|
+
}
|
|
3014
3043
|
foreignSnapshot() {
|
|
3015
3044
|
return {
|
|
3016
3045
|
state: "idle",
|
|
@@ -3138,18 +3167,52 @@ var VoiceSessionManager = class {
|
|
|
3138
3167
|
console.warn(`[voiceSession] REFUSED device tool call from "${sender || "unknown origin"}" (expected "${expected}")`);
|
|
3139
3168
|
break;
|
|
3140
3169
|
}
|
|
3141
|
-
|
|
3142
|
-
if (!this.isToolAuthorized(data.name)) {
|
|
3143
|
-
const notReady = this.frozenTools === null;
|
|
3144
|
-
console.warn(`[voiceSession] REFUSED "${data.name}" \u2014 ${notReady ? "allowset not received yet" : "not authorized for this session"}`);
|
|
3145
|
-
this.postDeviceToolRefusal(data.correlationId, data.name, notReady);
|
|
3146
|
-
break;
|
|
3147
|
-
}
|
|
3148
|
-
void this.runDeviceTool(data);
|
|
3170
|
+
void this.handleDeviceToolCall(data);
|
|
3149
3171
|
break;
|
|
3150
3172
|
}
|
|
3151
3173
|
}
|
|
3152
3174
|
}
|
|
3175
|
+
/**
|
|
3176
|
+
* One device-tool call, arrival to outcome: validate → authorize → run. The
|
|
3177
|
+
* answer goes back to the mini-app (the model reads it) AND becomes
|
|
3178
|
+
* `lastDeviceTool` — the same object, recorded only once it is known, so a
|
|
3179
|
+
* refused call never looks like one that ran.
|
|
3180
|
+
*/
|
|
3181
|
+
async handleDeviceToolCall(data) {
|
|
3182
|
+
const { correlationId, name, args, timeoutMs } = data || {};
|
|
3183
|
+
const at = Date.now();
|
|
3184
|
+
const epoch = this.sessionEpoch;
|
|
3185
|
+
const toolName = typeof name === "string" ? name : "";
|
|
3186
|
+
const sessionOwner = this.owner;
|
|
3187
|
+
const owner = typeof sessionOwner === "string" && toolName && hasDeviceTool(toolName, sessionOwner) ? sessionOwner : null;
|
|
3188
|
+
let answer;
|
|
3189
|
+
if (!toolName) {
|
|
3190
|
+
answer = { ok: false, reason: "invalid", error: "The device tool call named no tool." };
|
|
3191
|
+
} else if (!correlationId) {
|
|
3192
|
+
answer = {
|
|
3193
|
+
ok: false,
|
|
3194
|
+
reason: "invalid",
|
|
3195
|
+
error: `The call to "${toolName}" carried no correlationId, so it could not be answered.`
|
|
3196
|
+
};
|
|
3197
|
+
} else if (!this.isToolAuthorized(toolName)) {
|
|
3198
|
+
const notReady = this.frozenTools === null;
|
|
3199
|
+
console.warn(`[voiceSession] REFUSED "${toolName}" \u2014 ${notReady ? "allowset not received yet" : "not authorized for this session"}`);
|
|
3200
|
+
answer = notReady ? {
|
|
3201
|
+
ok: false,
|
|
3202
|
+
reason: "not_ready",
|
|
3203
|
+
error: "The voice session is still starting up \u2014 its tool allowset has not arrived yet. Try again in a moment."
|
|
3204
|
+
} : { ok: false, reason: "not_authorized", error: `"${toolName}" is not available in this voice session.` };
|
|
3205
|
+
} else {
|
|
3206
|
+
answer = await this.runDeviceTool(toolName, args, timeoutMs);
|
|
3207
|
+
}
|
|
3208
|
+
if (correlationId) this.postDeviceToolResult({ correlationId, ...answer });
|
|
3209
|
+
if (epoch !== this.sessionEpoch) return;
|
|
3210
|
+
const ms = Math.max(0, Date.now() - at);
|
|
3211
|
+
const call = answer.ok ? { name: toolName, at, ms, ok: true, reason: null, error: null } : { name: toolName, at, ms, ok: false, reason: answer.reason, error: clipError(answer.error) };
|
|
3212
|
+
this.setState({
|
|
3213
|
+
deviceToolCalls: [...this.state.deviceToolCalls.filter((r) => r.owner !== owner), { owner, call }]
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3153
3216
|
/**
|
|
3154
3217
|
* Final authorization = Core's frozen allowset ∩ the current valid local
|
|
3155
3218
|
* registry, bound to the ACTIVE session. All must hold:
|
|
@@ -3169,21 +3232,9 @@ var VoiceSessionManager = class {
|
|
|
3169
3232
|
if (!this.frozenTools.has(name)) return false;
|
|
3170
3233
|
return hasDeviceTool(name, this.toolScope ?? void 0);
|
|
3171
3234
|
}
|
|
3172
|
-
/**
|
|
3173
|
-
*
|
|
3174
|
-
|
|
3175
|
-
postDeviceToolRefusal(correlationId, name, notReady) {
|
|
3176
|
-
const payload = notReady ? {
|
|
3177
|
-
correlationId,
|
|
3178
|
-
ok: false,
|
|
3179
|
-
reason: "not_ready",
|
|
3180
|
-
error: "The voice session is still starting up \u2014 its tool allowset has not arrived yet. Try again in a moment."
|
|
3181
|
-
} : {
|
|
3182
|
-
correlationId,
|
|
3183
|
-
ok: false,
|
|
3184
|
-
reason: "not_authorized",
|
|
3185
|
-
error: `"${name}" is not available in this voice session.`
|
|
3186
|
-
};
|
|
3235
|
+
/** Answer the mini-app's pending device-tool request (a call's outcome, or the
|
|
3236
|
+
* tool list) — it resolves the request with this payload by correlationId. */
|
|
3237
|
+
postDeviceToolResult(payload) {
|
|
3187
3238
|
this.webView?.injectJavaScript?.(
|
|
3188
3239
|
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3189
3240
|
JSON.stringify(payload)
|
|
@@ -3200,28 +3251,16 @@ var VoiceSessionManager = class {
|
|
|
3200
3251
|
description: t.description,
|
|
3201
3252
|
input_schema: t.input_schema
|
|
3202
3253
|
}));
|
|
3203
|
-
this.
|
|
3204
|
-
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3205
|
-
JSON.stringify({ correlationId, ok: true, tools })
|
|
3206
|
-
)}); true;`
|
|
3207
|
-
);
|
|
3254
|
+
this.postDeviceToolResult({ correlationId, ok: true, tools });
|
|
3208
3255
|
}
|
|
3209
|
-
/** Run
|
|
3210
|
-
async runDeviceTool(
|
|
3211
|
-
const { correlationId, name, args, timeoutMs } = data || {};
|
|
3212
|
-
if (!correlationId || typeof name !== "string") return;
|
|
3213
|
-
let payload;
|
|
3256
|
+
/** Run an authorized device tool. Never throws: every failure is an answer. */
|
|
3257
|
+
async runDeviceTool(name, args, timeoutMs) {
|
|
3214
3258
|
try {
|
|
3215
3259
|
const outcome = await callDeviceTool(name, args || {}, { timeoutMs });
|
|
3216
|
-
|
|
3260
|
+
return outcome.ok ? { ok: true, result: outcome.result } : { ok: false, reason: outcome.reason, error: outcome.error };
|
|
3217
3261
|
} catch (err) {
|
|
3218
|
-
|
|
3262
|
+
return { ok: false, reason: "failed", error: err?.message || String(err) };
|
|
3219
3263
|
}
|
|
3220
|
-
this.webView?.injectJavaScript?.(
|
|
3221
|
-
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3222
|
-
JSON.stringify(payload)
|
|
3223
|
-
)}); true;`
|
|
3224
|
-
);
|
|
3225
3264
|
}
|
|
3226
3265
|
// ── internals ────────────────────────────────────────────────
|
|
3227
3266
|
setState(partial) {
|
|
@@ -3241,22 +3280,21 @@ var VoiceSessionManager = class {
|
|
|
3241
3280
|
}
|
|
3242
3281
|
}
|
|
3243
3282
|
}
|
|
3244
|
-
waitForConnect() {
|
|
3283
|
+
waitForConnect(caller) {
|
|
3245
3284
|
return new Promise((resolve, reject) => {
|
|
3246
3285
|
const timer = setTimeout(() => {
|
|
3247
3286
|
this.pending = this.pending.filter((p) => p.timer !== timer);
|
|
3248
3287
|
reject(new Error("voice: timed out waiting to connect (is VoiceSessionHost mounted?)"));
|
|
3249
3288
|
}, CONNECT_TIMEOUT_MS);
|
|
3250
|
-
this.pending.push({ resolve, reject, timer });
|
|
3289
|
+
this.pending.push({ caller, resolve, reject, timer });
|
|
3251
3290
|
});
|
|
3252
3291
|
}
|
|
3253
3292
|
resolvePending() {
|
|
3254
|
-
const snap = this.realSnapshot();
|
|
3255
3293
|
const waiting = this.pending;
|
|
3256
3294
|
this.pending = [];
|
|
3257
3295
|
for (const p of waiting) {
|
|
3258
3296
|
clearTimeout(p.timer);
|
|
3259
|
-
p.resolve(
|
|
3297
|
+
p.resolve(this.realSnapshot(p.caller));
|
|
3260
3298
|
}
|
|
3261
3299
|
}
|
|
3262
3300
|
rejectPending(err) {
|
|
@@ -3277,6 +3315,7 @@ var VoiceSessionManager = class {
|
|
|
3277
3315
|
this.pendingStarts.clear();
|
|
3278
3316
|
this.frozenTools = null;
|
|
3279
3317
|
this.frozenSessionId = null;
|
|
3318
|
+
this.sessionEpoch++;
|
|
3280
3319
|
this.listeners.clear();
|
|
3281
3320
|
this.webView = null;
|
|
3282
3321
|
}
|