@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.d.mts
CHANGED
|
@@ -3,7 +3,7 @@ import { Message, JobProgress, UIAction, AdasSession, TenantMembership, HandoffE
|
|
|
3
3
|
import * as React$1 from 'react';
|
|
4
4
|
import React__default, { ReactNode } from 'react';
|
|
5
5
|
import * as _ateam_ai_mobile_plugin_runtime from '@ateam-ai-mobile/plugin-runtime';
|
|
6
|
-
import { PluginSurfaceMeta, PlatformPluginManifest, VoiceState, VoiceTranscriptEntry, VoiceSessionProvider, VoicePersona, VoiceStartOptions, VoiceCaller, VoiceSnapshot, VoiceEvent, VoiceSubscription } from '@ateam-ai-mobile/plugin-runtime';
|
|
6
|
+
import { PluginSurfaceMeta, PlatformPluginManifest, VoiceState, VoiceTranscriptEntry, VoiceSessionProvider, VoiceDeviceToolCall, VoicePersona, VoiceStartOptions, VoiceCaller, VoiceSnapshot, VoiceEvent, VoiceSubscription } from '@ateam-ai-mobile/plugin-runtime';
|
|
7
7
|
import * as zustand from 'zustand';
|
|
8
8
|
|
|
9
9
|
interface AdasChatScreenProps {
|
|
@@ -382,6 +382,13 @@ declare function VoiceCallSheet({ visible, onClose, skillSlug }: Props): react_j
|
|
|
382
382
|
|
|
383
383
|
type CallState = VoiceState;
|
|
384
384
|
type TranscriptEntry = VoiceTranscriptEntry;
|
|
385
|
+
/** A finished call, tagged with the session owner's pluginId when the tool is
|
|
386
|
+
* that plugin's own; null when it is not (another solution's tool, the
|
|
387
|
+
* platform's, an unregistered name, no name) — then only the platform sees it. */
|
|
388
|
+
interface DeviceToolRecord {
|
|
389
|
+
owner: string | null;
|
|
390
|
+
call: VoiceDeviceToolCall;
|
|
391
|
+
}
|
|
385
392
|
interface InternalState {
|
|
386
393
|
callState: CallState;
|
|
387
394
|
sessionId: string | null;
|
|
@@ -394,11 +401,11 @@ interface InternalState {
|
|
|
394
401
|
userSpeaking: boolean;
|
|
395
402
|
/** Epoch ms of the last speaking:false, or null before the first utterance. */
|
|
396
403
|
lastUserSpeechEndedAt: number | null;
|
|
397
|
-
/**
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
404
|
+
/** This session's finished device-tool calls: the LATEST per owner tag (see
|
|
405
|
+
* DeviceToolRecord), in the order they finished. The platform reads the last
|
|
406
|
+
* one; the owning plugin reads its own (lastDeviceToolFor) — one store, both
|
|
407
|
+
* views derived from it. */
|
|
408
|
+
deviceToolCalls: DeviceToolRecord[];
|
|
402
409
|
/** The WebView should be mounted (a session is wanted / live). */
|
|
403
410
|
mounted: boolean;
|
|
404
411
|
/** The WebView source, computed ONCE at start() so state changes (transcript,
|
|
@@ -461,6 +468,11 @@ declare class VoiceSessionManager implements VoiceSessionProvider {
|
|
|
461
468
|
* and the bundleManager registers a plugin's tools under that same id (never
|
|
462
469
|
* the bundle's own short `id`), so exact equality is the whole check. */
|
|
463
470
|
private toolScope;
|
|
471
|
+
/** Bumped whenever a session begins or ends. A device-tool call remembers the
|
|
472
|
+
* epoch it arrived in, and its outcome is recorded only if that session is
|
|
473
|
+
* still the current one — a tool that finishes after hang-up (or after a new
|
|
474
|
+
* call started) must not show up as the new session's last call. */
|
|
475
|
+
private sessionEpoch;
|
|
464
476
|
/** The platform (built-in sheet, mic indicator, session host) sees and
|
|
465
477
|
* controls any session — it is the user's own surface. A plugin caller may
|
|
466
478
|
* only touch a session it owns. */
|
|
@@ -486,13 +498,30 @@ declare class VoiceSessionManager implements VoiceSessionProvider {
|
|
|
486
498
|
*/
|
|
487
499
|
observe(text: string, caller?: VoiceCaller): void;
|
|
488
500
|
getSnapshot(caller?: VoiceCaller): VoiceSnapshot;
|
|
501
|
+
/** The snapshot for a caller that may access the session. Everything is the
|
|
502
|
+
* session's, except `lastDeviceTool`, which is scoped to the caller's tools. */
|
|
489
503
|
private realSnapshot;
|
|
504
|
+
/**
|
|
505
|
+
* The last finished device-tool call THIS caller may see. The platform (the
|
|
506
|
+
* user's own surface) sees every call. A plugin sees only calls to tools it
|
|
507
|
+
* registered — even on a session it owns, the model can call another
|
|
508
|
+
* solution's tool (a normal call offers every registered tool), and that
|
|
509
|
+
* tool's name and error are not this plugin's to read.
|
|
510
|
+
*/
|
|
511
|
+
private lastDeviceToolFor;
|
|
490
512
|
private foreignSnapshot;
|
|
491
513
|
subscribe(cb: (event: VoiceEvent) => void, caller?: VoiceCaller): VoiceSubscription;
|
|
492
514
|
transcript(caller?: VoiceCaller): TranscriptEntry[];
|
|
493
515
|
private buildUri;
|
|
494
516
|
/** Handle a postMessage from the WebView. `event` is the RN WebView event. */
|
|
495
517
|
handleMessage(event: any): void;
|
|
518
|
+
/**
|
|
519
|
+
* One device-tool call, arrival to outcome: validate → authorize → run. The
|
|
520
|
+
* answer goes back to the mini-app (the model reads it) AND becomes
|
|
521
|
+
* `lastDeviceTool` — the same object, recorded only once it is known, so a
|
|
522
|
+
* refused call never looks like one that ran.
|
|
523
|
+
*/
|
|
524
|
+
private handleDeviceToolCall;
|
|
496
525
|
/**
|
|
497
526
|
* Final authorization = Core's frozen allowset ∩ the current valid local
|
|
498
527
|
* registry, bound to the ACTIVE session. All must hold:
|
|
@@ -506,15 +535,14 @@ declare class VoiceSessionManager implements VoiceSessionProvider {
|
|
|
506
535
|
* No fallback to the live registry alone, ever.
|
|
507
536
|
*/
|
|
508
537
|
private isToolAuthorized;
|
|
509
|
-
/**
|
|
510
|
-
*
|
|
511
|
-
|
|
512
|
-
private postDeviceToolRefusal;
|
|
538
|
+
/** Answer the mini-app's pending device-tool request (a call's outcome, or the
|
|
539
|
+
* tool list) — it resolves the request with this payload by correlationId. */
|
|
540
|
+
private postDeviceToolResult;
|
|
513
541
|
/** PROPOSE the phone's available tools — this is an offer to Core, not the
|
|
514
542
|
* authorization set. Core sanitizes it and returns the accepted set in a
|
|
515
543
|
* `deviceToolAllowset` message, which is what actually gates execution. */
|
|
516
544
|
private sendDeviceToolList;
|
|
517
|
-
/** Run
|
|
545
|
+
/** Run an authorized device tool. Never throws: every failure is an answer. */
|
|
518
546
|
private runDeviceTool;
|
|
519
547
|
private setState;
|
|
520
548
|
private appendTranscript;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Message, JobProgress, UIAction, AdasSession, TenantMembership, HandoffE
|
|
|
3
3
|
import * as React$1 from 'react';
|
|
4
4
|
import React__default, { ReactNode } from 'react';
|
|
5
5
|
import * as _ateam_ai_mobile_plugin_runtime from '@ateam-ai-mobile/plugin-runtime';
|
|
6
|
-
import { PluginSurfaceMeta, PlatformPluginManifest, VoiceState, VoiceTranscriptEntry, VoiceSessionProvider, VoicePersona, VoiceStartOptions, VoiceCaller, VoiceSnapshot, VoiceEvent, VoiceSubscription } from '@ateam-ai-mobile/plugin-runtime';
|
|
6
|
+
import { PluginSurfaceMeta, PlatformPluginManifest, VoiceState, VoiceTranscriptEntry, VoiceSessionProvider, VoiceDeviceToolCall, VoicePersona, VoiceStartOptions, VoiceCaller, VoiceSnapshot, VoiceEvent, VoiceSubscription } from '@ateam-ai-mobile/plugin-runtime';
|
|
7
7
|
import * as zustand from 'zustand';
|
|
8
8
|
|
|
9
9
|
interface AdasChatScreenProps {
|
|
@@ -382,6 +382,13 @@ declare function VoiceCallSheet({ visible, onClose, skillSlug }: Props): react_j
|
|
|
382
382
|
|
|
383
383
|
type CallState = VoiceState;
|
|
384
384
|
type TranscriptEntry = VoiceTranscriptEntry;
|
|
385
|
+
/** A finished call, tagged with the session owner's pluginId when the tool is
|
|
386
|
+
* that plugin's own; null when it is not (another solution's tool, the
|
|
387
|
+
* platform's, an unregistered name, no name) — then only the platform sees it. */
|
|
388
|
+
interface DeviceToolRecord {
|
|
389
|
+
owner: string | null;
|
|
390
|
+
call: VoiceDeviceToolCall;
|
|
391
|
+
}
|
|
385
392
|
interface InternalState {
|
|
386
393
|
callState: CallState;
|
|
387
394
|
sessionId: string | null;
|
|
@@ -394,11 +401,11 @@ interface InternalState {
|
|
|
394
401
|
userSpeaking: boolean;
|
|
395
402
|
/** Epoch ms of the last speaking:false, or null before the first utterance. */
|
|
396
403
|
lastUserSpeechEndedAt: number | null;
|
|
397
|
-
/**
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
404
|
+
/** This session's finished device-tool calls: the LATEST per owner tag (see
|
|
405
|
+
* DeviceToolRecord), in the order they finished. The platform reads the last
|
|
406
|
+
* one; the owning plugin reads its own (lastDeviceToolFor) — one store, both
|
|
407
|
+
* views derived from it. */
|
|
408
|
+
deviceToolCalls: DeviceToolRecord[];
|
|
402
409
|
/** The WebView should be mounted (a session is wanted / live). */
|
|
403
410
|
mounted: boolean;
|
|
404
411
|
/** The WebView source, computed ONCE at start() so state changes (transcript,
|
|
@@ -461,6 +468,11 @@ declare class VoiceSessionManager implements VoiceSessionProvider {
|
|
|
461
468
|
* and the bundleManager registers a plugin's tools under that same id (never
|
|
462
469
|
* the bundle's own short `id`), so exact equality is the whole check. */
|
|
463
470
|
private toolScope;
|
|
471
|
+
/** Bumped whenever a session begins or ends. A device-tool call remembers the
|
|
472
|
+
* epoch it arrived in, and its outcome is recorded only if that session is
|
|
473
|
+
* still the current one — a tool that finishes after hang-up (or after a new
|
|
474
|
+
* call started) must not show up as the new session's last call. */
|
|
475
|
+
private sessionEpoch;
|
|
464
476
|
/** The platform (built-in sheet, mic indicator, session host) sees and
|
|
465
477
|
* controls any session — it is the user's own surface. A plugin caller may
|
|
466
478
|
* only touch a session it owns. */
|
|
@@ -486,13 +498,30 @@ declare class VoiceSessionManager implements VoiceSessionProvider {
|
|
|
486
498
|
*/
|
|
487
499
|
observe(text: string, caller?: VoiceCaller): void;
|
|
488
500
|
getSnapshot(caller?: VoiceCaller): VoiceSnapshot;
|
|
501
|
+
/** The snapshot for a caller that may access the session. Everything is the
|
|
502
|
+
* session's, except `lastDeviceTool`, which is scoped to the caller's tools. */
|
|
489
503
|
private realSnapshot;
|
|
504
|
+
/**
|
|
505
|
+
* The last finished device-tool call THIS caller may see. The platform (the
|
|
506
|
+
* user's own surface) sees every call. A plugin sees only calls to tools it
|
|
507
|
+
* registered — even on a session it owns, the model can call another
|
|
508
|
+
* solution's tool (a normal call offers every registered tool), and that
|
|
509
|
+
* tool's name and error are not this plugin's to read.
|
|
510
|
+
*/
|
|
511
|
+
private lastDeviceToolFor;
|
|
490
512
|
private foreignSnapshot;
|
|
491
513
|
subscribe(cb: (event: VoiceEvent) => void, caller?: VoiceCaller): VoiceSubscription;
|
|
492
514
|
transcript(caller?: VoiceCaller): TranscriptEntry[];
|
|
493
515
|
private buildUri;
|
|
494
516
|
/** Handle a postMessage from the WebView. `event` is the RN WebView event. */
|
|
495
517
|
handleMessage(event: any): void;
|
|
518
|
+
/**
|
|
519
|
+
* One device-tool call, arrival to outcome: validate → authorize → run. The
|
|
520
|
+
* answer goes back to the mini-app (the model reads it) AND becomes
|
|
521
|
+
* `lastDeviceTool` — the same object, recorded only once it is known, so a
|
|
522
|
+
* refused call never looks like one that ran.
|
|
523
|
+
*/
|
|
524
|
+
private handleDeviceToolCall;
|
|
496
525
|
/**
|
|
497
526
|
* Final authorization = Core's frozen allowset ∩ the current valid local
|
|
498
527
|
* registry, bound to the ACTIVE session. All must hold:
|
|
@@ -506,15 +535,14 @@ declare class VoiceSessionManager implements VoiceSessionProvider {
|
|
|
506
535
|
* No fallback to the live registry alone, ever.
|
|
507
536
|
*/
|
|
508
537
|
private isToolAuthorized;
|
|
509
|
-
/**
|
|
510
|
-
*
|
|
511
|
-
|
|
512
|
-
private postDeviceToolRefusal;
|
|
538
|
+
/** Answer the mini-app's pending device-tool request (a call's outcome, or the
|
|
539
|
+
* tool list) — it resolves the request with this payload by correlationId. */
|
|
540
|
+
private postDeviceToolResult;
|
|
513
541
|
/** PROPOSE the phone's available tools — this is an offer to Core, not the
|
|
514
542
|
* authorization set. Core sanitizes it and returns the accepted set in a
|
|
515
543
|
* `deviceToolAllowset` message, which is what actually gates execution. */
|
|
516
544
|
private sendDeviceToolList;
|
|
517
|
-
/** Run
|
|
545
|
+
/** Run an authorized device tool. Never throws: every failure is an answer. */
|
|
518
546
|
private runDeviceTool;
|
|
519
547
|
private setState;
|
|
520
548
|
private appendTranscript;
|
package/dist/index.js
CHANGED
|
@@ -2822,6 +2822,11 @@ var buildObserveScript = (text) => `
|
|
|
2822
2822
|
}
|
|
2823
2823
|
})(${JSON.stringify(text)}); true;
|
|
2824
2824
|
`;
|
|
2825
|
+
var DEVICE_TOOL_ERROR_MAX = 200;
|
|
2826
|
+
function clipError(message) {
|
|
2827
|
+
const chars = Array.from(message);
|
|
2828
|
+
return chars.length > DEVICE_TOOL_ERROR_MAX ? chars.slice(0, DEVICE_TOOL_ERROR_MAX - 1).join("") + "\u2026" : message;
|
|
2829
|
+
}
|
|
2825
2830
|
function originOf(url) {
|
|
2826
2831
|
if (!url) return null;
|
|
2827
2832
|
const m = /^([a-z][a-z0-9+.-]*:\/\/[^/?#]+)/i.exec(url);
|
|
@@ -2837,7 +2842,7 @@ var IDLE = {
|
|
|
2837
2842
|
muted: false,
|
|
2838
2843
|
userSpeaking: false,
|
|
2839
2844
|
lastUserSpeechEndedAt: null,
|
|
2840
|
-
|
|
2845
|
+
deviceToolCalls: [],
|
|
2841
2846
|
mounted: false,
|
|
2842
2847
|
webViewSource: null,
|
|
2843
2848
|
skillSlug: void 0
|
|
@@ -2893,6 +2898,11 @@ var VoiceSessionManager = class {
|
|
|
2893
2898
|
* and the bundleManager registers a plugin's tools under that same id (never
|
|
2894
2899
|
* the bundle's own short `id`), so exact equality is the whole check. */
|
|
2895
2900
|
this.toolScope = null;
|
|
2901
|
+
/** Bumped whenever a session begins or ends. A device-tool call remembers the
|
|
2902
|
+
* epoch it arrived in, and its outcome is recorded only if that session is
|
|
2903
|
+
* still the current one — a tool that finishes after hang-up (or after a new
|
|
2904
|
+
* call started) must not show up as the new session's last call. */
|
|
2905
|
+
this.sessionEpoch = 0;
|
|
2896
2906
|
this.injectedTrackHook = INJECT_TRACK_HOOK;
|
|
2897
2907
|
}
|
|
2898
2908
|
/** The platform (built-in sheet, mic indicator, session host) sees and
|
|
@@ -2922,11 +2932,11 @@ var VoiceSessionManager = class {
|
|
|
2922
2932
|
async start(opts, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
|
|
2923
2933
|
if (this.state.callState === "connected") {
|
|
2924
2934
|
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2925
|
-
return this.realSnapshot();
|
|
2935
|
+
return this.realSnapshot(caller);
|
|
2926
2936
|
}
|
|
2927
2937
|
if (this.state.callState === "connecting") {
|
|
2928
2938
|
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2929
|
-
return this.waitForConnect();
|
|
2939
|
+
return this.waitForConnect(caller);
|
|
2930
2940
|
}
|
|
2931
2941
|
if (opts?.skills === "none" && opts?.skillSlug) {
|
|
2932
2942
|
throw new Error("voice: skills:'none' (a clean call) cannot also hand the mic to a skill (skillSlug).");
|
|
@@ -2947,16 +2957,17 @@ var VoiceSessionManager = class {
|
|
|
2947
2957
|
const after = this.getInternal().callState;
|
|
2948
2958
|
if (after === "connected") {
|
|
2949
2959
|
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2950
|
-
return this.realSnapshot();
|
|
2960
|
+
return this.realSnapshot(caller);
|
|
2951
2961
|
}
|
|
2952
2962
|
if (after === "connecting") {
|
|
2953
2963
|
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2954
|
-
return this.waitForConnect();
|
|
2964
|
+
return this.waitForConnect(caller);
|
|
2955
2965
|
}
|
|
2956
2966
|
this.owner = caller;
|
|
2957
2967
|
this.toolScope = opts?.skills === "none" && typeof caller === "string" ? caller : null;
|
|
2958
2968
|
this.frozenTools = null;
|
|
2959
2969
|
this.frozenSessionId = null;
|
|
2970
|
+
this.sessionEpoch++;
|
|
2960
2971
|
const skillSlug = opts?.skillSlug;
|
|
2961
2972
|
this.setState({
|
|
2962
2973
|
callState: "connecting",
|
|
@@ -2965,12 +2976,15 @@ var VoiceSessionManager = class {
|
|
|
2965
2976
|
aiPartial: "",
|
|
2966
2977
|
userPartial: "",
|
|
2967
2978
|
muted: false,
|
|
2979
|
+
// A session that ended from inside the voice page (not via stop()) left
|
|
2980
|
+
// its calls here; the new session starts with none.
|
|
2981
|
+
deviceToolCalls: [],
|
|
2968
2982
|
mounted: true,
|
|
2969
2983
|
skillSlug,
|
|
2970
2984
|
persona: opts?.persona,
|
|
2971
2985
|
webViewSource: { uri: this.buildUri(skillSlug, opts?.persona, opts?.skills) }
|
|
2972
2986
|
});
|
|
2973
|
-
return this.waitForConnect();
|
|
2987
|
+
return this.waitForConnect(caller);
|
|
2974
2988
|
}
|
|
2975
2989
|
stop(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
|
|
2976
2990
|
if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
|
|
@@ -2984,6 +2998,7 @@ var VoiceSessionManager = class {
|
|
|
2984
2998
|
this.rejectPending(new Error("voice: session stopped before it connected"));
|
|
2985
2999
|
this.frozenTools = null;
|
|
2986
3000
|
this.frozenSessionId = null;
|
|
3001
|
+
this.sessionEpoch++;
|
|
2987
3002
|
this.setState({
|
|
2988
3003
|
...IDLE,
|
|
2989
3004
|
// keep the transcript of the call that just ended for any reader still up
|
|
@@ -3016,9 +3031,11 @@ var VoiceSessionManager = class {
|
|
|
3016
3031
|
this.webView?.injectJavaScript?.(buildObserveScript(text));
|
|
3017
3032
|
}
|
|
3018
3033
|
getSnapshot(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
|
|
3019
|
-
return this.canAccess(caller) ? this.realSnapshot() : this.foreignSnapshot();
|
|
3034
|
+
return this.canAccess(caller) ? this.realSnapshot(caller) : this.foreignSnapshot();
|
|
3020
3035
|
}
|
|
3021
|
-
|
|
3036
|
+
/** The snapshot for a caller that may access the session. Everything is the
|
|
3037
|
+
* session's, except `lastDeviceTool`, which is scoped to the caller's tools. */
|
|
3038
|
+
realSnapshot(caller) {
|
|
3022
3039
|
const s = this.state;
|
|
3023
3040
|
return {
|
|
3024
3041
|
state: s.callState,
|
|
@@ -3028,11 +3045,23 @@ var VoiceSessionManager = class {
|
|
|
3028
3045
|
listening: !!s.userPartial,
|
|
3029
3046
|
userSpeaking: s.userSpeaking,
|
|
3030
3047
|
lastUserSpeechEndedAt: s.lastUserSpeechEndedAt,
|
|
3031
|
-
lastDeviceTool:
|
|
3048
|
+
lastDeviceTool: this.lastDeviceToolFor(caller),
|
|
3032
3049
|
authorizedSessionId: this.frozenSessionId,
|
|
3033
3050
|
lastError: s.error
|
|
3034
3051
|
};
|
|
3035
3052
|
}
|
|
3053
|
+
/**
|
|
3054
|
+
* The last finished device-tool call THIS caller may see. The platform (the
|
|
3055
|
+
* user's own surface) sees every call. A plugin sees only calls to tools it
|
|
3056
|
+
* registered — even on a session it owns, the model can call another
|
|
3057
|
+
* solution's tool (a normal call offers every registered tool), and that
|
|
3058
|
+
* tool's name and error are not this plugin's to read.
|
|
3059
|
+
*/
|
|
3060
|
+
lastDeviceToolFor(caller) {
|
|
3061
|
+
const calls = this.state.deviceToolCalls;
|
|
3062
|
+
if (caller === import_plugin_runtime13.PLATFORM_VOICE_CALLER) return calls.length ? calls[calls.length - 1].call : null;
|
|
3063
|
+
return calls.find((r) => r.owner === caller)?.call ?? null;
|
|
3064
|
+
}
|
|
3036
3065
|
foreignSnapshot() {
|
|
3037
3066
|
return {
|
|
3038
3067
|
state: "idle",
|
|
@@ -3160,18 +3189,52 @@ var VoiceSessionManager = class {
|
|
|
3160
3189
|
console.warn(`[voiceSession] REFUSED device tool call from "${sender || "unknown origin"}" (expected "${expected}")`);
|
|
3161
3190
|
break;
|
|
3162
3191
|
}
|
|
3163
|
-
|
|
3164
|
-
if (!this.isToolAuthorized(data.name)) {
|
|
3165
|
-
const notReady = this.frozenTools === null;
|
|
3166
|
-
console.warn(`[voiceSession] REFUSED "${data.name}" \u2014 ${notReady ? "allowset not received yet" : "not authorized for this session"}`);
|
|
3167
|
-
this.postDeviceToolRefusal(data.correlationId, data.name, notReady);
|
|
3168
|
-
break;
|
|
3169
|
-
}
|
|
3170
|
-
void this.runDeviceTool(data);
|
|
3192
|
+
void this.handleDeviceToolCall(data);
|
|
3171
3193
|
break;
|
|
3172
3194
|
}
|
|
3173
3195
|
}
|
|
3174
3196
|
}
|
|
3197
|
+
/**
|
|
3198
|
+
* One device-tool call, arrival to outcome: validate → authorize → run. The
|
|
3199
|
+
* answer goes back to the mini-app (the model reads it) AND becomes
|
|
3200
|
+
* `lastDeviceTool` — the same object, recorded only once it is known, so a
|
|
3201
|
+
* refused call never looks like one that ran.
|
|
3202
|
+
*/
|
|
3203
|
+
async handleDeviceToolCall(data) {
|
|
3204
|
+
const { correlationId, name, args, timeoutMs } = data || {};
|
|
3205
|
+
const at = Date.now();
|
|
3206
|
+
const epoch = this.sessionEpoch;
|
|
3207
|
+
const toolName = typeof name === "string" ? name : "";
|
|
3208
|
+
const sessionOwner = this.owner;
|
|
3209
|
+
const owner = typeof sessionOwner === "string" && toolName && (0, import_plugin_runtime13.hasDeviceTool)(toolName, sessionOwner) ? sessionOwner : null;
|
|
3210
|
+
let answer;
|
|
3211
|
+
if (!toolName) {
|
|
3212
|
+
answer = { ok: false, reason: "invalid", error: "The device tool call named no tool." };
|
|
3213
|
+
} else if (!correlationId) {
|
|
3214
|
+
answer = {
|
|
3215
|
+
ok: false,
|
|
3216
|
+
reason: "invalid",
|
|
3217
|
+
error: `The call to "${toolName}" carried no correlationId, so it could not be answered.`
|
|
3218
|
+
};
|
|
3219
|
+
} else if (!this.isToolAuthorized(toolName)) {
|
|
3220
|
+
const notReady = this.frozenTools === null;
|
|
3221
|
+
console.warn(`[voiceSession] REFUSED "${toolName}" \u2014 ${notReady ? "allowset not received yet" : "not authorized for this session"}`);
|
|
3222
|
+
answer = notReady ? {
|
|
3223
|
+
ok: false,
|
|
3224
|
+
reason: "not_ready",
|
|
3225
|
+
error: "The voice session is still starting up \u2014 its tool allowset has not arrived yet. Try again in a moment."
|
|
3226
|
+
} : { ok: false, reason: "not_authorized", error: `"${toolName}" is not available in this voice session.` };
|
|
3227
|
+
} else {
|
|
3228
|
+
answer = await this.runDeviceTool(toolName, args, timeoutMs);
|
|
3229
|
+
}
|
|
3230
|
+
if (correlationId) this.postDeviceToolResult({ correlationId, ...answer });
|
|
3231
|
+
if (epoch !== this.sessionEpoch) return;
|
|
3232
|
+
const ms = Math.max(0, Date.now() - at);
|
|
3233
|
+
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) };
|
|
3234
|
+
this.setState({
|
|
3235
|
+
deviceToolCalls: [...this.state.deviceToolCalls.filter((r) => r.owner !== owner), { owner, call }]
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3175
3238
|
/**
|
|
3176
3239
|
* Final authorization = Core's frozen allowset ∩ the current valid local
|
|
3177
3240
|
* registry, bound to the ACTIVE session. All must hold:
|
|
@@ -3191,21 +3254,9 @@ var VoiceSessionManager = class {
|
|
|
3191
3254
|
if (!this.frozenTools.has(name)) return false;
|
|
3192
3255
|
return (0, import_plugin_runtime13.hasDeviceTool)(name, this.toolScope ?? void 0);
|
|
3193
3256
|
}
|
|
3194
|
-
/**
|
|
3195
|
-
*
|
|
3196
|
-
|
|
3197
|
-
postDeviceToolRefusal(correlationId, name, notReady) {
|
|
3198
|
-
const payload = notReady ? {
|
|
3199
|
-
correlationId,
|
|
3200
|
-
ok: false,
|
|
3201
|
-
reason: "not_ready",
|
|
3202
|
-
error: "The voice session is still starting up \u2014 its tool allowset has not arrived yet. Try again in a moment."
|
|
3203
|
-
} : {
|
|
3204
|
-
correlationId,
|
|
3205
|
-
ok: false,
|
|
3206
|
-
reason: "not_authorized",
|
|
3207
|
-
error: `"${name}" is not available in this voice session.`
|
|
3208
|
-
};
|
|
3257
|
+
/** Answer the mini-app's pending device-tool request (a call's outcome, or the
|
|
3258
|
+
* tool list) — it resolves the request with this payload by correlationId. */
|
|
3259
|
+
postDeviceToolResult(payload) {
|
|
3209
3260
|
this.webView?.injectJavaScript?.(
|
|
3210
3261
|
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3211
3262
|
JSON.stringify(payload)
|
|
@@ -3222,28 +3273,16 @@ var VoiceSessionManager = class {
|
|
|
3222
3273
|
description: t.description,
|
|
3223
3274
|
input_schema: t.input_schema
|
|
3224
3275
|
}));
|
|
3225
|
-
this.
|
|
3226
|
-
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3227
|
-
JSON.stringify({ correlationId, ok: true, tools })
|
|
3228
|
-
)}); true;`
|
|
3229
|
-
);
|
|
3276
|
+
this.postDeviceToolResult({ correlationId, ok: true, tools });
|
|
3230
3277
|
}
|
|
3231
|
-
/** Run
|
|
3232
|
-
async runDeviceTool(
|
|
3233
|
-
const { correlationId, name, args, timeoutMs } = data || {};
|
|
3234
|
-
if (!correlationId || typeof name !== "string") return;
|
|
3235
|
-
let payload;
|
|
3278
|
+
/** Run an authorized device tool. Never throws: every failure is an answer. */
|
|
3279
|
+
async runDeviceTool(name, args, timeoutMs) {
|
|
3236
3280
|
try {
|
|
3237
3281
|
const outcome = await (0, import_plugin_runtime13.callDeviceTool)(name, args || {}, { timeoutMs });
|
|
3238
|
-
|
|
3282
|
+
return outcome.ok ? { ok: true, result: outcome.result } : { ok: false, reason: outcome.reason, error: outcome.error };
|
|
3239
3283
|
} catch (err) {
|
|
3240
|
-
|
|
3284
|
+
return { ok: false, reason: "failed", error: err?.message || String(err) };
|
|
3241
3285
|
}
|
|
3242
|
-
this.webView?.injectJavaScript?.(
|
|
3243
|
-
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3244
|
-
JSON.stringify(payload)
|
|
3245
|
-
)}); true;`
|
|
3246
|
-
);
|
|
3247
3286
|
}
|
|
3248
3287
|
// ── internals ────────────────────────────────────────────────
|
|
3249
3288
|
setState(partial) {
|
|
@@ -3263,22 +3302,21 @@ var VoiceSessionManager = class {
|
|
|
3263
3302
|
}
|
|
3264
3303
|
}
|
|
3265
3304
|
}
|
|
3266
|
-
waitForConnect() {
|
|
3305
|
+
waitForConnect(caller) {
|
|
3267
3306
|
return new Promise((resolve, reject) => {
|
|
3268
3307
|
const timer = setTimeout(() => {
|
|
3269
3308
|
this.pending = this.pending.filter((p) => p.timer !== timer);
|
|
3270
3309
|
reject(new Error("voice: timed out waiting to connect (is VoiceSessionHost mounted?)"));
|
|
3271
3310
|
}, CONNECT_TIMEOUT_MS);
|
|
3272
|
-
this.pending.push({ resolve, reject, timer });
|
|
3311
|
+
this.pending.push({ caller, resolve, reject, timer });
|
|
3273
3312
|
});
|
|
3274
3313
|
}
|
|
3275
3314
|
resolvePending() {
|
|
3276
|
-
const snap = this.realSnapshot();
|
|
3277
3315
|
const waiting = this.pending;
|
|
3278
3316
|
this.pending = [];
|
|
3279
3317
|
for (const p of waiting) {
|
|
3280
3318
|
clearTimeout(p.timer);
|
|
3281
|
-
p.resolve(
|
|
3319
|
+
p.resolve(this.realSnapshot(p.caller));
|
|
3282
3320
|
}
|
|
3283
3321
|
}
|
|
3284
3322
|
rejectPending(err) {
|
|
@@ -3299,6 +3337,7 @@ var VoiceSessionManager = class {
|
|
|
3299
3337
|
this.pendingStarts.clear();
|
|
3300
3338
|
this.frozenTools = null;
|
|
3301
3339
|
this.frozenSessionId = null;
|
|
3340
|
+
this.sessionEpoch++;
|
|
3302
3341
|
this.listeners.clear();
|
|
3303
3342
|
this.webView = null;
|
|
3304
3343
|
}
|