@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,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host tools: UI effects in the embedding client (`docs/agent-embedding.md` §2, ADR 0020 §6).
|
|
3
|
+
*
|
|
4
|
+
* A surface declares the effects its own client can perform with every request. The model sees
|
|
5
|
+
* each as `host_<app>__<name>`; when it calls one, the server records the call and emits
|
|
6
|
+
* `alia.host_action`, and this client — the only one that declared it — performs it and sends
|
|
7
|
+
* the outcome with its NEXT request, where it becomes that call's result. A host tool is never
|
|
8
|
+
* authority over data: it moves the host's own screen.
|
|
9
|
+
*
|
|
10
|
+
* Two rules hold here, in a queue that knows nothing of React:
|
|
11
|
+
* - an action is performed at most once, whatever the stream repeats;
|
|
12
|
+
* - an outcome stays queued until a request carrying it has been accepted, so a turn that ends
|
|
13
|
+
* before its outcomes are sent hands them to the next one.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { HostAction } from './stream-events';
|
|
17
|
+
|
|
18
|
+
export type { HostAction as AliaHostAction } from './stream-events';
|
|
19
|
+
|
|
20
|
+
/** One effect the host can perform. `name` matches `^[a-z][a-z0-9_]{0,47}$`. */
|
|
21
|
+
export interface AliaHostToolDefinition {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
/** A JSON Schema with an object root. */
|
|
25
|
+
inputSchema: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const HOST_OUTCOMES = ['applied', 'inline', 'rejected', 'stale', 'failed'] as const;
|
|
29
|
+
export type AliaHostOutcomeKind = (typeof HOST_OUTCOMES)[number];
|
|
30
|
+
|
|
31
|
+
/** What came of an action: the host's own word for it, and a line for the model. */
|
|
32
|
+
export interface AliaHostOutcome {
|
|
33
|
+
outcome: AliaHostOutcomeKind;
|
|
34
|
+
/** At most 500 characters; longer is cut. */
|
|
35
|
+
detail?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The host tools a surface declares (`hostTools` on the chat). */
|
|
39
|
+
export interface AliaHostTools {
|
|
40
|
+
/** The app's slug, `^[a-z][a-z0-9-]{0,31}$`. */
|
|
41
|
+
app: string;
|
|
42
|
+
/**
|
|
43
|
+
* At most 16. What the host's screen shows now is not part of the declaration: it is the chat's
|
|
44
|
+
* `clientContext`, built by the host per turn.
|
|
45
|
+
*/
|
|
46
|
+
definitions: readonly AliaHostToolDefinition[];
|
|
47
|
+
/** Perform `action` and say what came of it. Called once per action. */
|
|
48
|
+
execute: (action: HostAction) => Promise<AliaHostOutcome>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An outcome as a request carries it. */
|
|
52
|
+
export interface HostOutcomeRecord {
|
|
53
|
+
toolCallId: string;
|
|
54
|
+
outcome: AliaHostOutcomeKind;
|
|
55
|
+
detail?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The request's `host` member. */
|
|
59
|
+
export interface HostRequest {
|
|
60
|
+
app: string;
|
|
61
|
+
version: 1;
|
|
62
|
+
tools: AliaHostToolDefinition[];
|
|
63
|
+
/** Omitted when there is nothing to report. */
|
|
64
|
+
outcomes?: HostOutcomeRecord[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const DETAIL_LIMIT = 500;
|
|
68
|
+
|
|
69
|
+
function record(toolCallId: string, outcome: AliaHostOutcome | undefined): HostOutcomeRecord {
|
|
70
|
+
const kind = HOST_OUTCOMES.includes(outcome?.outcome as AliaHostOutcomeKind)
|
|
71
|
+
? (outcome?.outcome as AliaHostOutcomeKind)
|
|
72
|
+
: 'failed';
|
|
73
|
+
const detail = typeof outcome?.detail === 'string' ? outcome.detail.slice(0, DETAIL_LIMIT) : '';
|
|
74
|
+
return { toolCallId, outcome: kind, ...(detail === '' ? {} : { detail }) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The outcomes of a conversation's host actions, between the stream that asked and the next request. */
|
|
78
|
+
export class HostOutcomeQueue {
|
|
79
|
+
private readonly performed = new Set<string>();
|
|
80
|
+
private ready: HostOutcomeRecord[] = [];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Perform `action` with `tools`, once. Resolves when its outcome is queued; `false` when the
|
|
84
|
+
* action had already been performed and nothing was done.
|
|
85
|
+
*/
|
|
86
|
+
async dispatch(action: HostAction, tools: AliaHostTools | undefined): Promise<boolean> {
|
|
87
|
+
if (this.performed.has(action.toolCallId)) return false;
|
|
88
|
+
this.performed.add(action.toolCallId);
|
|
89
|
+
let outcome: AliaHostOutcome;
|
|
90
|
+
if (tools === undefined || action.app !== tools.app) {
|
|
91
|
+
outcome = { outcome: 'rejected', detail: 'This app did not declare that tool.' };
|
|
92
|
+
} else if (!tools.definitions.some((tool) => tool.name === action.name)) {
|
|
93
|
+
outcome = { outcome: 'rejected', detail: `Unknown tool "${action.name}".` };
|
|
94
|
+
} else {
|
|
95
|
+
try {
|
|
96
|
+
outcome = await tools.execute(action);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
outcome = {
|
|
99
|
+
outcome: 'failed',
|
|
100
|
+
detail: error instanceof Error ? error.message : 'The app could not do it.',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
this.ready.push(record(action.toolCallId, outcome));
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The outcomes ready to send, oldest first. */
|
|
109
|
+
pending(): HostOutcomeRecord[] {
|
|
110
|
+
return [...this.ready];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A request carrying `sent` was accepted: those are delivered. */
|
|
114
|
+
delivered(sent: readonly HostOutcomeRecord[]): void {
|
|
115
|
+
this.ready = this.ready.filter((outcome) => !sent.includes(outcome));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A different conversation: its outcomes are not this one's. What was performed stays performed. */
|
|
119
|
+
clear(): void {
|
|
120
|
+
this.ready = [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The `host` member of a request, or nothing for a surface without host tools. */
|
|
125
|
+
export function hostRequest(
|
|
126
|
+
tools: AliaHostTools | undefined,
|
|
127
|
+
outcomes: readonly HostOutcomeRecord[],
|
|
128
|
+
): HostRequest | undefined {
|
|
129
|
+
if (tools === undefined) return undefined;
|
|
130
|
+
return {
|
|
131
|
+
app: tools.app,
|
|
132
|
+
version: 1,
|
|
133
|
+
tools: tools.definitions.map(({ name, description, inputSchema }) => ({
|
|
134
|
+
name,
|
|
135
|
+
description,
|
|
136
|
+
inputSchema,
|
|
137
|
+
})),
|
|
138
|
+
...(outcomes.length === 0 ? {} : { outcomes: [...outcomes] }),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation runtime: the data layer under the sheet, the screen and the Alia app.
|
|
3
|
+
*
|
|
4
|
+
* Layers, each depending only on the ones above it in this list:
|
|
5
|
+
*
|
|
6
|
+
* 1. transport — `api` (REST over the signed-in Oxy client), the turn's SSE stream
|
|
7
|
+
* 2. model and state — `types`, `query-keys`, the zustand stores, the cached conversation lists
|
|
8
|
+
* 3. hooks — `useConversations`…, `useConversationThread`
|
|
9
|
+
*
|
|
10
|
+
* Nothing here draws anything. It is also a leaf entry, `@alia.onl/sdk/runtime`, for a host that
|
|
11
|
+
* needs the data layer without the chat UI (a sidebar, a test).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { AliaRuntimeProvider, useAliaRuntime } from './config';
|
|
15
|
+
export type {
|
|
16
|
+
AliaArtifact,
|
|
17
|
+
AliaRuntimeConfig,
|
|
18
|
+
AliaRuntimeHost,
|
|
19
|
+
AliaRuntimeProviderProps,
|
|
20
|
+
AliaTurnRequest,
|
|
21
|
+
AliaUsageFrame,
|
|
22
|
+
OfflineConversationStore,
|
|
23
|
+
SpeechPreferences,
|
|
24
|
+
ThoughtOpening,
|
|
25
|
+
} from './config';
|
|
26
|
+
export { AliaApiError, useAliaApi } from './api';
|
|
27
|
+
export {
|
|
28
|
+
ALIA_FEATURES,
|
|
29
|
+
featureOffer,
|
|
30
|
+
readAgentFeatures,
|
|
31
|
+
readAgentProfile,
|
|
32
|
+
useAgentFeatures,
|
|
33
|
+
useAgentProfile,
|
|
34
|
+
} from './agents';
|
|
35
|
+
export type { AliaAgentFeature, AliaAgentProfile, AliaAgentStarter, FeatureOffer } from './agents';
|
|
36
|
+
export { HOST_OUTCOMES, HostOutcomeQueue, hostRequest } from './host-tools';
|
|
37
|
+
export type {
|
|
38
|
+
AliaHostAction,
|
|
39
|
+
AliaHostOutcome,
|
|
40
|
+
AliaHostOutcomeKind,
|
|
41
|
+
AliaHostToolDefinition,
|
|
42
|
+
AliaHostTools,
|
|
43
|
+
} from './host-tools';
|
|
44
|
+
export { FEATURE_ENTITLEMENT_REQUIRED, parseConsultEvent, parseHostAction } from './stream-events';
|
|
45
|
+
export type { ConsultAgent, ConsultEvent } from './stream-events';
|
|
46
|
+
export {
|
|
47
|
+
DEFAULT_SHARE_TTL_HOURS,
|
|
48
|
+
readSharedConversation,
|
|
49
|
+
SharedConversationGoneError,
|
|
50
|
+
sharedMessages,
|
|
51
|
+
useShareConversation,
|
|
52
|
+
useSharedConversation,
|
|
53
|
+
} from './sharing';
|
|
54
|
+
export type {
|
|
55
|
+
AliaConversationShare,
|
|
56
|
+
AliaSharedAgent,
|
|
57
|
+
AliaSharedConversationData,
|
|
58
|
+
AliaSharedMessage,
|
|
59
|
+
} from './sharing';
|
|
60
|
+
export { uploadToOxyFiles, useDocumentUpload } from './oxy-files';
|
|
61
|
+
export type { OxyFilesClient } from './oxy-files';
|
|
62
|
+
export type { AliaApi, AliaApiRequest, AliaApiResult } from './api';
|
|
63
|
+
export { aliaQueryKeys } from './query-keys';
|
|
64
|
+
export {
|
|
65
|
+
ConversationNotFoundError,
|
|
66
|
+
useClearConversation,
|
|
67
|
+
useConversation,
|
|
68
|
+
useConversations,
|
|
69
|
+
useCreateConversation,
|
|
70
|
+
useDeleteConversation,
|
|
71
|
+
usePrefetchConversation,
|
|
72
|
+
useRenameConversation,
|
|
73
|
+
useSaveConversation,
|
|
74
|
+
} from './conversations';
|
|
75
|
+
export { useConversationThread, mergeEditedContent } from './use-conversation-thread';
|
|
76
|
+
export type {
|
|
77
|
+
ConversationThread,
|
|
78
|
+
EditedContent,
|
|
79
|
+
UseConversationThreadOptions,
|
|
80
|
+
} from './use-conversation-thread';
|
|
81
|
+
export type { SendOutcome } from './use-conversation-stream';
|
|
82
|
+
export { useConversationStore } from './conversation-store';
|
|
83
|
+
export type { PendingInitialMessage } from './conversation-store';
|
|
84
|
+
export {
|
|
85
|
+
releaseAttachments,
|
|
86
|
+
useComposerDraft,
|
|
87
|
+
useComposerDraftStore,
|
|
88
|
+
useDraftAddress,
|
|
89
|
+
} from './composer-draft-store';
|
|
90
|
+
export type {
|
|
91
|
+
ComposerDraft,
|
|
92
|
+
DraftAddress,
|
|
93
|
+
DraftTarget,
|
|
94
|
+
} from './composer-draft-store';
|
|
95
|
+
export { useAgentActivity } from './use-agent-activity';
|
|
96
|
+
export type { UseAgentActivityResult } from './use-agent-activity';
|
|
97
|
+
export { useThoughtStore } from './thought-store';
|
|
98
|
+
export type {
|
|
99
|
+
ThoughtScope,
|
|
100
|
+
ThoughtScopeStatus,
|
|
101
|
+
ThoughtSelection,
|
|
102
|
+
ThoughtTab,
|
|
103
|
+
} from './thought-store';
|
|
104
|
+
export { useThreadHistory } from './use-thread-history';
|
|
105
|
+
export { useThreadSearch, useThreadWindow } from './use-thread-search';
|
|
106
|
+
export { useStartConversation } from './use-start-conversation';
|
|
107
|
+
export type { StartConversationOptions } from './use-start-conversation';
|
|
108
|
+
export { useTurnEdit } from './use-turn-edit';
|
|
109
|
+
export { buildMessageContent } from './attachment-utils';
|
|
110
|
+
export type { BuiltMessageContent, DroppedAttachment, UploadDocument } from './attachment-utils';
|
|
111
|
+
|
|
112
|
+
// The conversation as data (also the leaf entry `@alia.onl/sdk/model`).
|
|
113
|
+
export * from './model';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Message, ToolInvocation } from './types';
|
|
2
|
+
|
|
3
|
+
/** Message shape accepted by the chat-completions endpoint. */
|
|
4
|
+
export interface OutboundMessage {
|
|
5
|
+
/**
|
|
6
|
+
* The id this screen draws the message under. The server stores the turn
|
|
7
|
+
* with the ids it was sent (`packages/api/src/lib/conversation-saver.ts`), so
|
|
8
|
+
* a vote or a read-aloud addressed by `Message.id` finds the row without a
|
|
9
|
+
* reload in between.
|
|
10
|
+
*/
|
|
11
|
+
id: string;
|
|
12
|
+
role: string;
|
|
13
|
+
content: Message['content'];
|
|
14
|
+
toolInvocations?: Array<{
|
|
15
|
+
toolCallId: string;
|
|
16
|
+
toolName: string;
|
|
17
|
+
state: ToolInvocation['state'];
|
|
18
|
+
args?: Record<string, unknown>;
|
|
19
|
+
result?: unknown;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatOutboundMessage(message: Message): OutboundMessage {
|
|
24
|
+
const outbound: OutboundMessage = {
|
|
25
|
+
id: message.id,
|
|
26
|
+
role: message.role,
|
|
27
|
+
content: message.content,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (message.role === 'assistant' && message.toolInvocations?.length) {
|
|
31
|
+
outbound.toolInvocations = message.toolInvocations.map((invocation) => ({
|
|
32
|
+
toolCallId: invocation.toolCallId,
|
|
33
|
+
toolName: invocation.toolName,
|
|
34
|
+
state: invocation.state,
|
|
35
|
+
args: invocation.args,
|
|
36
|
+
result: invocation.result,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return outbound;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The id of the `index`-th consulted agent's answer (`alia.agent_start`, in the
|
|
45
|
+
* order the consultations started) in the turn whose reply is `assistantMessageId`.
|
|
46
|
+
*
|
|
47
|
+
* Derived, not received: the frames name a consultation (`consultId`), not a
|
|
48
|
+
* message. The server stores the same answer under the same derivation
|
|
49
|
+
* (`conversation-saver.ts` `agentMessageId`).
|
|
50
|
+
*/
|
|
51
|
+
export function agentMessageId(assistantMessageId: string, index: number): string {
|
|
52
|
+
return `${agentMessagePrefix(assistantMessageId)}${index}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether `id` is one of the agent answers {@link agentMessageId} names for this reply. */
|
|
56
|
+
export function isAgentMessageOf(id: string, assistantMessageId: string): boolean {
|
|
57
|
+
return id.startsWith(agentMessagePrefix(assistantMessageId));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function agentMessagePrefix(assistantMessageId: string): string {
|
|
61
|
+
return `${assistantMessageId}-agent-`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Build a turn from the history captured before optimistic UI updates. */
|
|
65
|
+
export function buildOutboundMessages(
|
|
66
|
+
historySnapshot: readonly Message[],
|
|
67
|
+
userMessage: Message,
|
|
68
|
+
): OutboundMessage[] {
|
|
69
|
+
return [...historySnapshot, userMessage].map(formatOutboundMessage);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sameContent(a: Message['content'], b: Message['content']): boolean {
|
|
73
|
+
if (a === b) return true;
|
|
74
|
+
try {
|
|
75
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isEmptyAssistantPlaceholder(message: Message | undefined): boolean {
|
|
82
|
+
return (
|
|
83
|
+
message?.role === 'assistant' &&
|
|
84
|
+
message.content === '' &&
|
|
85
|
+
!message.thinking &&
|
|
86
|
+
!message.toolInvocations?.length &&
|
|
87
|
+
!message.agentInfo
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Hide histories written by the former optimistic-send race.
|
|
93
|
+
*
|
|
94
|
+
* That race persisted one exact sequence: user, empty assistant placeholder,
|
|
95
|
+
* the same user again. Only that signature is collapsed; intentional repeated
|
|
96
|
+
* prompts separated by a real assistant response remain untouched. The next
|
|
97
|
+
* successful turn rewrites divergent server history from this clean view.
|
|
98
|
+
*/
|
|
99
|
+
export function normalizeConversationMessages(messages: readonly Message[]): Message[] {
|
|
100
|
+
const normalized: Message[] = [];
|
|
101
|
+
// A host's client context travels as a system message and is never part of the thread.
|
|
102
|
+
messages = messages.filter((message) => message.role !== 'system');
|
|
103
|
+
|
|
104
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
105
|
+
const current = messages[index];
|
|
106
|
+
const placeholder = messages[index + 1];
|
|
107
|
+
const duplicate = messages[index + 2];
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
current.role === 'user' &&
|
|
111
|
+
isEmptyAssistantPlaceholder(placeholder) &&
|
|
112
|
+
duplicate?.role === 'user' &&
|
|
113
|
+
sameContent(current.content, duplicate.content)
|
|
114
|
+
) {
|
|
115
|
+
normalized.push(current);
|
|
116
|
+
index += 2;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
normalized.push(current);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return normalized;
|
|
124
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation as data: the records, and what a list of messages becomes before anything
|
|
3
|
+
* draws it (the timeline, the thought panel's steps and sources, tool cards, day lines,
|
|
4
|
+
* the agent thread's windows, the markdown export), plus the typed ways a turn fails.
|
|
5
|
+
*
|
|
6
|
+
* Pure: no React, no React Native, no Oxy client. It is a leaf entry (`@alia.onl/sdk/model`) for
|
|
7
|
+
* code that only reads conversations, and the runtime re-exports all of it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { API_ROUTES as ALIA_API_ROUTES } from './routes';
|
|
11
|
+
export * from './power-levels';
|
|
12
|
+
export { englishCopy } from '../lib/copy';
|
|
13
|
+
export type { Translate } from '../lib/copy';
|
|
14
|
+
export * from './thread/agent-activity';
|
|
15
|
+
export * from './thread/automation-labels';
|
|
16
|
+
export * from './thread/conversation-export';
|
|
17
|
+
export * from './thread/message-days';
|
|
18
|
+
export * from './thread/message-processor';
|
|
19
|
+
export * from './thread/task-utils';
|
|
20
|
+
export * from './thread/thought-utils';
|
|
21
|
+
export * from './thread/thread-history';
|
|
22
|
+
export * from './thread/timeline';
|
|
23
|
+
export * from './thread/tool-cards';
|
|
24
|
+
export { failureDetail } from './turn-failure';
|
|
25
|
+
export type { FailedTurn } from './turn-failure';
|
|
26
|
+
export type {
|
|
27
|
+
AgentRun,
|
|
28
|
+
AutomationResource,
|
|
29
|
+
AutomationTrigger,
|
|
30
|
+
Conversation,
|
|
31
|
+
ConversationsInfiniteData,
|
|
32
|
+
ConversationsPage,
|
|
33
|
+
Message,
|
|
34
|
+
MessageContent,
|
|
35
|
+
SendOptions,
|
|
36
|
+
ToolInvocation,
|
|
37
|
+
} from './types';
|
|
38
|
+
export { UsageLimitError } from './usage-limit-error';
|
|
39
|
+
export type { UsageLimitDetails, UsageLimitType } from './usage-limit-error';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useOxy } from '@oxy.so/services/ui/client';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import type { AliaAttachment } from '../types';
|
|
4
|
+
import type { UploadDocument } from './attachment-utils';
|
|
5
|
+
|
|
6
|
+
/** The slice of `OxyServices` an upload needs: Oxy Files' own `assets.upload` (`POST /assets/upload`). */
|
|
7
|
+
export interface OxyFilesClient {
|
|
8
|
+
assets: {
|
|
9
|
+
upload(
|
|
10
|
+
file: { uri: string; type?: string; name?: string; size?: number },
|
|
11
|
+
options?: { visibility?: 'private' | 'public' | 'unlisted' },
|
|
12
|
+
): Promise<{ file: { id: string } }>;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Put `attachment` in the person's Oxy Files, private, and answer its id. */
|
|
17
|
+
export function uploadToOxyFiles(oxy: OxyFilesClient): UploadDocument {
|
|
18
|
+
return async (attachment: AliaAttachment) => {
|
|
19
|
+
const { file } = await oxy.assets.upload(
|
|
20
|
+
{
|
|
21
|
+
uri: attachment.uri,
|
|
22
|
+
type: attachment.mimeType,
|
|
23
|
+
name: attachment.name,
|
|
24
|
+
size: attachment.size,
|
|
25
|
+
},
|
|
26
|
+
{ visibility: 'private' },
|
|
27
|
+
);
|
|
28
|
+
if (typeof file?.id !== 'string' || file.id === '') throw new Error('Oxy Files gave no id');
|
|
29
|
+
return file.id;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The signed-in person's document upload, for a turn's `file` parts. */
|
|
34
|
+
export function useDocumentUpload(): UploadDocument {
|
|
35
|
+
const { oxyServices } = useOxy();
|
|
36
|
+
return useMemo(() => uploadToOxyFiles(oxyServices as unknown as OxyFilesClient), [oxyServices]);
|
|
37
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Power levels — what a person chooses in Alia instead of a model.
|
|
3
|
+
*
|
|
4
|
+
* A level is one of Oxy's routing profiles (a slug with no `/`). The chat sends
|
|
5
|
+
* it as the request's `model`; Oxy picks an available model of that level for
|
|
6
|
+
* each request, cheapest first, and fails over inside the level. The chat never
|
|
7
|
+
* names a hosted model. The one exception is a model running on one of the
|
|
8
|
+
* person's OWN devices (`local/<runtime>/<model>`): that is their machine, not
|
|
9
|
+
* a model Alia offers, and it stays selectable while the device is connected.
|
|
10
|
+
*
|
|
11
|
+
* The seven slugs are Oxy's fixed vocabulary, so the chat can put words to
|
|
12
|
+
* each one in every language it speaks.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const POWER_LEVELS = ['auto', 'instant', 'medium', 'high', 'xhigh', 'pro', 'ultra'] as const;
|
|
16
|
+
export type PowerLevel = (typeof POWER_LEVELS)[number];
|
|
17
|
+
|
|
18
|
+
/** Where everyone starts, and what any choice no longer offered becomes. */
|
|
19
|
+
export const DEFAULT_POWER_LEVEL: PowerLevel = 'auto';
|
|
20
|
+
|
|
21
|
+
export function isPowerLevel(value: unknown): value is PowerLevel {
|
|
22
|
+
return typeof value === 'string' && (POWER_LEVELS as readonly string[]).includes(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A model served by one of the person's own devices. */
|
|
26
|
+
export function isDeviceModelId(value: unknown): value is string {
|
|
27
|
+
return typeof value === 'string' && value.startsWith('local/') && value.length > 'local/'.length;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ModeSelection {
|
|
31
|
+
/** What the selector shows as chosen: a level, or a connected device model. */
|
|
32
|
+
readonly shown: string;
|
|
33
|
+
/** What a request carries as `model`. Always present: the chat always says. */
|
|
34
|
+
readonly send: string;
|
|
35
|
+
/**
|
|
36
|
+
* `requested`: the stored choice is sent. `replaced`: it is not offered right
|
|
37
|
+
* now (a device that is not connected, or a value from an older build), so
|
|
38
|
+
* `auto` answers and the selector says so.
|
|
39
|
+
*/
|
|
40
|
+
readonly source: 'requested' | 'replaced';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a stored choice against what can be sent now.
|
|
45
|
+
*
|
|
46
|
+
* `deviceModelIds` is `undefined` while the person's devices are still being
|
|
47
|
+
* listed: a device model is then kept rather than replaced, so a slow cold
|
|
48
|
+
* start never moves the person off their own machine.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveModeSelection(
|
|
51
|
+
stored: string | null | undefined,
|
|
52
|
+
deviceModelIds: readonly string[] | undefined,
|
|
53
|
+
): ModeSelection {
|
|
54
|
+
if (isPowerLevel(stored)) return { shown: stored, send: stored, source: 'requested' };
|
|
55
|
+
if (
|
|
56
|
+
isDeviceModelId(stored) &&
|
|
57
|
+
(deviceModelIds === undefined || deviceModelIds.includes(stored))
|
|
58
|
+
) {
|
|
59
|
+
return { shown: stored, send: stored, source: 'requested' };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
shown: DEFAULT_POWER_LEVEL,
|
|
63
|
+
send: DEFAULT_POWER_LEVEL,
|
|
64
|
+
source: stored === null || stored === undefined || stored === '' ? 'requested' : 'replaced',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The React Query keys of the conversation runtime. The host's persister and its sidebar read
|
|
3
|
+
* the same keys the hooks write, so a list shown anywhere is the list the chat updates.
|
|
4
|
+
*/
|
|
5
|
+
export const aliaQueryKeys = {
|
|
6
|
+
conversations: {
|
|
7
|
+
all: ['conversations'] as const,
|
|
8
|
+
detail: (id: string) => ['conversation', id] as const,
|
|
9
|
+
/** The person's conversations with one agent: under `all`, so every write to the list reaches it. */
|
|
10
|
+
withAgent: (agentId: string) => ['conversations', { agentId }] as const,
|
|
11
|
+
},
|
|
12
|
+
/** An agent the chat is opened on: its profile and the state of its feature gates. */
|
|
13
|
+
agents: {
|
|
14
|
+
profile: (id: string) => ['alia-agent-profile', id] as const,
|
|
15
|
+
features: (id: string) => ['alia-agent-features', id] as const,
|
|
16
|
+
},
|
|
17
|
+
/** A conversation somebody shared, by its token. */
|
|
18
|
+
shared: (token: string) => ['alia-shared-conversation', token] as const,
|
|
19
|
+
/** An agent's permanent thread: the stretches behind the open conversation, and finding in them. */
|
|
20
|
+
threads: {
|
|
21
|
+
/** The thread itself (which conversation is active): the host's own query lives under it. */
|
|
22
|
+
all: (handle: string) => ['agent-thread', handle] as const,
|
|
23
|
+
/**
|
|
24
|
+
* The history, kept apart from the thread itself: re-reading which conversation is active is
|
|
25
|
+
* cheap and happens whenever a new stretch starts, while the pages behind it are immutable and
|
|
26
|
+
* expensive to refetch.
|
|
27
|
+
*/
|
|
28
|
+
messages: (handle: string) => ['agent-thread-messages', handle] as const,
|
|
29
|
+
/** Keyed by the query as typed, so each one is cached and none is re-asked. */
|
|
30
|
+
search: (handle: string, query: string) => ['agent-thread-search', handle, query] as const,
|
|
31
|
+
/** One window of the thread, addressed by the cursor a search hit carries. */
|
|
32
|
+
window: (handle: string, cursor: string) => ['agent-thread-window', handle, cursor] as const,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A UUID v4 that does not rely on `crypto.randomUUID()`.
|
|
3
|
+
*
|
|
4
|
+
* Web Crypto exposes `getRandomValues()` in every browsing context, while
|
|
5
|
+
* `randomUUID()` is restricted to secure contexts (a LAN-hosted development
|
|
6
|
+
* build is not one), and React Native has neither until a polyfill installs
|
|
7
|
+
* them. The last resort is `Math.random`: these ids name messages in a thread,
|
|
8
|
+
* they are not secrets.
|
|
9
|
+
*/
|
|
10
|
+
export function createRandomUuid(): string {
|
|
11
|
+
const bytes = new Uint8Array(16);
|
|
12
|
+
const webCrypto = (globalThis as { crypto?: Crypto }).crypto;
|
|
13
|
+
if (webCrypto?.getRandomValues) {
|
|
14
|
+
webCrypto.getRandomValues(bytes);
|
|
15
|
+
} else {
|
|
16
|
+
for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// RFC 4122 version 4 and variant 1 bits.
|
|
20
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
|
|
21
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
22
|
+
|
|
23
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
24
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The routes the SDK calls — all of them on Alia's product API, `/v1`.
|
|
3
|
+
*
|
|
4
|
+
* The SDK is embedded by every Oxy app on its own web origin, so it may only call a surface that
|
|
5
|
+
* answers a browser from any origin with a bearer and no cookies: `/v1` (public CORS). The
|
|
6
|
+
* cookie-era routes (`/conversations`, `/alia/chat`) answer Alia's own origins alone and are not
|
|
7
|
+
* for the SDK. `tests/runtime/v1-only.test.ts` fails if the transport builds a URL outside `/v1`.
|
|
8
|
+
*
|
|
9
|
+
* `/v1/chat/completions` is the product's turn route: the same handler `/alia/chat` mounts, with
|
|
10
|
+
* every `alia.*` event, `conversationId` and the stored history.
|
|
11
|
+
*/
|
|
12
|
+
export const API_ROUTES = {
|
|
13
|
+
chat: '/v1/chat/completions',
|
|
14
|
+
conversations: {
|
|
15
|
+
list: '/v1/conversations',
|
|
16
|
+
save: '/v1/conversations',
|
|
17
|
+
create: '/v1/conversations/new',
|
|
18
|
+
get: (id: string) => `/v1/conversations/${id}`,
|
|
19
|
+
remove: (id: string) => `/v1/conversations/${id}`,
|
|
20
|
+
/**
|
|
21
|
+
* Empty a thread without removing it: every message goes, the row and its
|
|
22
|
+
* title stay. A sub-resource DELETE rather than a `/clear` verb, because
|
|
23
|
+
* what is being deleted is the message collection and nothing else.
|
|
24
|
+
*/
|
|
25
|
+
clearMessages: (id: string) => `/v1/conversations/${id}/messages`,
|
|
26
|
+
vote: (id: string, messageId: string) => `/v1/conversations/${id}/messages/${messageId}/vote`,
|
|
27
|
+
/** Mint (`POST { ttlHours }`) or revoke (`DELETE`) the conversation's read-only share link. */
|
|
28
|
+
share: (id: string) => `/v1/conversations/${id}/share`,
|
|
29
|
+
},
|
|
30
|
+
/** An agent any app can open: who it is, and which of Alia's features it gates. */
|
|
31
|
+
agents: {
|
|
32
|
+
profile: (id: string) => `/v1/agents/${encodeURIComponent(id)}/profile`,
|
|
33
|
+
features: (id: string) => `/v1/agents/${encodeURIComponent(id)}/features`,
|
|
34
|
+
},
|
|
35
|
+
/** A shared conversation, read without a session. */
|
|
36
|
+
shared: (token: string) => `/v1/shared/${encodeURIComponent(token)}`,
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* An agent's thread and its runs. These belong to Alia's own clients (the app), which are the only
|
|
41
|
+
* ones with agents of their own: `agentId`, `threadHandle` and the agent panel are not for an
|
|
42
|
+
* embedding Oxy app, and these routes answer Alia's origins alone, like `/conversations` does.
|
|
43
|
+
*/
|
|
44
|
+
export const AGENT_ROUTES = {
|
|
45
|
+
/** A page of the thread, oldest-first, crossing the seams between its conversations. */
|
|
46
|
+
threadMessages: (handle: string) => `/agents/thread/${encodeURIComponent(handle)}/messages`,
|
|
47
|
+
/** Everything said in the thread that matches, across every conversation in it. */
|
|
48
|
+
threadSearch: (handle: string) => `/agents/thread/${encodeURIComponent(handle)}/search`,
|
|
49
|
+
/** What a run of an agent has done so far, to hydrate the live feed. */
|
|
50
|
+
sessionStatus: (sessionId: string) => `/agents/sessions/${sessionId}/status`,
|
|
51
|
+
} as const;
|