@theoremjs/react 0.0.0-stage → 0.3.0
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/LICENSE +21 -0
- package/README.md +246 -2
- package/client/attachment-hover-preview.d.ts +11 -0
- package/client/attachment-hover-preview.js +36 -0
- package/client/audio-level.d.ts +5 -0
- package/client/audio-level.js +24 -0
- package/client/composer-attachments.d.ts +30 -0
- package/client/composer-attachments.js +33 -0
- package/client/composer-drawer.d.ts +19 -0
- package/client/composer-drawer.js +15 -0
- package/client/composer-hints.d.ts +25 -0
- package/client/composer-hints.js +20 -0
- package/client/composer-primary.d.ts +16 -0
- package/client/composer-primary.js +25 -0
- package/client/decision-transport.d.ts +48 -0
- package/client/decision-transport.js +102 -0
- package/client/decode-composer-draft.d.ts +11 -0
- package/client/decode-composer-draft.js +12 -0
- package/client/encode-composer-draft.d.ts +9 -0
- package/client/encode-composer-draft.js +34 -0
- package/client/encode-files.d.ts +18 -0
- package/client/encode-files.js +54 -0
- package/client/failure.d.ts +32 -0
- package/client/failure.js +34 -0
- package/client/generation-selection.d.ts +15 -0
- package/client/generation-selection.js +24 -0
- package/client/host-call.d.ts +51 -0
- package/client/host-call.js +42 -0
- package/client/host-transport.d.ts +68 -0
- package/client/host-transport.js +74 -0
- package/client/image-output.d.ts +6 -0
- package/client/image-output.js +13 -0
- package/client/index.d.ts +20 -0
- package/client/index.js +18 -0
- package/client/ink-waveform.d.ts +17 -0
- package/client/ink-waveform.js +69 -0
- package/client/live/apply-live-turn-tool-event.d.ts +14 -0
- package/client/live/apply-live-turn-tool-event.js +39 -0
- package/client/live/live-captions.d.ts +23 -0
- package/client/live/live-captions.js +70 -0
- package/client/live/live-mic-forward.d.ts +29 -0
- package/client/live/live-mic-forward.js +37 -0
- package/client/live/live-state.d.ts +11 -0
- package/client/live/live-state.js +20 -0
- package/client/live/live-tool.d.ts +11 -0
- package/client/live/live-tool.js +1 -0
- package/client/live/live-video.d.ts +10 -0
- package/client/live/live-video.js +71 -0
- package/client/live/run-live-tool-call.d.ts +17 -0
- package/client/live/run-live-tool-call.js +32 -0
- package/client/live-client.d.ts +141 -0
- package/client/live-client.js +638 -0
- package/client/live-errors.d.ts +2 -0
- package/client/live-errors.js +9 -0
- package/client/live-messages.d.ts +110 -0
- package/client/live-messages.js +67 -0
- package/client/mic-capture.d.ts +4 -0
- package/client/mic-capture.js +6 -0
- package/client/oauth-popup.d.ts +26 -0
- package/client/oauth-popup.js +30 -0
- package/client/pcm-downsample.d.ts +4 -0
- package/client/pcm-downsample.js +45 -0
- package/client/run-commit.d.ts +61 -0
- package/client/run-commit.js +124 -0
- package/client/run-session.d.ts +80 -0
- package/client/run-session.js +215 -0
- package/client/schema-fields.d.ts +63 -0
- package/client/schema-fields.js +178 -0
- package/client/shaped-data.d.ts +118 -0
- package/client/shaped-data.js +314 -0
- package/client/source-citations.d.ts +13 -0
- package/client/source-citations.js +56 -0
- package/client/state-fields.d.ts +16 -0
- package/client/state-fields.js +44 -0
- package/client/tool-result.d.ts +78 -0
- package/client/tool-result.js +275 -0
- package/client/tool-resume.d.ts +33 -0
- package/client/tool-resume.js +11 -0
- package/client/trace-feed.d.ts +13 -0
- package/client/trace-feed.js +18 -0
- package/client/trace-story.d.ts +163 -0
- package/client/trace-story.js +416 -0
- package/client/trace-view.d.ts +130 -0
- package/client/trace-view.js +293 -0
- package/client/transcript-groups.d.ts +114 -0
- package/client/transcript-groups.js +167 -0
- package/client/transcript-scroll.d.ts +7 -0
- package/client/transcript-scroll.js +6 -0
- package/client/transport.d.ts +176 -0
- package/client/transport.js +256 -0
- package/client/turn-client.d.ts +35 -0
- package/client/turn-client.js +93 -0
- package/client/voice-label.d.ts +2 -0
- package/client/voice-label.js +15 -0
- package/client/voice-recorder.d.ts +27 -0
- package/client/voice-recorder.js +165 -0
- package/client/walk-away.d.ts +34 -0
- package/client/walk-away.js +48 -0
- package/client/wire-line.d.ts +56 -0
- package/client/wire-line.js +90 -0
- package/client/worklets/mic-capture.js +16 -0
- package/components/InkWaveform.d.ts +10 -0
- package/components/InkWaveform.js +108 -0
- package/components/live/use-live-runner-controls.d.ts +48 -0
- package/components/live/use-live-runner-controls.js +100 -0
- package/components/live/use-live-runner-model.d.ts +45 -0
- package/components/live/use-live-runner-model.js +175 -0
- package/components/live/use-live-runner-ui.d.ts +65 -0
- package/components/live/use-live-runner-ui.js +159 -0
- package/components/live/use-live-session-client.d.ts +41 -0
- package/components/live/use-live-session-client.js +123 -0
- package/components/use-composer-voice.d.ts +28 -0
- package/components/use-composer-voice.js +90 -0
- package/components/use-editor-selection.d.ts +3 -0
- package/components/use-editor-selection.js +16 -0
- package/components/use-voice-playback.d.ts +16 -0
- package/components/use-voice-playback.js +68 -0
- package/hooks/use-described.d.ts +8 -0
- package/hooks/use-described.js +23 -0
- package/hooks/use-theorem-chat-actions.d.ts +75 -0
- package/hooks/use-theorem-chat-actions.js +265 -0
- package/hooks/use-theorem-chat-state.d.ts +66 -0
- package/hooks/use-theorem-chat-state.js +118 -0
- package/hooks/use-theorem-chat.d.ts +63 -0
- package/hooks/use-theorem-chat.js +340 -0
- package/hooks/use-theorem-decision.d.ts +23 -0
- package/hooks/use-theorem-decision.js +75 -0
- package/hooks/use-theorem-host.d.ts +44 -0
- package/hooks/use-theorem-host.js +117 -0
- package/hooks/use-theorem-interface.d.ts +19 -0
- package/hooks/use-theorem-interface.js +25 -0
- package/hooks/use-trace-records.d.ts +4 -0
- package/hooks/use-trace-records.js +7 -0
- package/index.d.ts +14 -0
- package/index.js +14 -0
- package/live.d.ts +8 -0
- package/live.js +10 -0
- package/package.json +73 -4
- package/server/credential-store.d.ts +24 -0
- package/server/credential-store.js +14 -0
- package/server/decision-handler.d.ts +34 -0
- package/server/decision-handler.js +55 -0
- package/server/handler.d.ts +114 -0
- package/server/handler.js +568 -0
- package/server/json-response.d.ts +2 -0
- package/server/json-response.js +7 -0
- package/server/mod.d.ts +16 -0
- package/server/mod.js +15 -0
- package/server/request-check.d.ts +16 -0
- package/server/request-check.js +34 -0
- package/server/session-store.d.ts +74 -0
- package/server/session-store.js +49 -0
- package/server/steer-inbox.d.ts +35 -0
- package/server/steer-inbox.js +52 -0
- package/server/walk-away.d.ts +22 -0
- package/server/walk-away.js +41 -0
- package/styles/ink-controls.css +134 -0
- package/ui/ChatComposerBar.d.ts +39 -0
- package/ui/ChatComposerBar.js +251 -0
- package/ui/ChatTranscript.d.ts +30 -0
- package/ui/ChatTranscript.js +474 -0
- package/ui/ConsoleFrame.d.ts +47 -0
- package/ui/ConsoleFrame.js +46 -0
- package/ui/DecisionAnswers.d.ts +15 -0
- package/ui/DecisionAnswers.js +102 -0
- package/ui/LiveRunner.d.ts +28 -0
- package/ui/LiveRunner.js +193 -0
- package/ui/SchemaFields.d.ts +10 -0
- package/ui/SchemaFields.js +122 -0
- package/ui/ShapedData.d.ts +11 -0
- package/ui/ShapedData.js +151 -0
- package/ui/SidePanel.d.ts +57 -0
- package/ui/SidePanel.js +50 -0
- package/ui/StateFields.d.ts +36 -0
- package/ui/StateFields.js +96 -0
- package/ui/TheoremChat.d.ts +71 -0
- package/ui/TheoremChat.js +146 -0
- package/ui/TheoremDecision.d.ts +38 -0
- package/ui/TheoremDecision.js +87 -0
- package/ui/TheoremHost.d.ts +38 -0
- package/ui/TheoremHost.js +150 -0
- package/ui/ToolGateCard.d.ts +28 -0
- package/ui/ToolGateCard.js +100 -0
- package/ui/ToolResult.d.ts +10 -0
- package/ui/ToolResult.js +60 -0
- package/ui/ToolResultCharts.d.ts +5 -0
- package/ui/ToolResultCharts.js +86 -0
- package/ui/TraceCharts.d.ts +13 -0
- package/ui/TraceCharts.js +235 -0
- package/ui/TraceGuardrails.d.ts +10 -0
- package/ui/TraceGuardrails.js +56 -0
- package/ui/TraceInspector.d.ts +6 -0
- package/ui/TraceInspector.js +189 -0
- package/ui/TraceInspectorPanel.d.ts +24 -0
- package/ui/TraceInspectorPanel.js +57 -0
- package/ui/TraceOverview.d.ts +34 -0
- package/ui/TraceOverview.js +96 -0
- package/ui/TraceSpanDetail.d.ts +10 -0
- package/ui/TraceSpanDetail.js +188 -0
- package/ui/TraceStory.d.ts +20 -0
- package/ui/TraceStory.js +107 -0
- package/ui/TraceValues.d.ts +36 -0
- package/ui/TraceValues.js +113 -0
- package/ui/TraceWaterfall.d.ts +13 -0
- package/ui/TraceWaterfall.js +132 -0
- package/ui/VoiceNote.d.ts +12 -0
- package/ui/VoiceNote.js +28 -0
- package/ui/built/theme.css +774 -0
- package/ui/built/theorem.d.ts +13 -0
- package/ui/built/theorem.js +631 -0
- package/ui/built/theorem.variants.d.ts +16 -0
- package/ui/disclosure-motion.d.ts +6 -0
- package/ui/disclosure-motion.js +163 -0
- package/ui/icons.d.ts +4 -0
- package/ui/icons.js +37 -0
- package/ui/index.d.ts +25 -0
- package/ui/index.js +29 -0
- package/ui/labels-provider.d.ts +16 -0
- package/ui/labels-provider.js +61 -0
- package/ui/labels.d.ts +1153 -0
- package/ui/labels.js +603 -0
- package/ui/theme.d.ts +10 -0
- package/ui/theme.js +13 -0
- package/ui/theorem-theme.d.ts +10 -0
- package/ui/theorem-theme.js +83 -0
- package/ui/trace-palette.d.ts +24 -0
- package/ui/trace-palette.js +24 -0
- package/ui/trace-patterns.d.ts +32 -0
- package/ui/trace-patterns.js +44 -0
- package/ui/transcript-copy-text.d.ts +4 -0
- package/ui/transcript-copy-text.js +39 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const INK_WAVE_BAR_COUNT = 21;
|
|
2
|
+
export declare const INK_WAVE_HERO_BAR_COUNT = 45;
|
|
3
|
+
/** Per-bar phase offsets — stable undulation, not a fixed bell curve. */
|
|
4
|
+
export declare function inkWavePhases(count: number): number[];
|
|
5
|
+
export type InkWaveStatus = 'disconnected' | 'connecting' | 'connected' | 'ready' | 'listening' | 'speaking' | 'working' | 'error';
|
|
6
|
+
export type InkWaveDriver = 'idle' | 'input' | 'output' | 'tool' | 'working' | 'connecting';
|
|
7
|
+
export declare function inkWaveDriver(status: InkWaveStatus, toolActive: boolean, inputLevel: number, outputLevel: number, frozen?: boolean): InkWaveDriver;
|
|
8
|
+
export declare function computeInkBarTargets(args: {
|
|
9
|
+
phases: readonly number[];
|
|
10
|
+
timeMs: number;
|
|
11
|
+
driver: InkWaveDriver;
|
|
12
|
+
inputLevel: number;
|
|
13
|
+
outputLevel: number;
|
|
14
|
+
frozen?: boolean;
|
|
15
|
+
}): number[];
|
|
16
|
+
/** Smooth toward targets each animation frame (orchid ease feel). */
|
|
17
|
+
export declare function stepInkBarHeights(current: number[], targets: number[], alpha?: number): number[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const INK_WAVE_BAR_COUNT = 21;
|
|
2
|
+
export const INK_WAVE_HERO_BAR_COUNT = 45;
|
|
3
|
+
const LEVEL_FLOOR = 0.02;
|
|
4
|
+
/** Per-bar phase offsets — stable undulation, not a fixed bell curve. */
|
|
5
|
+
export function inkWavePhases(count) {
|
|
6
|
+
return Array.from({ length: count }, (_, i) => i * 0.73 + Math.sin(i * 1.17) * 0.4);
|
|
7
|
+
}
|
|
8
|
+
export function inkWaveDriver(status, toolActive, inputLevel, outputLevel, frozen = false) {
|
|
9
|
+
if (frozen)
|
|
10
|
+
return 'idle';
|
|
11
|
+
if (status === 'disconnected' || status === 'error')
|
|
12
|
+
return 'idle';
|
|
13
|
+
if (status === 'connecting')
|
|
14
|
+
return 'connecting';
|
|
15
|
+
if (toolActive)
|
|
16
|
+
return 'tool';
|
|
17
|
+
if (status === 'speaking' || outputLevel > LEVEL_FLOOR)
|
|
18
|
+
return 'output';
|
|
19
|
+
if (inputLevel > LEVEL_FLOOR)
|
|
20
|
+
return 'input';
|
|
21
|
+
if (status === 'working')
|
|
22
|
+
return 'working';
|
|
23
|
+
if (status === 'listening' || status === 'ready')
|
|
24
|
+
return 'idle';
|
|
25
|
+
return 'idle';
|
|
26
|
+
}
|
|
27
|
+
export function computeInkBarTargets(args) {
|
|
28
|
+
const t = args.timeMs * 0.001;
|
|
29
|
+
if (args.frozen) {
|
|
30
|
+
return args.phases.map((phase, index) => {
|
|
31
|
+
const silhouette = 0.22 + 0.55 * (0.5 + 0.5 * Math.sin(phase * 1.7 + index * 0.41));
|
|
32
|
+
return Math.min(0.82, Math.max(0.1, silhouette));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const input = args.inputLevel;
|
|
36
|
+
const output = args.outputLevel;
|
|
37
|
+
let master;
|
|
38
|
+
switch (args.driver) {
|
|
39
|
+
case 'output':
|
|
40
|
+
master = 0.18 + output * 0.95;
|
|
41
|
+
break;
|
|
42
|
+
case 'input':
|
|
43
|
+
master = 0.16 + input * 0.96;
|
|
44
|
+
break;
|
|
45
|
+
case 'tool':
|
|
46
|
+
master = 0.28 + 0.18 * Math.sin(t * 5.2) + Math.max(output, input) * 0.45;
|
|
47
|
+
break;
|
|
48
|
+
case 'working':
|
|
49
|
+
// Quiet, slow breathing: the server is reasoning, nobody is speaking.
|
|
50
|
+
master = 0.16 + 0.08 * Math.sin(t * 2.2) + Math.max(output, input) * 0.45;
|
|
51
|
+
break;
|
|
52
|
+
case 'connecting':
|
|
53
|
+
master = 0.14 + 0.1 * Math.sin(t * 3.1);
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
master = 0.08 + 0.04 * Math.sin(t * 1.4);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
return args.phases.map((phase, index) => {
|
|
60
|
+
const wobble = 0.72 + 0.28 * Math.sin(t * 3.4 + phase);
|
|
61
|
+
const ripple = 0.68 + 0.32 * Math.sin(t * 5.3 + index * 0.62);
|
|
62
|
+
const height = master * wobble * ripple;
|
|
63
|
+
return Math.min(1, Math.max(0.06, height));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Smooth toward targets each animation frame (orchid ease feel). */
|
|
67
|
+
export function stepInkBarHeights(current, targets, alpha = 0.14) {
|
|
68
|
+
return current.map((value, index) => value + (targets[index] - value) * alpha);
|
|
69
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TurnEvent } from '@theoremjs/agents';
|
|
2
|
+
type LiveToolEventArgs = {
|
|
3
|
+
/** The call whose gate the user is looking at, if one is open. */
|
|
4
|
+
gateCallId: string | undefined;
|
|
5
|
+
/** Close the open gate without an answer: its call is gone. */
|
|
6
|
+
withdrawGate: () => void;
|
|
7
|
+
clearInterim: () => void;
|
|
8
|
+
clearActiveTool: () => void;
|
|
9
|
+
reportFailure: (err: unknown) => void;
|
|
10
|
+
setActiveTool: (name: string) => void;
|
|
11
|
+
};
|
|
12
|
+
export type { LiveToolEventArgs };
|
|
13
|
+
/** Apply a live turn event to active-tool / caption / error state. */
|
|
14
|
+
export declare function applyLiveTurnToolEvent(event: TurnEvent, args: LiveToolEventArgs): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TheoremStreamError } from "../transport.js";
|
|
2
|
+
/**
|
|
3
|
+
* A failed tool step as the host reported it: the user reads its wording (else
|
|
4
|
+
* the lexicon's for its kind); the model's `message` stays builder detail.
|
|
5
|
+
*/
|
|
6
|
+
function toolStepFailure(failure) {
|
|
7
|
+
return new TheoremStreamError(failure.kind, failure.error, failure.message);
|
|
8
|
+
}
|
|
9
|
+
function clearToolUnlessGated(args) {
|
|
10
|
+
if (args.gateCallId === undefined)
|
|
11
|
+
args.clearActiveTool();
|
|
12
|
+
}
|
|
13
|
+
function applyToolPhase(tool, args) {
|
|
14
|
+
if (tool.phase === 'cancel' && tool.callId === args.gateCallId) {
|
|
15
|
+
args.withdrawGate();
|
|
16
|
+
args.clearActiveTool();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (tool.phase === 'cancel' || tool.phase === 'complete') {
|
|
20
|
+
clearToolUnlessGated(args);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (tool.phase === 'error') {
|
|
24
|
+
args.reportFailure(toolStepFailure(tool.failure));
|
|
25
|
+
clearToolUnlessGated(args);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
args.setActiveTool(tool.name);
|
|
29
|
+
}
|
|
30
|
+
/** Apply a live turn event to active-tool / caption / error state. */
|
|
31
|
+
export function applyLiveTurnToolEvent(event, args) {
|
|
32
|
+
if (event.type === 'done') {
|
|
33
|
+
args.clearInterim();
|
|
34
|
+
clearToolUnlessGated(args);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (event.type === 'tool')
|
|
38
|
+
applyToolPhase(event.tool, args);
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type LiveCaptionTurn = {
|
|
2
|
+
id: string;
|
|
3
|
+
role: 'user' | 'agent';
|
|
4
|
+
text: string;
|
|
5
|
+
};
|
|
6
|
+
export type LiveCaptionState = {
|
|
7
|
+
turns: LiveCaptionTurn[];
|
|
8
|
+
interimUser: string;
|
|
9
|
+
interimAgent: string;
|
|
10
|
+
};
|
|
11
|
+
export type ApplyLiveTranscriptOptions = {
|
|
12
|
+
/** Commit as a new turn even when the last turn shares the same role (explicit text send). */
|
|
13
|
+
forceNew?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function emptyLiveCaptionState(): LiveCaptionState;
|
|
16
|
+
/**
|
|
17
|
+
* Fold streaming ASR deltas into turn-by-turn caption lines.
|
|
18
|
+
* Final chunks for the same role append to the open turn; role switches start a new turn.
|
|
19
|
+
*/
|
|
20
|
+
export declare function applyLiveTranscript(state: LiveCaptionState, text: string, isUser: boolean, interim?: boolean, options?: ApplyLiveTranscriptOptions): LiveCaptionState;
|
|
21
|
+
/** Clear streaming partials when a live turn completes or is interrupted. */
|
|
22
|
+
export declare function clearLiveCaptionInterim(state: LiveCaptionState): LiveCaptionState;
|
|
23
|
+
export declare function latestLiveCaptionTurnId(state: LiveCaptionState): string | null;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export function emptyLiveCaptionState() {
|
|
2
|
+
return { turns: [], interimUser: '', interimAgent: '' };
|
|
3
|
+
}
|
|
4
|
+
function mergeInterimWithFinal(interim, final) {
|
|
5
|
+
if (!interim)
|
|
6
|
+
return final;
|
|
7
|
+
if (!final)
|
|
8
|
+
return interim;
|
|
9
|
+
if (final.startsWith(interim) || interim.startsWith(final)) {
|
|
10
|
+
return final.length >= interim.length ? final : interim;
|
|
11
|
+
}
|
|
12
|
+
return appendTurnText(interim, final);
|
|
13
|
+
}
|
|
14
|
+
function appendTurnText(existing, chunk) {
|
|
15
|
+
if (!existing)
|
|
16
|
+
return chunk;
|
|
17
|
+
if (!chunk)
|
|
18
|
+
return existing;
|
|
19
|
+
const needsSpace = !existing.endsWith(' ') &&
|
|
20
|
+
!chunk.startsWith(' ') &&
|
|
21
|
+
!/^\s/.test(chunk) &&
|
|
22
|
+
!/^[\s.,!?;:]/.test(chunk);
|
|
23
|
+
return needsSpace ? `${existing} ${chunk}` : `${existing}${chunk}`;
|
|
24
|
+
}
|
|
25
|
+
function nextTurnId(state) {
|
|
26
|
+
return `${String(Date.now())}-${String(state.turns.length)}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fold streaming ASR deltas into turn-by-turn caption lines.
|
|
30
|
+
* Final chunks for the same role append to the open turn; role switches start a new turn.
|
|
31
|
+
*/
|
|
32
|
+
export function applyLiveTranscript(state, text, isUser, interim, options) {
|
|
33
|
+
if (!text)
|
|
34
|
+
return state;
|
|
35
|
+
if (interim) {
|
|
36
|
+
return isUser ? { ...state, interimUser: text } : { ...state, interimAgent: text };
|
|
37
|
+
}
|
|
38
|
+
const role = isUser ? 'user' : 'agent';
|
|
39
|
+
const turns = [...state.turns];
|
|
40
|
+
const last = turns.at(-1);
|
|
41
|
+
const interimText = isUser ? state.interimUser : state.interimAgent;
|
|
42
|
+
if (!options?.forceNew && last?.role === role) {
|
|
43
|
+
turns[turns.length - 1] = {
|
|
44
|
+
...last,
|
|
45
|
+
text: appendTurnText(last.text, text),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const committed = options?.forceNew ? text : mergeInterimWithFinal(interimText, text);
|
|
50
|
+
turns.push({
|
|
51
|
+
id: nextTurnId(state),
|
|
52
|
+
role,
|
|
53
|
+
text: committed,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
turns,
|
|
58
|
+
interimUser: isUser ? '' : state.interimUser,
|
|
59
|
+
interimAgent: isUser ? state.interimAgent : '',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Clear streaming partials when a live turn completes or is interrupted. */
|
|
63
|
+
export function clearLiveCaptionInterim(state) {
|
|
64
|
+
if (!state.interimUser && !state.interimAgent)
|
|
65
|
+
return state;
|
|
66
|
+
return { ...state, interimUser: '', interimAgent: '' };
|
|
67
|
+
}
|
|
68
|
+
export function latestLiveCaptionTurnId(state) {
|
|
69
|
+
return state.turns.at(-1)?.id ?? null;
|
|
70
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ToolCallEvent, TurnEventOf } from '@theoremjs/agents';
|
|
2
|
+
/** Whether a mic frame should be sent upstream given mute / socket / barge-in gates. */
|
|
3
|
+
export declare function shouldForwardMicFrame(args: {
|
|
4
|
+
isMuted: boolean;
|
|
5
|
+
socketOpen: boolean;
|
|
6
|
+
modelPlaying: boolean;
|
|
7
|
+
rms: number;
|
|
8
|
+
bargeInRmsWhileSpeaking: number;
|
|
9
|
+
}): boolean;
|
|
10
|
+
/** Classify live evidence transcription into a UI transcript callback payload. */
|
|
11
|
+
export declare function liveTranscriptFromEvidence(event: TurnEventOf<'evidence'>): {
|
|
12
|
+
text: string;
|
|
13
|
+
isUser: boolean;
|
|
14
|
+
interim: boolean;
|
|
15
|
+
} | null;
|
|
16
|
+
export type LiveToolCallDraft = {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
arguments: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Fold a tool turn event into cancelled-id / runnable-call lists. The model's
|
|
23
|
+
* call is runnable; a failure in the same batch (a name or arguments the
|
|
24
|
+
* provider could not use) is not: the session answered the model with it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyLiveToolTurnEvent(tool: ToolCallEvent, accum: {
|
|
27
|
+
cancelledToolIds: Set<string>;
|
|
28
|
+
toolCalls: LiveToolCallDraft[];
|
|
29
|
+
}): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Whether a mic frame should be sent upstream given mute / socket / barge-in gates. */
|
|
2
|
+
export function shouldForwardMicFrame(args) {
|
|
3
|
+
if (args.isMuted || !args.socketOpen)
|
|
4
|
+
return false;
|
|
5
|
+
if (args.modelPlaying && args.rms < args.bargeInRmsWhileSpeaking)
|
|
6
|
+
return false;
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
/** Classify live evidence transcription into a UI transcript callback payload. */
|
|
10
|
+
export function liveTranscriptFromEvidence(event) {
|
|
11
|
+
const { evidence, text } = event;
|
|
12
|
+
if (!text)
|
|
13
|
+
return null;
|
|
14
|
+
if (evidence.kind !== 'input_transcription' && evidence.kind !== 'output_transcription')
|
|
15
|
+
return null;
|
|
16
|
+
return { text, isUser: evidence.kind === 'input_transcription', interim: evidence.interim === true };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Fold a tool turn event into cancelled-id / runnable-call lists. The model's
|
|
20
|
+
* call is runnable; a failure in the same batch (a name or arguments the
|
|
21
|
+
* provider could not use) is not: the session answered the model with it.
|
|
22
|
+
*/
|
|
23
|
+
export function applyLiveToolTurnEvent(tool, accum) {
|
|
24
|
+
if (tool.phase === undefined) {
|
|
25
|
+
accum.toolCalls.push({ id: tool.callId, name: tool.name, arguments: tool.arguments });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (tool.phase === 'cancel') {
|
|
29
|
+
accum.cancelledToolIds.add(tool.callId);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (tool.phase !== 'error')
|
|
33
|
+
return;
|
|
34
|
+
const failed = accum.toolCalls.findIndex((draft) => draft.id === tool.callId);
|
|
35
|
+
if (failed !== -1)
|
|
36
|
+
accum.toolCalls.splice(failed, 1);
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type LiveSessionStatus = 'disconnected' | 'connecting' | 'ready' | 'listening' | 'speaking' | 'working' | 'error';
|
|
2
|
+
export type LiveConnectPhase = 'socket' | 'microphone';
|
|
3
|
+
/** What a live call is doing, for its status line; the UI words it. */
|
|
4
|
+
export type LiveState = 'calling_tool' | 'connecting' | 'requesting_mic' | 'speaking' | 'connected' | 'muted' | 'listening' | 'error' | 'ended';
|
|
5
|
+
export declare function liveState(args: {
|
|
6
|
+
status: LiveSessionStatus;
|
|
7
|
+
connectPhase: LiveConnectPhase | null;
|
|
8
|
+
toolName: string | null;
|
|
9
|
+
isMuted: boolean;
|
|
10
|
+
voiceEnabled?: boolean;
|
|
11
|
+
}): LiveState;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function liveState(args) {
|
|
2
|
+
if (args.toolName)
|
|
3
|
+
return 'calling_tool';
|
|
4
|
+
if (args.connectPhase === 'socket')
|
|
5
|
+
return 'connecting';
|
|
6
|
+
if (args.connectPhase === 'microphone')
|
|
7
|
+
return 'requesting_mic';
|
|
8
|
+
if (args.status === 'speaking')
|
|
9
|
+
return 'speaking';
|
|
10
|
+
if (args.status === 'listening' || args.status === 'ready') {
|
|
11
|
+
if (args.voiceEnabled === false)
|
|
12
|
+
return 'connected';
|
|
13
|
+
return args.isMuted ? 'muted' : 'listening';
|
|
14
|
+
}
|
|
15
|
+
if (args.status === 'connecting')
|
|
16
|
+
return 'connecting';
|
|
17
|
+
if (args.status === 'error')
|
|
18
|
+
return 'error';
|
|
19
|
+
return 'ended';
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolGate } from '@theoremjs/agents/kernel';
|
|
2
|
+
import type { ToolGateResolution } from '../tool-resume.ts';
|
|
3
|
+
/** A gate the user is asked to answer, for the model's call `callId`. */
|
|
4
|
+
export type LiveToolGatePrompt = {
|
|
5
|
+
callId: string;
|
|
6
|
+
toolName: string;
|
|
7
|
+
input: Record<string, unknown>;
|
|
8
|
+
gate: ToolGate;
|
|
9
|
+
};
|
|
10
|
+
/** The user's answer to a live gate, or `withdrawn`: the model cancelled the call, so nothing is asked. */
|
|
11
|
+
export type LiveGateAnswer = ToolGateResolution | 'withdrawn';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type LiveFacingMode = 'user' | 'environment';
|
|
2
|
+
export type LiveVideoCapture = {
|
|
3
|
+
stop: () => void;
|
|
4
|
+
readonly video: HTMLVideoElement;
|
|
5
|
+
facingMode: () => LiveFacingMode;
|
|
6
|
+
flip: () => Promise<LiveFacingMode>;
|
|
7
|
+
};
|
|
8
|
+
export declare function startLiveVideoCapture(onFrame: (base64: string) => void, options?: {
|
|
9
|
+
facingMode?: LiveFacingMode;
|
|
10
|
+
}): Promise<LiveVideoCapture>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { TheoremError } from '@theoremjs/agents';
|
|
2
|
+
const JPEG_QUALITY = 0.62;
|
|
3
|
+
const FRAME_INTERVAL_MS = 400;
|
|
4
|
+
function openCamera(facing) {
|
|
5
|
+
return navigator.mediaDevices.getUserMedia({
|
|
6
|
+
video: { facingMode: { ideal: facing }, width: { ideal: 640 }, height: { ideal: 480 } },
|
|
7
|
+
audio: false,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export function startLiveVideoCapture(onFrame, options) {
|
|
11
|
+
const initialFacing = options?.facingMode ?? 'user';
|
|
12
|
+
return openCamera(initialFacing).then((stream) => {
|
|
13
|
+
const video = document.createElement('video');
|
|
14
|
+
video.srcObject = stream;
|
|
15
|
+
video.muted = true;
|
|
16
|
+
video.playsInline = true;
|
|
17
|
+
video.setAttribute('playsinline', '');
|
|
18
|
+
const canvas = document.createElement('canvas');
|
|
19
|
+
const context = canvas.getContext('2d');
|
|
20
|
+
if (!context) {
|
|
21
|
+
for (const track of stream.getTracks())
|
|
22
|
+
track.stop();
|
|
23
|
+
throw new TheoremError('unsupported', 'no 2d canvas for live video'); // lexicon-exempt: internal diagnostic
|
|
24
|
+
}
|
|
25
|
+
let timer = null;
|
|
26
|
+
let stopped = false;
|
|
27
|
+
let state = { stream, facing: initialFacing };
|
|
28
|
+
const capture = () => {
|
|
29
|
+
if (stopped || video.videoWidth === 0)
|
|
30
|
+
return;
|
|
31
|
+
canvas.width = video.videoWidth;
|
|
32
|
+
canvas.height = video.videoHeight;
|
|
33
|
+
context.drawImage(video, 0, 0);
|
|
34
|
+
const dataUrl = canvas.toDataURL('image/jpeg', JPEG_QUALITY);
|
|
35
|
+
const base64 = dataUrl.split(',')[1];
|
|
36
|
+
if (base64)
|
|
37
|
+
onFrame(base64);
|
|
38
|
+
};
|
|
39
|
+
const startTimer = () => {
|
|
40
|
+
if (timer || stopped)
|
|
41
|
+
return;
|
|
42
|
+
timer = setInterval(capture, FRAME_INTERVAL_MS);
|
|
43
|
+
};
|
|
44
|
+
void video.play().then(startTimer);
|
|
45
|
+
return {
|
|
46
|
+
video,
|
|
47
|
+
facingMode: () => state.facing,
|
|
48
|
+
flip: async () => {
|
|
49
|
+
if (stopped)
|
|
50
|
+
return state.facing;
|
|
51
|
+
const nextFacing = state.facing === 'user' ? 'environment' : 'user';
|
|
52
|
+
const nextStream = await openCamera(nextFacing);
|
|
53
|
+
for (const track of state.stream.getTracks())
|
|
54
|
+
track.stop();
|
|
55
|
+
state = { stream: nextStream, facing: nextFacing };
|
|
56
|
+
video.srcObject = nextStream;
|
|
57
|
+
await video.play();
|
|
58
|
+
return nextFacing;
|
|
59
|
+
},
|
|
60
|
+
stop: () => {
|
|
61
|
+
stopped = true;
|
|
62
|
+
if (timer)
|
|
63
|
+
clearInterval(timer);
|
|
64
|
+
timer = null;
|
|
65
|
+
video.srcObject = null;
|
|
66
|
+
for (const track of state.stream.getTracks())
|
|
67
|
+
track.stop();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ExecuteToolOnRelay } from '../live-messages.ts';
|
|
2
|
+
import type { LiveGateAnswer, LiveToolGatePrompt } from './live-tool.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Live provider tool-call handler: run the model's call on the relay; at a
|
|
5
|
+
* gate, ask the user and send their decision. The session answers the model
|
|
6
|
+
* however the call ends; its tool events tell the user. A call the model
|
|
7
|
+
* cancels while its gate is open is gone: nothing is sent.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runLiveToolCall(args: {
|
|
10
|
+
executeToolOnRelay: ExecuteToolOnRelay;
|
|
11
|
+
name: string;
|
|
12
|
+
toolArgs: Record<string, unknown>;
|
|
13
|
+
callId: string;
|
|
14
|
+
sessionPermissions: string[];
|
|
15
|
+
setSessionPermissions: (next: string[]) => void;
|
|
16
|
+
waitForGateDecision: (prompt: LiveToolGatePrompt) => Promise<LiveGateAnswer>;
|
|
17
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { continueGatedToolInvocation } from "../tool-resume.js";
|
|
2
|
+
/**
|
|
3
|
+
* Live provider tool-call handler: run the model's call on the relay; at a
|
|
4
|
+
* gate, ask the user and send their decision. The session answers the model
|
|
5
|
+
* however the call ends; its tool events tell the user. A call the model
|
|
6
|
+
* cancels while its gate is open is gone: nothing is sent.
|
|
7
|
+
*/
|
|
8
|
+
export async function runLiveToolCall(args) {
|
|
9
|
+
const { executeToolOnRelay, name, toolArgs, callId } = args;
|
|
10
|
+
let sessionPermissions = args.sessionPermissions;
|
|
11
|
+
let step = await executeToolOnRelay({ callId });
|
|
12
|
+
while (step.status === 'gated') {
|
|
13
|
+
const { gate } = step;
|
|
14
|
+
const resolution = await args.waitForGateDecision({ callId, toolName: name, input: toolArgs, gate });
|
|
15
|
+
if (resolution === 'withdrawn')
|
|
16
|
+
return;
|
|
17
|
+
const reply = continueGatedToolInvocation({ toolName: name, gate, sessionPermissions, resolution });
|
|
18
|
+
if (reply.decision === 'deny') {
|
|
19
|
+
// The session settles the refusal; its tool event tells the user.
|
|
20
|
+
await executeToolOnRelay({ callId, decision: 'deny' });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
sessionPermissions = reply.sessionPermissions;
|
|
24
|
+
args.setSessionPermissions(reply.sessionPermissions);
|
|
25
|
+
// Signed in: a typed key goes once, with the approval; after an OAuth callback there is none.
|
|
26
|
+
step = await executeToolOnRelay({
|
|
27
|
+
callId,
|
|
28
|
+
decision: 'approve',
|
|
29
|
+
...(reply.secret !== undefined ? { secret: reply.secret } : {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure client-side SDK for THEOREM Gemini 3.1 Flash Live sessions over WebSocket relay.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Bidirectional WebSocket connection to `/api/live/relay`.
|
|
6
|
+
* - 16-bit 16kHz PCM microphone audio recording & streaming.
|
|
7
|
+
* - Gapless 24kHz PCM / WAV model voice playback scheduling.
|
|
8
|
+
* - Barge-in interruption cancellation (instant audio queue flush).
|
|
9
|
+
* - Quiet-mic gate while model audio plays (reduces false barge-in from speaker bleed).
|
|
10
|
+
* - UI tool execution and response routing.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { type SessionEventOf, type TraceRecord } from '@theoremjs/agents';
|
|
15
|
+
import type { LiveConnectPhase, LiveSessionStatus } from './live/live-state.ts';
|
|
16
|
+
import { type ExecuteToolOnRelay, type LiveToolStep } from './live-messages.ts';
|
|
17
|
+
import { type ClientTurnEvent } from './transport.ts';
|
|
18
|
+
export type { LiveConnection, LiveSocket } from './live-messages.ts';
|
|
19
|
+
import type { LiveSocket } from './live-messages.ts';
|
|
20
|
+
export interface LiveClientOptions {
|
|
21
|
+
profile?: string;
|
|
22
|
+
/** JSON sent before anything else, as the socket opens. */
|
|
23
|
+
openMessage?: Record<string, unknown>;
|
|
24
|
+
relayUrl?: string;
|
|
25
|
+
/** In-process connections reuse the same microphone, playback and tool client. */
|
|
26
|
+
createSocket?: () => LiveSocket;
|
|
27
|
+
/** When false, skip microphone capture; session still receives model audio. */
|
|
28
|
+
voiceIngress?: boolean;
|
|
29
|
+
onStatusChange?: (status: LiveSessionStatus) => void;
|
|
30
|
+
onConnectPhase?: (phase: LiveConnectPhase | null) => void;
|
|
31
|
+
onTranscript?: (text: string, isUser: boolean, meta?: {
|
|
32
|
+
interim?: boolean;
|
|
33
|
+
}) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Every event the session sends, `unsupported` for a kind this client does
|
|
36
|
+
* not know, and `malformed` for one that failed its check (left out; the call goes on).
|
|
37
|
+
*/
|
|
38
|
+
onTurnEvent?: (event: ClientTurnEvent) => void;
|
|
39
|
+
/** A trace record the session wrote, when the relay delivers them. */
|
|
40
|
+
onTrace?: (record: TraceRecord) => void;
|
|
41
|
+
/** A failure, typed by kind; word it with `clientFailure` and the interface's `lexicon`. */
|
|
42
|
+
onError?: (error: Error) => void;
|
|
43
|
+
/** Provider signalled the upstream session is draining (e.g. goAway). */
|
|
44
|
+
onSessionClosing?: (timeLeftMs?: number) => void;
|
|
45
|
+
/**
|
|
46
|
+
* The provider ended the session after warning it would: not a failure.
|
|
47
|
+
* `session.message` is the user's line; `session.ended` the close, for the builder.
|
|
48
|
+
*/
|
|
49
|
+
onSessionEnded?: (session: SessionEventOf<'ended'>) => void;
|
|
50
|
+
/**
|
|
51
|
+
* The model called a tool. The host runs it with `executeToolOnRelay` and
|
|
52
|
+
* answers its gates; the session answers the model. A throw reaches `onError`.
|
|
53
|
+
*/
|
|
54
|
+
onToolCall: (name: string, args: Record<string, unknown>, meta: {
|
|
55
|
+
callId: string;
|
|
56
|
+
}) => Promise<void>;
|
|
57
|
+
/** The relay opened the call: the id it gave the call, and the profile it runs. */
|
|
58
|
+
onSessionReady?: (info: {
|
|
59
|
+
sessionId?: string;
|
|
60
|
+
profile?: string;
|
|
61
|
+
}) => void;
|
|
62
|
+
onVolumeLevel?: (level: number, isUser: boolean) => void;
|
|
63
|
+
}
|
|
64
|
+
export declare class LiveSessionClient {
|
|
65
|
+
private ws;
|
|
66
|
+
private audioContext;
|
|
67
|
+
private micStream;
|
|
68
|
+
private micSource;
|
|
69
|
+
private micWorklet;
|
|
70
|
+
private micWorkletModuleLoaded;
|
|
71
|
+
private playbackNodes;
|
|
72
|
+
private playbackBus;
|
|
73
|
+
private playbackAnalyser;
|
|
74
|
+
private playbackMeterFrame;
|
|
75
|
+
private playbackMeterBuffer;
|
|
76
|
+
private nextPlaybackTime;
|
|
77
|
+
private status;
|
|
78
|
+
/** Server reported background work (reasoning / async tools) still in flight. */
|
|
79
|
+
private serverWorking;
|
|
80
|
+
private micActivating;
|
|
81
|
+
private connectTimeout;
|
|
82
|
+
private isMuted;
|
|
83
|
+
private options;
|
|
84
|
+
/** Serialize control-plane handling (tools / status) without waiting on audio decode. */
|
|
85
|
+
private inboundChain;
|
|
86
|
+
/** Ordered model-audio playback queue (separate so tools are not stuck behind decode). */
|
|
87
|
+
private audioChain;
|
|
88
|
+
/**
|
|
89
|
+
* Tool calls, one at a time in the order the model made them. Apart from
|
|
90
|
+
* `inboundChain`: a call waits on the relay's reply, which arrives inbound.
|
|
91
|
+
*/
|
|
92
|
+
private toolChain;
|
|
93
|
+
/** Bumped on barge-in / cancel so stale audioChain work is skipped. */
|
|
94
|
+
private audioEpoch;
|
|
95
|
+
private pendingExecuteResults;
|
|
96
|
+
constructor(options: LiveClientOptions);
|
|
97
|
+
private setStatus;
|
|
98
|
+
private setConnectPhase;
|
|
99
|
+
private clearConnectTimeout;
|
|
100
|
+
private detachWebSocket;
|
|
101
|
+
private teardownConnection;
|
|
102
|
+
/** End the session on `error`: a failed connect or a lost socket. */
|
|
103
|
+
private failSession;
|
|
104
|
+
private createSocket;
|
|
105
|
+
connect(): Promise<void>;
|
|
106
|
+
private ensureAudioContext;
|
|
107
|
+
private activateMicrophone;
|
|
108
|
+
private sendMicFrame;
|
|
109
|
+
private reportMicVolume;
|
|
110
|
+
private forwardMicBuffer;
|
|
111
|
+
private setupMicrophonePipeline;
|
|
112
|
+
private ensurePlaybackBus;
|
|
113
|
+
private stopPlaybackMeter;
|
|
114
|
+
private startPlaybackMeter;
|
|
115
|
+
private processServerEnvelope;
|
|
116
|
+
private enqueueServerMessage;
|
|
117
|
+
private handleReadyEnvelope;
|
|
118
|
+
private handleExecuteToolResultEnvelope;
|
|
119
|
+
private tryHandleControlEnvelope;
|
|
120
|
+
private collectInboundTurn;
|
|
121
|
+
private processTurnEvent;
|
|
122
|
+
private handleEvidenceTurnEvent;
|
|
123
|
+
private handleSessionTurnEvent;
|
|
124
|
+
/** Hand the session's closing and end to the host; `true` when it was one of them. */
|
|
125
|
+
private notifySessionLifecycle;
|
|
126
|
+
/** Status to settle into once model speech stops. */
|
|
127
|
+
private restingStatus;
|
|
128
|
+
private collectMediaTurnEvent;
|
|
129
|
+
private handleDoneTurnEvent;
|
|
130
|
+
private scheduleMediaChunks;
|
|
131
|
+
/** The relay runs `LiveSession.executeTool`; see `ExecuteToolOnRelay`. */
|
|
132
|
+
executeToolOnRelay(args: Parameters<ExecuteToolOnRelay>[0]): Promise<LiveToolStep>;
|
|
133
|
+
private enqueueAudioChunk;
|
|
134
|
+
cancelPlayback(): void;
|
|
135
|
+
toggleMute(): boolean;
|
|
136
|
+
sendText(text: string): void;
|
|
137
|
+
sendContext(text: string): void;
|
|
138
|
+
sendVideo(data: string, mimeType?: string): void;
|
|
139
|
+
disconnect(): void;
|
|
140
|
+
private cleanupAudio;
|
|
141
|
+
}
|