@alia.onl/sdk 15.7.0 → 19.0.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/CHANGELOG.md +255 -12
- package/README.md +241 -155
- package/package.json +17 -5
- package/src/animations.ts +0 -1
- package/src/components/AliaAskProvider.tsx +127 -0
- package/src/components/AliaChatContent.tsx +604 -392
- package/src/components/AliaChatHeader.tsx +14 -38
- package/src/components/AliaChatScreen.tsx +56 -31
- package/src/components/AliaChatSheet.tsx +110 -36
- package/src/components/AliaComposer.tsx +1 -1
- package/src/components/AliaParticipants.tsx +202 -0
- package/src/components/AliaParticipantsHeader.tsx +60 -0
- package/src/components/AliaSharedConversation.tsx +86 -0
- package/src/components/AliaWelcomeMessage.tsx +18 -5
- package/src/components/Image.tsx +12 -0
- package/src/components/Markdown.tsx +498 -14
- package/src/components/PlanPreviewCard.tsx +7 -49
- package/src/components/ReplyMarkdown.tsx +359 -0
- package/src/components/RichBlocks.tsx +195 -0
- package/src/components/canvas/CanvasComponent.tsx +59 -0
- package/src/components/canvas/ChartRenderer.tsx +73 -0
- package/src/components/canvas/CodeRenderer.tsx +24 -0
- package/src/components/canvas/FormRenderer.tsx +101 -0
- package/src/components/canvas/MarkdownRenderer.tsx +14 -0
- package/src/components/canvas/TableRenderer.tsx +47 -0
- package/src/components/thought/FilesAndSources.tsx +120 -0
- package/src/components/thought/ThoughtPanel.tsx +417 -0
- package/src/components/thought/ToolStep.tsx +141 -0
- package/src/components/thread/AgentResultCard.tsx +125 -0
- package/src/components/thread/AgentTaskCard.tsx +127 -0
- package/src/components/thread/AliaThread.tsx +401 -0
- package/src/components/thread/MessageBlockBoundary.tsx +78 -0
- package/src/components/thread/NewConversationOffer.tsx +54 -0
- package/src/components/thread/ThreadRow.tsx +458 -0
- package/src/components/thread/ThreadSearch.tsx +129 -0
- package/src/components/thread/ToolResultCard.tsx +254 -0
- package/src/components/thread/TurnMenu.tsx +54 -0
- package/src/components/thread/TurnStatusLine.tsx +34 -0
- package/src/components/voice/AliaVoiceCall.tsx +21 -0
- package/src/components/voice/VoiceControls.tsx +2 -28
- package/src/components/voice/VoiceSession.tsx +39 -11
- package/src/csp.ts +50 -0
- package/src/hooks/useAliaActivity.ts +3 -3
- package/src/hooks/useAtBottom.ts +51 -0
- package/src/hooks/useAttachMenu.ts +94 -0
- package/src/hooks/useChatComposer.ts +299 -0
- package/src/hooks/useChatVoice.ts +129 -0
- package/src/hooks/useDictation.ts +2 -3
- package/src/hooks/useDocumentPicker.ts +3 -29
- package/src/hooks/useFailedTurnToast.ts +43 -0
- package/src/hooks/useMessageVote.ts +50 -0
- package/src/hooks/useOpenThought.ts +79 -0
- package/src/hooks/usePendingQuestions.ts +2 -2
- package/src/hooks/useReadAloud.ts +92 -0
- package/src/hooks/useShareHandler.ts +24 -0
- package/src/hooks/useSoundEffects.ts +1 -1
- package/src/hooks/useTTS.ts +2 -3
- package/src/hooks/useThoughtScope.ts +59 -0
- package/src/hooks/useThreadRows.ts +145 -0
- package/src/hooks/useThreadView.ts +68 -0
- package/src/hooks/useTimelineWindow.ts +256 -0
- package/src/hooks/useVoiceLauncher.ts +180 -0
- package/src/hooks/useVoiceRoom.ts +2 -6
- package/src/index.ts +43 -29
- package/src/lib/activity-store.ts +1 -1
- package/src/lib/add-menu.ts +7 -0
- package/src/lib/call-thread.ts +33 -0
- package/src/lib/character-runtime.ts +6 -4
- package/src/lib/citations.ts +79 -2
- package/src/lib/clipboard.ts +23 -0
- package/src/lib/content-utils.ts +10 -0
- package/src/lib/copy-en.ts +284 -0
- package/src/lib/copy.ts +32 -0
- package/src/lib/document-picker.ts +31 -0
- package/src/lib/flights.ts +22 -13
- package/src/lib/focus-return.ts +66 -0
- package/src/lib/keyboard.native.tsx +19 -0
- package/src/lib/keyboard.tsx +9 -0
- package/src/lib/labels.tsx +69 -17
- package/src/lib/origins.ts +15 -0
- package/src/lib/participants.ts +86 -0
- package/src/lib/power-level-options.ts +66 -0
- package/src/lib/questions.ts +2 -2
- package/src/lib/task-labels.ts +4 -4
- package/src/lib/tool-call-status.ts +1 -1
- package/src/lib/tool-icons.ts +36 -0
- package/src/lib/tool-renderers.tsx +48 -0
- package/src/lib/turn-state.ts +2 -2
- package/src/lib/voice-audio-engine.ts +14 -158
- package/src/lib/voice-call-request.ts +49 -0
- package/src/lib/voice-call.ts +76 -34
- package/src/lib/voice-controls-labels.ts +31 -0
- package/src/lib/voice-protocol.ts +32 -14
- package/src/lib/voice-worklets.ts +19 -0
- package/src/lib/work-log.ts +6 -6
- package/src/runtime/agents.ts +151 -0
- package/src/runtime/api.ts +123 -0
- package/src/runtime/attachment-utils.ts +96 -0
- package/src/runtime/composer-draft-store.ts +270 -0
- package/src/runtime/config.tsx +186 -0
- package/src/runtime/conversation-store.ts +49 -0
- package/src/runtime/conversations.ts +557 -0
- package/src/runtime/error-utils.ts +104 -0
- package/src/runtime/host-tools.ts +140 -0
- package/src/runtime/index.ts +113 -0
- package/src/runtime/message-history.ts +124 -0
- package/src/runtime/model.ts +39 -0
- package/src/runtime/oxy-files.ts +37 -0
- package/src/runtime/power-levels.ts +66 -0
- package/src/runtime/query-keys.ts +34 -0
- package/src/runtime/random-uuid.ts +25 -0
- package/src/runtime/routes.ts +51 -0
- package/src/runtime/sharing.ts +165 -0
- package/src/runtime/sse-frame-reader.ts +88 -0
- package/src/runtime/stream-events.ts +146 -0
- package/src/runtime/stream-outcome.ts +21 -0
- package/src/runtime/thought-store.ts +103 -0
- package/src/runtime/thread/agent-activity.ts +261 -0
- package/src/runtime/thread/automation-labels.ts +79 -0
- package/src/runtime/thread/conversation-export.ts +135 -0
- package/src/runtime/thread/message-days.ts +120 -0
- package/src/runtime/thread/message-processor.ts +93 -0
- package/src/runtime/thread/task-utils.ts +11 -0
- package/src/runtime/thread/thought-utils.ts +752 -0
- package/src/runtime/thread/thread-history.ts +139 -0
- package/src/runtime/thread/timeline.ts +202 -0
- package/src/runtime/thread/title-tags.ts +8 -0
- package/src/runtime/thread/tool-cards.ts +383 -0
- package/src/runtime/turn-failure.ts +108 -0
- package/src/runtime/types.ts +148 -0
- package/src/runtime/usage-limit-error.ts +44 -0
- package/src/runtime/use-agent-activity.ts +80 -0
- package/src/runtime/use-conversation-stream.ts +1427 -0
- package/src/runtime/use-conversation-thread.ts +556 -0
- package/src/runtime/use-start-conversation.ts +107 -0
- package/src/runtime/use-thread-history.ts +64 -0
- package/src/runtime/use-thread-search.ts +95 -0
- package/src/runtime/use-turn-edit.ts +99 -0
- package/src/tools.ts +2 -2
- package/src/types.ts +18 -47
- package/worklets/voice-processors.js +152 -0
- package/src/components/AliaChatMessageList.tsx +0 -532
- package/src/components/ThinkingBlock.tsx +0 -43
- package/src/hooks/useAliaChat.ts +0 -573
- package/src/lib/api-url.ts +0 -2
- package/src/lib/catalogue.ts +0 -236
- package/src/lib/chat-stream.ts +0 -655
- package/src/lib/chat-transport.ts +0 -98
- package/src/lib/delegation.ts +0 -34
- package/src/lib/work-log-copy.ts +0 -61
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
|
|
3
|
+
/** A call somebody asked for, waiting for the screen that shows its conversation. */
|
|
4
|
+
export interface VoiceCallRequest {
|
|
5
|
+
readonly conversationId: string;
|
|
6
|
+
readonly accountId: string | null;
|
|
7
|
+
readonly at: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** How long a request waits for its screen. A navigation that never arrives must not open a call later. */
|
|
11
|
+
export const CALL_REQUEST_TTL_MS = 15_000;
|
|
12
|
+
|
|
13
|
+
interface VoiceCallRequestStore {
|
|
14
|
+
request: VoiceCallRequest | null;
|
|
15
|
+
ask: (conversationId: string, accountId: string | null) => void;
|
|
16
|
+
clear: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Where a call is asked for, apart from where it opens.
|
|
21
|
+
*
|
|
22
|
+
* Starting a call and showing it are the same thing when the conversation is already on screen,
|
|
23
|
+
* and two screens apart when it is not: a new-chat route makes the conversation and the host
|
|
24
|
+
* navigates to it. Both go the same way. The launcher AND the screen of the conversation meet
|
|
25
|
+
* here — it asks, and whichever launcher is mounted for that conversation (that account, in time)
|
|
26
|
+
* takes the request and opens the call. The sheet, the screen and the app share this one path.
|
|
27
|
+
*/
|
|
28
|
+
export const useVoiceCallRequests = create<VoiceCallRequestStore>((set) => ({
|
|
29
|
+
request: null,
|
|
30
|
+
ask: (conversationId, accountId) =>
|
|
31
|
+
set({ request: { conversationId, accountId, at: Date.now() } }),
|
|
32
|
+
clear: () => set({ request: null }),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
/** Whether `request` is for this conversation and account, and still worth opening. */
|
|
36
|
+
export function isRequestFor(
|
|
37
|
+
request: VoiceCallRequest | null,
|
|
38
|
+
conversationId: string | undefined,
|
|
39
|
+
accountId: string | null,
|
|
40
|
+
now = Date.now(),
|
|
41
|
+
): boolean {
|
|
42
|
+
return (
|
|
43
|
+
request !== null &&
|
|
44
|
+
conversationId !== undefined &&
|
|
45
|
+
request.conversationId === conversationId &&
|
|
46
|
+
request.accountId === accountId &&
|
|
47
|
+
now - request.at <= CALL_REQUEST_TTL_MS
|
|
48
|
+
);
|
|
49
|
+
}
|
package/src/lib/voice-call.ts
CHANGED
|
@@ -88,8 +88,6 @@ export interface VoiceCallOptions {
|
|
|
88
88
|
readonly onChange: (snapshot: VoiceCallSnapshot) => void;
|
|
89
89
|
/** Alia stored messages of the call in the conversation. */
|
|
90
90
|
readonly onMessagesSaved?: (conversationId: string, messageIds: readonly string[]) => void;
|
|
91
|
-
/** A tool filed an action for the person's OK; the card is in the conversation. */
|
|
92
|
-
readonly onApprovalRequested?: (toolName: string) => void;
|
|
93
91
|
readonly tokenRefreshMs?: number;
|
|
94
92
|
readonly resumeRetryMs?: number;
|
|
95
93
|
}
|
|
@@ -113,6 +111,13 @@ export class VoiceCall {
|
|
|
113
111
|
private readonly options: VoiceCallOptions;
|
|
114
112
|
private state: VoiceCallState = { kind: 'idle' };
|
|
115
113
|
private messages: VoiceMessage[] = [];
|
|
114
|
+
/**
|
|
115
|
+
* Each message's text by the item it was spoken in, in the order the items
|
|
116
|
+
* began: an assistant turn that called a tool is one message of two items.
|
|
117
|
+
*/
|
|
118
|
+
private readonly itemTexts = new Map<string, Map<string, string>>();
|
|
119
|
+
/** The message each item belongs to. */
|
|
120
|
+
private readonly messageOfItem = new Map<string, string>();
|
|
116
121
|
private muted = false;
|
|
117
122
|
private endingSoonMs: number | null = null;
|
|
118
123
|
private model: VoiceCallSnapshot['model'] = null;
|
|
@@ -362,10 +367,10 @@ export class VoiceCall {
|
|
|
362
367
|
this.setState({ kind: 'thinking' });
|
|
363
368
|
return;
|
|
364
369
|
case 'transcript.delta':
|
|
365
|
-
this.upsertMessage(event
|
|
370
|
+
this.upsertMessage(event, (previous) => previous + event.text, true);
|
|
366
371
|
return;
|
|
367
372
|
case 'transcript.done':
|
|
368
|
-
this.upsertMessage(event
|
|
373
|
+
this.upsertMessage(event, () => event.text, false);
|
|
369
374
|
return;
|
|
370
375
|
case 'assistant.audio_done':
|
|
371
376
|
return;
|
|
@@ -379,11 +384,13 @@ export class VoiceCall {
|
|
|
379
384
|
if (this.state.kind === 'thinking' && this.playingItem === null) this.afterSpeech();
|
|
380
385
|
return;
|
|
381
386
|
case 'tool.started':
|
|
387
|
+
this.recordTool(event, 'call');
|
|
382
388
|
this.activeTools.set(event.callId, event.name);
|
|
383
389
|
this.toolRan = true;
|
|
384
390
|
if (LIVE.has(this.state.kind)) this.setState({ kind: 'using-tool', tool: event.name });
|
|
385
391
|
return;
|
|
386
392
|
case 'tool.finished': {
|
|
393
|
+
this.recordTool(event, 'result');
|
|
387
394
|
this.activeTools.delete(event.callId);
|
|
388
395
|
if (this.state.kind !== 'using-tool') return;
|
|
389
396
|
const still = [...this.activeTools.values()].at(-1);
|
|
@@ -392,9 +399,6 @@ export class VoiceCall {
|
|
|
392
399
|
);
|
|
393
400
|
return;
|
|
394
401
|
}
|
|
395
|
-
case 'approval.requested':
|
|
396
|
-
this.options.onApprovalRequested?.(event.toolName);
|
|
397
|
-
return;
|
|
398
402
|
case 'messages.saved':
|
|
399
403
|
this.options.onMessagesSaved?.(event.conversationId, event.messageIds);
|
|
400
404
|
return;
|
|
@@ -455,41 +459,79 @@ export class VoiceCall {
|
|
|
455
459
|
this.finish({ kind: 'ended', reason });
|
|
456
460
|
}
|
|
457
461
|
|
|
462
|
+
/**
|
|
463
|
+
* One message per `messageId`, the id the server stores and announces it
|
|
464
|
+
* under, so the stored copy that reaches the thread is this message, not a
|
|
465
|
+
* second one. Its text is its items' texts in order.
|
|
466
|
+
*/
|
|
458
467
|
private upsertMessage(
|
|
459
|
-
role: 'user' | 'assistant',
|
|
460
|
-
|
|
461
|
-
content: (previous: string) => string,
|
|
468
|
+
event: { role: 'user' | 'assistant'; messageId: string; itemId: string },
|
|
469
|
+
text: (previous: string) => string,
|
|
462
470
|
isStreaming: boolean,
|
|
463
471
|
): void {
|
|
464
|
-
const
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
472
|
+
const { messageId, itemId } = event;
|
|
473
|
+
const items = this.itemTexts.get(messageId) ?? new Map<string, string>();
|
|
474
|
+
this.itemTexts.set(messageId, items);
|
|
475
|
+
this.messageOfItem.set(itemId, messageId);
|
|
476
|
+
items.set(itemId, text(items.get(itemId) ?? ''));
|
|
477
|
+
const content = [...items.values()].filter((part) => part !== '').join(' ');
|
|
478
|
+
this.writeMessage(messageId, event.role, (message) => ({ ...message, content, isStreaming }));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* A tool the answer ran, inside the answer's message, by name and state: the
|
|
483
|
+
* device never receives its arguments or result, and the stored message that
|
|
484
|
+
* carries them replaces this one once it is saved.
|
|
485
|
+
*/
|
|
486
|
+
private recordTool(
|
|
487
|
+
event: { messageId: string; callId: string; name: string },
|
|
488
|
+
state: 'call' | 'result',
|
|
489
|
+
): void {
|
|
490
|
+
this.writeMessage(event.messageId, 'assistant', (message) => {
|
|
491
|
+
const invocation = { toolCallId: event.callId, toolName: event.name, state };
|
|
492
|
+
const tools = message.toolInvocations ?? [];
|
|
493
|
+
const index = tools.findIndex((tool) => tool.toolCallId === event.callId);
|
|
494
|
+
return {
|
|
495
|
+
...message,
|
|
496
|
+
toolInvocations:
|
|
497
|
+
index === -1
|
|
498
|
+
? [...tools, invocation]
|
|
499
|
+
: tools.map((tool, at) => (at === index ? invocation : tool)),
|
|
500
|
+
};
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private writeMessage(
|
|
505
|
+
messageId: string,
|
|
506
|
+
role: 'user' | 'assistant',
|
|
507
|
+
change: (message: VoiceMessage) => VoiceMessage,
|
|
508
|
+
): void {
|
|
509
|
+
const index = this.messages.findIndex((message) => message.id === messageId);
|
|
510
|
+
const current: VoiceMessage =
|
|
511
|
+
index === -1
|
|
512
|
+
? {
|
|
513
|
+
id: messageId,
|
|
514
|
+
role,
|
|
515
|
+
...(role === 'assistant' ? { speaker: 'primary' as const } : {}),
|
|
516
|
+
content: '',
|
|
517
|
+
timestamp: Date.now(),
|
|
518
|
+
isStreaming: false,
|
|
519
|
+
}
|
|
520
|
+
: this.messages[index]!;
|
|
521
|
+
const next = change(current);
|
|
522
|
+
this.messages =
|
|
523
|
+
index === -1
|
|
524
|
+
? [...this.messages, next]
|
|
525
|
+
: this.messages.map((message, at) => (at === index ? next : message));
|
|
484
526
|
this.emit();
|
|
485
527
|
}
|
|
486
528
|
|
|
487
529
|
private settleStreaming(itemId: string | null): void {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (!this.messages.some((message) => message.id ===
|
|
530
|
+
const messageId = itemId === null ? undefined : this.messageOfItem.get(itemId);
|
|
531
|
+
if (messageId === undefined) return;
|
|
532
|
+
if (!this.messages.some((message) => message.id === messageId && message.isStreaming)) return;
|
|
491
533
|
this.messages = this.messages.map((message) =>
|
|
492
|
-
message.id ===
|
|
534
|
+
message.id === messageId ? { ...message, isStreaming: false } : message,
|
|
493
535
|
);
|
|
494
536
|
this.emit();
|
|
495
537
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The call bar's words, apart from the component so the chat's labels can hold them without
|
|
2
|
+
// reaching React Native.
|
|
3
|
+
|
|
4
|
+
/** Every word the bar shows or announces. An app in another language passes its own. */
|
|
5
|
+
export interface VoiceControlsLabels {
|
|
6
|
+
connecting: string;
|
|
7
|
+
reconnecting: string;
|
|
8
|
+
connected: string;
|
|
9
|
+
listening: string;
|
|
10
|
+
muted: string;
|
|
11
|
+
thinking: string;
|
|
12
|
+
usingTool: string;
|
|
13
|
+
speaking: string;
|
|
14
|
+
mute: string;
|
|
15
|
+
unmute: string;
|
|
16
|
+
end: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const VOICE_CONTROLS_LABELS: VoiceControlsLabels = {
|
|
20
|
+
connecting: 'Connecting...',
|
|
21
|
+
reconnecting: 'Reconnecting...',
|
|
22
|
+
connected: 'Connected',
|
|
23
|
+
listening: 'Listening...',
|
|
24
|
+
muted: 'Muted',
|
|
25
|
+
thinking: 'Thinking...',
|
|
26
|
+
usingTool: 'Working on it...',
|
|
27
|
+
speaking: 'Speaking...',
|
|
28
|
+
mute: 'Mute',
|
|
29
|
+
unmute: 'Unmute',
|
|
30
|
+
end: 'End',
|
|
31
|
+
};
|
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
* `session.accepted` carries `seq`; a resume replays from `afterSeq`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* 2: every transcript and tool event names `messageId`, the message it belongs
|
|
18
|
+
* to as it is stored.
|
|
19
|
+
*/
|
|
20
|
+
export const VOICE_PROTOCOL_VERSION = 2;
|
|
17
21
|
|
|
18
22
|
export const VOICE_SESSION_PATH = '/v1/voice/session';
|
|
19
23
|
|
|
@@ -107,17 +111,20 @@ export type VoiceServerEvent =
|
|
|
107
111
|
| { type: 'input.ready' }
|
|
108
112
|
| { type: 'user.speech_started'; itemId: string }
|
|
109
113
|
| { type: 'user.speech_stopped'; itemId: string }
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
/**
|
|
115
|
+
* `messageId` is the message the text belongs to, the id it is stored and
|
|
116
|
+
* announced under: an assistant turn's items are one message.
|
|
117
|
+
*/
|
|
118
|
+
| { type: 'transcript.delta'; role: VoiceRole; messageId: string; itemId: string; text: string }
|
|
119
|
+
| { type: 'transcript.done'; role: VoiceRole; messageId: string; itemId: string; text: string }
|
|
112
120
|
| { type: 'assistant.audio_done'; itemId: string }
|
|
113
121
|
| {
|
|
114
122
|
type: 'assistant.done';
|
|
115
123
|
itemId: string | null;
|
|
116
124
|
status: 'completed' | 'cancelled' | 'incomplete' | 'failed';
|
|
117
125
|
}
|
|
118
|
-
| { type: 'tool.started'; callId: string; name: string }
|
|
119
|
-
| { type: 'tool.finished'; callId: string; name: string; ok: boolean }
|
|
120
|
-
| { type: 'approval.requested'; toolName: string }
|
|
126
|
+
| { type: 'tool.started'; messageId: string; callId: string; name: string }
|
|
127
|
+
| { type: 'tool.finished'; messageId: string; callId: string; name: string; ok: boolean }
|
|
121
128
|
| { type: 'messages.saved'; conversationId: string; messageIds: string[] }
|
|
122
129
|
| { type: 'session.warning'; code: 'ending-soon'; remainingMs: number }
|
|
123
130
|
| { type: 'session.reconnecting' }
|
|
@@ -196,9 +203,17 @@ export function parseVoiceServerEvent(raw: string): ReceivedVoiceEvent | null {
|
|
|
196
203
|
case 'transcript.delta':
|
|
197
204
|
case 'transcript.done':
|
|
198
205
|
return (event.role === 'user' || event.role === 'assistant') &&
|
|
206
|
+
isString(event.messageId) &&
|
|
199
207
|
isString(event.itemId) &&
|
|
200
208
|
isString(event.text)
|
|
201
|
-
? {
|
|
209
|
+
? {
|
|
210
|
+
type: event.type,
|
|
211
|
+
role: event.role,
|
|
212
|
+
messageId: event.messageId,
|
|
213
|
+
itemId: event.itemId,
|
|
214
|
+
text: event.text,
|
|
215
|
+
...seq,
|
|
216
|
+
}
|
|
202
217
|
: null;
|
|
203
218
|
case 'assistant.done':
|
|
204
219
|
return event.itemId === null || isString(event.itemId)
|
|
@@ -215,23 +230,26 @@ export function parseVoiceServerEvent(raw: string): ReceivedVoiceEvent | null {
|
|
|
215
230
|
}
|
|
216
231
|
: null;
|
|
217
232
|
case 'tool.started':
|
|
218
|
-
return isString(event.callId) && isString(event.name)
|
|
219
|
-
? {
|
|
233
|
+
return isString(event.messageId) && isString(event.callId) && isString(event.name)
|
|
234
|
+
? {
|
|
235
|
+
type: 'tool.started',
|
|
236
|
+
messageId: event.messageId,
|
|
237
|
+
callId: event.callId,
|
|
238
|
+
name: event.name,
|
|
239
|
+
...seq,
|
|
240
|
+
}
|
|
220
241
|
: null;
|
|
221
242
|
case 'tool.finished':
|
|
222
|
-
return isString(event.callId) && isString(event.name)
|
|
243
|
+
return isString(event.messageId) && isString(event.callId) && isString(event.name)
|
|
223
244
|
? {
|
|
224
245
|
type: 'tool.finished',
|
|
246
|
+
messageId: event.messageId,
|
|
225
247
|
callId: event.callId,
|
|
226
248
|
name: event.name,
|
|
227
249
|
ok: event.ok === true,
|
|
228
250
|
...seq,
|
|
229
251
|
}
|
|
230
252
|
: null;
|
|
231
|
-
case 'approval.requested':
|
|
232
|
-
return isString(event.toolName)
|
|
233
|
-
? { type: 'approval.requested', toolName: event.toolName, ...seq }
|
|
234
|
-
: null;
|
|
235
253
|
case 'messages.saved':
|
|
236
254
|
return isString(event.conversationId) && Array.isArray(event.messageIds)
|
|
237
255
|
? {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ALIA_WEB_ORIGIN } from './origins';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The voice call's AudioWorklet processors (`worklets/voice-processors.js`), as the web engine
|
|
5
|
+
* loads them.
|
|
6
|
+
*
|
|
7
|
+
* Not a `blob:` URL: a worklet module is a script, so a blob would need `script-src blob:` in
|
|
8
|
+
* every host's CSP, which would let any blob the page makes run as script. Alia's web app serves
|
|
9
|
+
* the file instead, under `script-src https://alia.onl` that the avatar already needs
|
|
10
|
+
* (`@alia.onl/sdk/csp`), with CORS open for the cross-origin `addModule`.
|
|
11
|
+
*
|
|
12
|
+
* The name is the file's content hash, so an SDK embedded in another app (on its own release
|
|
13
|
+
* cycle) always loads the bytes it was written against: `packages/app/public/sdk/voice-worklets/`
|
|
14
|
+
* keeps every revision ever published, and a test fails if this revision is not the hash of the
|
|
15
|
+
* file or is not served.
|
|
16
|
+
*/
|
|
17
|
+
export const VOICE_WORKLETS_REVISION = '543d4a8e42937e99';
|
|
18
|
+
|
|
19
|
+
export const VOICE_WORKLETS_URL = `${ALIA_WEB_ORIGIN}/sdk/voice-worklets/${VOICE_WORKLETS_REVISION}.js`;
|
package/src/lib/work-log.ts
CHANGED
|
@@ -8,12 +8,12 @@ import {
|
|
|
8
8
|
} from '@oxy.so/bloom/icons';
|
|
9
9
|
import type { TaskListChip, TaskListStep, TaskListTask } from '@oxy.so/bloom/task-list';
|
|
10
10
|
import type { WebSearchBrand, WebSearchSource, WebSearchStep } from '@oxy.so/bloom/web-search';
|
|
11
|
-
import type { ToolInvocation } from '../types';
|
|
11
|
+
import type { ToolInvocation } from '../runtime/types';
|
|
12
12
|
import { extractCitationSources } from './citations';
|
|
13
13
|
import { domainOf } from './domain';
|
|
14
14
|
import { getToolDoneLabel, getToolPillLabel } from './task-labels';
|
|
15
15
|
import { toolCallStatus } from './tool-call-status';
|
|
16
|
-
import {
|
|
16
|
+
import { englishCopy, type Translate } from './copy';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* A turn's tool calls, as the two Bloom logs draw them.
|
|
@@ -32,7 +32,7 @@ import { englishWorkLog, type Translate } from './work-log-copy';
|
|
|
32
32
|
|
|
33
33
|
/** What a host can swap: the words, and where a site's icon is served from. */
|
|
34
34
|
export interface WorkLogOptions {
|
|
35
|
-
/** Translator for the log's words (see `
|
|
35
|
+
/** Translator for the log's words (see `copy.ts`). Default: English. */
|
|
36
36
|
t?: Translate;
|
|
37
37
|
/** The URL of a site's cached icon, or nothing for Bloom's quiet dot. */
|
|
38
38
|
faviconUrl?: (url: string) => string | null | undefined;
|
|
@@ -154,7 +154,7 @@ function webStep(
|
|
|
154
154
|
live: LiveResearch | undefined,
|
|
155
155
|
options: WorkLogOptions,
|
|
156
156
|
): MappedStep {
|
|
157
|
-
const t = options.t ??
|
|
157
|
+
const t = options.t ?? englishCopy;
|
|
158
158
|
const arrived = inv.state !== 'partial-call';
|
|
159
159
|
const ok = succeeded(inv);
|
|
160
160
|
const res = resultOf(inv);
|
|
@@ -256,7 +256,7 @@ export function webSearchLog(
|
|
|
256
256
|
research: LiveResearch | undefined,
|
|
257
257
|
options: WorkLogOptions = {},
|
|
258
258
|
): WebSearchLog {
|
|
259
|
-
const t = options.t ??
|
|
259
|
+
const t = options.t ?? englishCopy;
|
|
260
260
|
const web = (invocations ?? []).filter(isWebInvocation);
|
|
261
261
|
const mapped = web.map((inv) => webStep(inv, research, options));
|
|
262
262
|
|
|
@@ -333,7 +333,7 @@ export function taskListLog(
|
|
|
333
333
|
live: boolean,
|
|
334
334
|
options: WorkLogOptions = {},
|
|
335
335
|
): TaskListLog {
|
|
336
|
-
const t = options.t ??
|
|
336
|
+
const t = options.t ?? englishCopy;
|
|
337
337
|
let revealed = 0;
|
|
338
338
|
let open = true;
|
|
339
339
|
const tasks = (invocations ?? [])
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useOxy } from '@oxy.so/services/ui/client';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import { useAliaApi } from './api';
|
|
4
|
+
import { aliaQueryKeys } from './query-keys';
|
|
5
|
+
import { API_ROUTES } from './routes';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The agent a chat is opened on, as `GET /v1/agents/:id/profile` answers it
|
|
9
|
+
* (`docs/agent-embedding.md` §1). Alia herself has no profile: she is the lead when no agent is
|
|
10
|
+
* named.
|
|
11
|
+
*/
|
|
12
|
+
export interface AliaAgentProfile {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
handle?: string;
|
|
16
|
+
/** Its look, as Oxy stores a character (JSON); drawn through `readAgentCharacter`. */
|
|
17
|
+
character?: unknown;
|
|
18
|
+
color?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
/** What the empty chat says; omitted, the chat's own greeting. */
|
|
21
|
+
greeting?: string;
|
|
22
|
+
/** The lines offered over the composer of an empty chat. */
|
|
23
|
+
starters: AliaAgentStarter[];
|
|
24
|
+
access?: 'private' | 'public';
|
|
25
|
+
/** Which of Alia's features this agent gates behind an app's own check. */
|
|
26
|
+
features: Array<{ key: string; gated: boolean }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AliaAgentStarter {
|
|
30
|
+
title: string;
|
|
31
|
+
prompt: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One of Alia's features, for one agent and the person present (`GET /v1/agents/:id/features`). */
|
|
35
|
+
export interface AliaAgentFeature {
|
|
36
|
+
key: string;
|
|
37
|
+
state: 'available' | 'locked';
|
|
38
|
+
/** How many uses are left, when the publishing app counts them. */
|
|
39
|
+
remaining?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The feature keys the chat itself reads: the attach menu's two rows. */
|
|
43
|
+
export const ALIA_FEATURES = {
|
|
44
|
+
documents: 'attachments.documents',
|
|
45
|
+
images: 'attachments.images',
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
const FIVE_MINUTES = 1000 * 60 * 5;
|
|
49
|
+
|
|
50
|
+
/** A string, or nothing: the profile is read field by field rather than cast. */
|
|
51
|
+
const text = (value: unknown): string | undefined =>
|
|
52
|
+
typeof value === 'string' && value !== '' ? value : undefined;
|
|
53
|
+
|
|
54
|
+
/** The profile the server sent, kept only where it has the shape the contract names. */
|
|
55
|
+
export function readAgentProfile(value: unknown): AliaAgentProfile | null {
|
|
56
|
+
if (value === null || typeof value !== 'object') return null;
|
|
57
|
+
const raw = value as Record<string, unknown>;
|
|
58
|
+
const id = text(raw.id);
|
|
59
|
+
const name = text(raw.name);
|
|
60
|
+
if (id === undefined || name === undefined) return null;
|
|
61
|
+
const starters = Array.isArray(raw.starters)
|
|
62
|
+
? raw.starters.flatMap((starter): AliaAgentStarter[] => {
|
|
63
|
+
if (starter === null || typeof starter !== 'object') return [];
|
|
64
|
+
const { title, prompt } = starter as Record<string, unknown>;
|
|
65
|
+
const line = text(prompt);
|
|
66
|
+
return line === undefined ? [] : [{ title: text(title) ?? line, prompt: line }];
|
|
67
|
+
})
|
|
68
|
+
: [];
|
|
69
|
+
const features = Array.isArray(raw.features)
|
|
70
|
+
? raw.features.flatMap((feature): Array<{ key: string; gated: boolean }> => {
|
|
71
|
+
if (feature === null || typeof feature !== 'object') return [];
|
|
72
|
+
const { key, gated } = feature as Record<string, unknown>;
|
|
73
|
+
return typeof key === 'string' ? [{ key, gated: gated === true }] : [];
|
|
74
|
+
})
|
|
75
|
+
: [];
|
|
76
|
+
return {
|
|
77
|
+
id,
|
|
78
|
+
name,
|
|
79
|
+
handle: text(raw.handle),
|
|
80
|
+
character: raw.character ?? undefined,
|
|
81
|
+
color: text(raw.color),
|
|
82
|
+
description: text(raw.description),
|
|
83
|
+
greeting: text(raw.greeting),
|
|
84
|
+
starters,
|
|
85
|
+
access: raw.access === 'public' || raw.access === 'private' ? raw.access : undefined,
|
|
86
|
+
features,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The feature states the server sent; an entry of any other shape is not a state. */
|
|
91
|
+
export function readAgentFeatures(value: unknown): AliaAgentFeature[] {
|
|
92
|
+
const list =
|
|
93
|
+
value !== null && typeof value === 'object'
|
|
94
|
+
? (value as { features?: unknown }).features
|
|
95
|
+
: undefined;
|
|
96
|
+
if (!Array.isArray(list)) return [];
|
|
97
|
+
return list.flatMap((feature): AliaAgentFeature[] => {
|
|
98
|
+
if (feature === null || typeof feature !== 'object') return [];
|
|
99
|
+
const { key, state, remaining } = feature as Record<string, unknown>;
|
|
100
|
+
if (typeof key !== 'string' || (state !== 'available' && state !== 'locked')) return [];
|
|
101
|
+
return [{ key, state, ...(typeof remaining === 'number' ? { remaining } : {}) }];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The profile of the agent `agentId`, or nothing for Alia (no id) and while it loads. */
|
|
106
|
+
export function useAgentProfile(agentId: string | undefined) {
|
|
107
|
+
const { isAuthenticated } = useOxy();
|
|
108
|
+
const api = useAliaApi();
|
|
109
|
+
return useQuery({
|
|
110
|
+
queryKey: aliaQueryKeys.agents.profile(agentId ?? ''),
|
|
111
|
+
queryFn: async () =>
|
|
112
|
+
readAgentProfile((await api.get<unknown>(API_ROUTES.agents.profile(agentId ?? ''))).data),
|
|
113
|
+
enabled: agentId !== undefined && isAuthenticated,
|
|
114
|
+
staleTime: FIVE_MINUTES,
|
|
115
|
+
retry: 1,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What of Alia's features the agent `agentId` offers the person present. Alia (no id) gates
|
|
121
|
+
* nothing, so she has no answer to wait for; see `featureRows` for what the chat makes of it.
|
|
122
|
+
*/
|
|
123
|
+
export function useAgentFeatures(agentId: string | undefined) {
|
|
124
|
+
const { isAuthenticated } = useOxy();
|
|
125
|
+
const api = useAliaApi();
|
|
126
|
+
return useQuery({
|
|
127
|
+
queryKey: aliaQueryKeys.agents.features(agentId ?? ''),
|
|
128
|
+
queryFn: async () =>
|
|
129
|
+
readAgentFeatures((await api.get<unknown>(API_ROUTES.agents.features(agentId ?? ''))).data),
|
|
130
|
+
enabled: agentId !== undefined && isAuthenticated,
|
|
131
|
+
// A gate's state moves with the person's plan in another app: read it each time the chat opens.
|
|
132
|
+
staleTime: 0,
|
|
133
|
+
retry: 1,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** How an attachment feature is offered: attached, an upsell, or not at all. */
|
|
138
|
+
export type FeatureOffer = 'available' | 'locked' | 'absent';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What the attach menu offers for `key`. Alia's own chat (`features === undefined`) offers every
|
|
142
|
+
* attachment; an agent offers what its feature answer lists — `locked` is still offered, as the
|
|
143
|
+
* host's upsell — and nothing it does not list.
|
|
144
|
+
*/
|
|
145
|
+
export function featureOffer(
|
|
146
|
+
features: readonly AliaAgentFeature[] | undefined,
|
|
147
|
+
key: string,
|
|
148
|
+
): FeatureOffer {
|
|
149
|
+
if (features === undefined) return 'available';
|
|
150
|
+
return features.find((feature) => feature.key === key)?.state ?? 'absent';
|
|
151
|
+
}
|