@sveda-ai/core 0.1.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.
@@ -0,0 +1,22 @@
1
+ import type { SvedaClientToolDefinition } from '@sveda-ai/protocol';
2
+ export interface SvedaFrontendToolContext {
3
+ chatId: string;
4
+ toolCallId: string;
5
+ }
6
+ export interface SvedaFrontendTool {
7
+ name: string;
8
+ description: string;
9
+ parameters: Record<string, unknown>;
10
+ handler: (input: Record<string, unknown>, context: SvedaFrontendToolContext) => unknown | Promise<unknown>;
11
+ }
12
+ export declare class SvedaToolRegistry {
13
+ private tools;
14
+ register(tool: SvedaFrontendTool): () => void;
15
+ unregister(name: string): void;
16
+ has(name: string): boolean;
17
+ get(name: string): SvedaFrontendTool | undefined;
18
+ list(): SvedaClientToolDefinition[];
19
+ execute(name: string, input: Record<string, unknown>, context: SvedaFrontendToolContext): Promise<unknown>;
20
+ clear(): void;
21
+ }
22
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAEpE,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,OAAO,EAAE,CACP,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,wBAAwB,KAC9B,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,KAAK,CAAwC;IAErD,QAAQ,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,IAAI;IAM7C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI9B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAIhD,IAAI,IAAI,yBAAyB,EAAE;IAQ7B,OAAO,CACX,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,OAAO,CAAC;IASnB,KAAK,IAAI,IAAI;CAGd"}
package/dist/tools.js ADDED
@@ -0,0 +1,34 @@
1
+ export class SvedaToolRegistry {
2
+ tools = new Map();
3
+ register(tool) {
4
+ this.tools.set(tool.name, tool);
5
+ return () => this.unregister(tool.name);
6
+ }
7
+ unregister(name) {
8
+ this.tools.delete(name);
9
+ }
10
+ has(name) {
11
+ return this.tools.has(name);
12
+ }
13
+ get(name) {
14
+ return this.tools.get(name);
15
+ }
16
+ list() {
17
+ return [...this.tools.values()].map(tool => ({
18
+ name: tool.name,
19
+ description: tool.description,
20
+ parameters: tool.parameters,
21
+ }));
22
+ }
23
+ async execute(name, input, context) {
24
+ const tool = this.tools.get(name);
25
+ if (!tool) {
26
+ throw new Error(`[sveda] Unknown frontend tool: ${name}`);
27
+ }
28
+ return tool.handler(input, context);
29
+ }
30
+ clear() {
31
+ this.tools.clear();
32
+ }
33
+ }
34
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAiBA,MAAM,OAAO,iBAAiB;IACpB,KAAK,GAAG,IAAI,GAAG,EAA6B,CAAC;IAErD,QAAQ,CAAC,IAAuB;QAC9B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAEhC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1C,CAAC;IAED,UAAU,CAAC,IAAY;QACrB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC3C,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,UAAU,EAAE,IAAI,CAAC,UAAU;SAC5B,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,OAAO,CACX,IAAY,EACZ,KAA8B,EAC9B,OAAiC;QAEjC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,kCAAkC,IAAI,EAAE,CAAC,CAAC;QAC5D,CAAC;QAED,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACtC,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACF"}
@@ -0,0 +1,46 @@
1
+ import type { SvedaRenderHint, SvedaToolTarget } from '@sveda-ai/protocol';
2
+ export type SvedaMessageRole = 'user' | 'assistant' | 'system' | 'tool';
3
+ export type SvedaMessagePart = {
4
+ type: 'text';
5
+ text: string;
6
+ } | {
7
+ type: 'reasoning';
8
+ text: string;
9
+ } | {
10
+ type: 'tool-call';
11
+ toolCallId: string;
12
+ toolName: string;
13
+ target: SvedaToolTarget;
14
+ input: Record<string, unknown>;
15
+ } | {
16
+ type: 'tool-result';
17
+ toolCallId: string;
18
+ toolName: string;
19
+ output: unknown;
20
+ renderHint?: SvedaRenderHint;
21
+ renderData?: Record<string, unknown>;
22
+ } | {
23
+ type: 'file';
24
+ name: string;
25
+ mediaType?: string;
26
+ url?: string;
27
+ };
28
+ export interface SvedaDisplayMessage {
29
+ id: string;
30
+ role: SvedaMessageRole;
31
+ parts: SvedaMessagePart[];
32
+ createdAt?: string;
33
+ }
34
+ export type SvedaSessionStatus = 'idle' | 'submitted' | 'streaming' | 'error';
35
+ export interface SvedaChatHistorySummary {
36
+ chatId: string;
37
+ title: string;
38
+ updatedAt?: string;
39
+ [key: string]: unknown;
40
+ }
41
+ export interface SvedaChatHistoryDetail extends SvedaChatHistorySummary {
42
+ messages: SvedaDisplayMessage[];
43
+ }
44
+ export declare function messageText(message: SvedaDisplayMessage): string;
45
+ export declare function createMessageId(): string;
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE3E,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC;AAExE,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACnC;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,GACD;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC,GACD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,gBAAgB,CAAC;IACvB,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,GAAG,OAAO,CAAC;AAE9E,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,sBAAuB,SAAQ,uBAAuB;IACrE,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAKhE;AAED,wBAAgB,eAAe,IAAI,MAAM,CAMxC"}
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ export function messageText(message) {
2
+ return message.parts
3
+ .filter((part) => part.type === 'text')
4
+ .map(part => part.text)
5
+ .join('');
6
+ }
7
+ export function createMessageId() {
8
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
9
+ return crypto.randomUUID();
10
+ }
11
+ return `msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
12
+ }
13
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA4CA,MAAM,UAAU,WAAW,CAAC,OAA4B;IACtD,OAAO,OAAO,CAAC,KAAK;SACjB,MAAM,CAAC,CAAC,IAAI,EAAuD,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC;SAC3F,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;SACtB,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC7E,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;IAC7B,CAAC;IAED,OAAO,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACrF,CAAC"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@sveda-ai/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Framework-agnostic Sveda JS client — streaming, tools, and session.send.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/neresson/sveda.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "homepage": "https://github.com/neresson/sveda/tree/main/packages/core",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src"
27
+ ],
28
+ "dependencies": {
29
+ "@sveda-ai/protocol": "0.1.0"
30
+ },
31
+ "devDependencies": {
32
+ "typescript": "^5.8.2",
33
+ "vitest": "^4.1.4"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json",
37
+ "test": "vitest run",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit",
39
+ "lint": "echo 'no lint configured'"
40
+ }
41
+ }
package/src/client.ts ADDED
@@ -0,0 +1,492 @@
1
+ import type { SvedaStreamEvent, SvedaStreamRequest } from '@sveda-ai/protocol';
2
+ import { SvedaContextRegistry } from './context.js';
3
+ import { iterateSvedaStream, type SvedaStreamProtocolMode } from './streaming.js';
4
+ import { SvedaToolRegistry } from './tools.js';
5
+ import {
6
+ createMessageId,
7
+ messageText,
8
+ type SvedaChatHistoryDetail,
9
+ type SvedaChatHistorySummary,
10
+ type SvedaDisplayMessage,
11
+ type SvedaMessagePart,
12
+ type SvedaSessionStatus,
13
+ } from './types.js';
14
+
15
+ export interface SvedaClientEndpoints {
16
+ stream: string;
17
+ message?: string;
18
+ histories?: string;
19
+ history?: (chatId: string) => string;
20
+ documentsExtract?: string;
21
+ }
22
+
23
+ export interface SvedaClientOptions {
24
+ endpoints: SvedaClientEndpoints;
25
+ protocolMode?: SvedaStreamProtocolMode;
26
+ headers?: Record<string, string> | (() => Record<string, string>);
27
+ credentials?: RequestCredentials;
28
+ tools?: SvedaToolRegistry;
29
+ context?: SvedaContextRegistry;
30
+ autoSubmitFrontendToolResults?: boolean;
31
+ fetchFn?: typeof fetch;
32
+ }
33
+
34
+ export interface SvedaSendOptions {
35
+ context?: Record<string, unknown>;
36
+ model?: string;
37
+ provider?: string;
38
+ options?: Record<string, unknown>;
39
+ files?: Array<{ name: string; mediaType?: string; url?: string }>;
40
+ }
41
+
42
+ type SessionListener = (payload: unknown) => void;
43
+
44
+ class SessionEmitter {
45
+ private listeners = new Map<string, Set<SessionListener>>();
46
+
47
+ on(event: string, listener: SessionListener): () => void {
48
+ if (!this.listeners.has(event)) {
49
+ this.listeners.set(event, new Set());
50
+ }
51
+ this.listeners.get(event)!.add(listener);
52
+
53
+ return () => this.off(event, listener);
54
+ }
55
+
56
+ off(event: string, listener: SessionListener): void {
57
+ this.listeners.get(event)?.delete(listener);
58
+ }
59
+
60
+ emit(event: string, payload?: unknown): void {
61
+ for (const listener of this.listeners.get(event) ?? []) {
62
+ listener(payload);
63
+ }
64
+ }
65
+ }
66
+
67
+ export class SvedaChatSession {
68
+ readonly chatId: string;
69
+
70
+ messages: SvedaDisplayMessage[] = [];
71
+
72
+ status: SvedaSessionStatus = 'idle';
73
+
74
+ private readonly emitter = new SessionEmitter();
75
+
76
+ private abortController: AbortController | null = null;
77
+
78
+ private pendingFrontendResults: SvedaDisplayMessage[] = [];
79
+
80
+ constructor(
81
+ chatId: string,
82
+ private readonly client: SvedaClient
83
+ ) {
84
+ this.chatId = chatId;
85
+ }
86
+
87
+ on(event: string, listener: SessionListener): () => void {
88
+ return this.emitter.on(event, listener);
89
+ }
90
+
91
+ get isStreaming(): boolean {
92
+ return this.status === 'streaming' || this.status === 'submitted';
93
+ }
94
+
95
+ setMessages(messages: SvedaDisplayMessage[]): void {
96
+ this.messages = messages;
97
+ this.emitter.emit('messages', this.messages);
98
+ }
99
+
100
+ stop(): void {
101
+ this.abortController?.abort();
102
+ }
103
+
104
+ async send(text: string, sendOptions: SvedaSendOptions = {}): Promise<void> {
105
+ const userMessage: SvedaDisplayMessage = {
106
+ id: createMessageId(),
107
+ role: 'user',
108
+ parts: [
109
+ { type: 'text', text },
110
+ ...(sendOptions.files ?? []).map(
111
+ (file): SvedaMessagePart => ({
112
+ type: 'file',
113
+ name: file.name,
114
+ mediaType: file.mediaType,
115
+ url: file.url,
116
+ })
117
+ ),
118
+ ],
119
+ };
120
+
121
+ this.messages = [...this.messages, userMessage];
122
+ this.emitter.emit('messages', this.messages);
123
+
124
+ await this.streamRequest(text, sendOptions);
125
+ }
126
+
127
+ async continueAfterMaxSteps(sendOptions: SvedaSendOptions = {}): Promise<void> {
128
+ await this.streamRequest('Continue', sendOptions);
129
+ }
130
+
131
+ private buildRequestBody(prompt: string, sendOptions: SvedaSendOptions): SvedaStreamRequest {
132
+ const context: Record<string, unknown> = {
133
+ ...this.client.contextRegistry.snapshot(),
134
+ ...(sendOptions.context ?? {}),
135
+ };
136
+
137
+ return {
138
+ messages: this.messages.map(message => ({
139
+ id: message.id,
140
+ role: message.role,
141
+ content: messageText(message),
142
+ parts: message.parts as Array<Record<string, unknown>>,
143
+ })),
144
+ prompt,
145
+ chatId: this.chatId,
146
+ context,
147
+ clientTools: this.client.toolRegistry.list(),
148
+ model: sendOptions.model,
149
+ provider: sendOptions.provider,
150
+ options: sendOptions.options,
151
+ };
152
+ }
153
+
154
+ private async streamRequest(prompt: string, sendOptions: SvedaSendOptions): Promise<void> {
155
+ const requestBody = this.buildRequestBody(prompt, sendOptions);
156
+
157
+ this.status = 'submitted';
158
+ this.emitter.emit('status', this.status);
159
+ this.abortController = new AbortController();
160
+
161
+ const assistantMessage: SvedaDisplayMessage = {
162
+ id: createMessageId(),
163
+ role: 'assistant',
164
+ parts: [],
165
+ };
166
+ this.messages = [...this.messages, assistantMessage];
167
+ this.emitter.emit('messages', this.messages);
168
+
169
+ try {
170
+ const response = await this.client.fetchImpl(this.client.endpoints.stream, {
171
+ method: 'POST',
172
+ headers: {
173
+ 'Content-Type': 'application/json',
174
+ ...this.client.streamAcceptHeaders(),
175
+ ...this.client.resolveHeaders(),
176
+ },
177
+ credentials: this.client.credentials,
178
+ body: JSON.stringify(requestBody),
179
+ signal: this.abortController.signal,
180
+ });
181
+
182
+ if (!response.ok) {
183
+ throw new Error(`[sveda] Stream request failed: ${response.status}`);
184
+ }
185
+
186
+ this.status = 'streaming';
187
+ this.emitter.emit('status', this.status);
188
+
189
+ for await (const event of iterateSvedaStream(response, this.client.protocolMode)) {
190
+ this.applyStreamEvent(event);
191
+ }
192
+
193
+ this.status = 'idle';
194
+ this.emitter.emit('status', this.status);
195
+
196
+ if (
197
+ this.client.autoSubmitFrontendToolResults &&
198
+ this.pendingFrontendResults.length > 0
199
+ ) {
200
+ const results = this.pendingFrontendResults;
201
+ this.pendingFrontendResults = [];
202
+ this.messages = [...this.messages, ...results];
203
+ this.emitter.emit('messages', this.messages);
204
+ await this.streamRequest('tool_results', sendOptions);
205
+ }
206
+ } catch (error) {
207
+ if (error instanceof DOMException && error.name === 'AbortError') {
208
+ this.status = 'idle';
209
+ this.emitter.emit('status', this.status);
210
+ return;
211
+ }
212
+
213
+ this.status = 'error';
214
+ this.emitter.emit('status', this.status);
215
+ this.emitter.emit('error', error);
216
+ throw error;
217
+ }
218
+ }
219
+
220
+ private applyStreamEvent(event: SvedaStreamEvent): void {
221
+ this.emitter.emit('event', event);
222
+
223
+ const assistant = this.messages[this.messages.length - 1];
224
+ if (!assistant || assistant.role !== 'assistant') {
225
+ return;
226
+ }
227
+
228
+ const mutate = (mutator: (parts: SvedaMessagePart[]) => SvedaMessagePart[]) => {
229
+ const updated: SvedaDisplayMessage = {
230
+ ...assistant,
231
+ parts: mutator(assistant.parts),
232
+ };
233
+ this.messages = [...this.messages.slice(0, -1), updated];
234
+ this.emitter.emit('messages', this.messages);
235
+ };
236
+
237
+ switch (event.type) {
238
+ case 'text.delta':
239
+ mutate(parts => {
240
+ const last = parts[parts.length - 1];
241
+ if (last && last.type === 'text') {
242
+ return [...parts.slice(0, -1), { ...last, text: last.text + event.delta }];
243
+ }
244
+ return [...parts, { type: 'text', text: event.delta }];
245
+ });
246
+ break;
247
+
248
+ case 'reasoning.delta':
249
+ mutate(parts => {
250
+ const last = parts[parts.length - 1];
251
+ if (last && last.type === 'reasoning') {
252
+ return [...parts.slice(0, -1), { ...last, text: last.text + event.delta }];
253
+ }
254
+ return [...parts, { type: 'reasoning', text: event.delta }];
255
+ });
256
+ break;
257
+
258
+ case 'tool.call':
259
+ mutate(parts => [
260
+ ...parts,
261
+ {
262
+ type: 'tool-call',
263
+ toolCallId: event.toolCallId,
264
+ toolName: event.toolName,
265
+ target: event.target,
266
+ input: event.input,
267
+ },
268
+ ]);
269
+
270
+ if (event.target === 'frontend') {
271
+ void this.executeFrontendTool(event);
272
+ }
273
+ break;
274
+
275
+ case 'tool.result':
276
+ mutate(parts => [
277
+ ...parts,
278
+ {
279
+ type: 'tool-result',
280
+ toolCallId: event.toolCallId,
281
+ toolName: event.toolName,
282
+ output: event.output,
283
+ renderHint: event.renderHint,
284
+ renderData: event.renderData,
285
+ },
286
+ ]);
287
+ break;
288
+
289
+ case 'tool.progress':
290
+ this.emitter.emit('toolProgress', event);
291
+ break;
292
+
293
+ case 'context.usage':
294
+ this.emitter.emit('contextUsage', event);
295
+ break;
296
+
297
+ case 'chat.title':
298
+ this.emitter.emit('title', event.title);
299
+ break;
300
+
301
+ case 'max_steps':
302
+ this.emitter.emit('maxSteps', event);
303
+ break;
304
+
305
+ case 'message.end':
306
+ this.emitter.emit('finish', event);
307
+ break;
308
+
309
+ case 'error':
310
+ this.status = 'error';
311
+ this.emitter.emit('status', this.status);
312
+ this.emitter.emit('error', event);
313
+ this.abortController?.abort();
314
+ break;
315
+ }
316
+ }
317
+
318
+ private async executeFrontendTool(
319
+ event: Extract<SvedaStreamEvent, { type: 'tool.call' }>
320
+ ): Promise<void> {
321
+ let output: unknown;
322
+ try {
323
+ output = await this.client.toolRegistry.execute(event.toolName, event.input, {
324
+ chatId: this.chatId,
325
+ toolCallId: event.toolCallId,
326
+ });
327
+ } catch (error) {
328
+ output = { error: error instanceof Error ? error.message : String(error) };
329
+ }
330
+
331
+ this.pendingFrontendResults.push({
332
+ id: createMessageId(),
333
+ role: 'tool',
334
+ parts: [
335
+ {
336
+ type: 'tool-result',
337
+ toolCallId: event.toolCallId,
338
+ toolName: event.toolName,
339
+ output,
340
+ },
341
+ ],
342
+ });
343
+ }
344
+ }
345
+
346
+ export class SvedaClient {
347
+ readonly endpoints: SvedaClientEndpoints;
348
+
349
+ readonly protocolMode: SvedaStreamProtocolMode;
350
+
351
+ readonly toolRegistry: SvedaToolRegistry;
352
+
353
+ readonly contextRegistry: SvedaContextRegistry;
354
+
355
+ readonly autoSubmitFrontendToolResults: boolean;
356
+
357
+ readonly fetchImpl: typeof fetch;
358
+
359
+ readonly credentials?: RequestCredentials;
360
+
361
+ private readonly headers?: Record<string, string> | (() => Record<string, string>);
362
+
363
+ private sessions = new Map<string, SvedaChatSession>();
364
+
365
+ constructor(options: SvedaClientOptions) {
366
+ this.endpoints = options.endpoints;
367
+ this.protocolMode = options.protocolMode ?? 'sveda';
368
+ this.headers = options.headers;
369
+ this.credentials = options.credentials;
370
+ this.toolRegistry = options.tools ?? new SvedaToolRegistry();
371
+ this.contextRegistry = options.context ?? new SvedaContextRegistry();
372
+ this.autoSubmitFrontendToolResults = options.autoSubmitFrontendToolResults ?? true;
373
+ this.fetchImpl = options.fetchFn ?? ((input, init) => globalThis.fetch(input, init));
374
+ }
375
+
376
+ resolveHeaders(): Record<string, string> {
377
+ if (typeof this.headers === 'function') {
378
+ return this.headers();
379
+ }
380
+
381
+ return this.headers ?? {};
382
+ }
383
+
384
+ streamAcceptHeaders(): Record<string, string> {
385
+ if (this.protocolMode === 'sveda') {
386
+ return { Accept: 'application/vnd.sveda.stream+json' };
387
+ }
388
+
389
+ return { Accept: 'text/event-stream', 'X-Sveda-Protocol': 'vercel' };
390
+ }
391
+
392
+ session(chatId: string): SvedaChatSession {
393
+ if (!this.sessions.has(chatId)) {
394
+ this.sessions.set(chatId, new SvedaChatSession(chatId, this));
395
+ }
396
+
397
+ return this.sessions.get(chatId)!;
398
+ }
399
+
400
+ createSession(chatId: string = createMessageId()): SvedaChatSession {
401
+ return this.session(chatId);
402
+ }
403
+
404
+ destroySession(chatId: string): void {
405
+ this.sessions.get(chatId)?.stop();
406
+ this.sessions.delete(chatId);
407
+ }
408
+
409
+ private historiesUrl(): string {
410
+ if (!this.endpoints.histories) {
411
+ throw new Error('[sveda] histories endpoint is not configured');
412
+ }
413
+
414
+ return this.endpoints.histories;
415
+ }
416
+
417
+ private historyUrl(chatId: string): string {
418
+ if (this.endpoints.history) {
419
+ return this.endpoints.history(chatId);
420
+ }
421
+
422
+ return `${this.historiesUrl().replace(/\/$/, '')}/${encodeURIComponent(chatId)}`;
423
+ }
424
+
425
+ async listHistories(): Promise<SvedaChatHistorySummary[]> {
426
+ const response = await this.fetchImpl(this.historiesUrl(), {
427
+ headers: { Accept: 'application/json', ...this.resolveHeaders() },
428
+ credentials: this.credentials,
429
+ });
430
+
431
+ if (!response.ok) {
432
+ throw new Error(`[sveda] Failed to load chat histories: ${response.status}`);
433
+ }
434
+
435
+ const data = (await response.json()) as { histories?: SvedaChatHistorySummary[] };
436
+ return (data.histories ?? []).map(history => this.normalizeHistory(history));
437
+ }
438
+
439
+ async getHistory(chatId: string): Promise<SvedaChatHistoryDetail> {
440
+ const response = await this.fetchImpl(this.historyUrl(chatId), {
441
+ headers: { Accept: 'application/json', ...this.resolveHeaders() },
442
+ credentials: this.credentials,
443
+ });
444
+
445
+ if (!response.ok) {
446
+ throw new Error(`[sveda] Failed to load chat history ${chatId}: ${response.status}`);
447
+ }
448
+
449
+ const data = (await response.json()) as { history: SvedaChatHistoryDetail };
450
+ return this.normalizeHistory(data.history);
451
+ }
452
+
453
+ async renameHistory(chatId: string, title: string): Promise<void> {
454
+ const response = await this.fetchImpl(this.historyUrl(chatId), {
455
+ method: 'PATCH',
456
+ headers: {
457
+ 'Content-Type': 'application/json',
458
+ Accept: 'application/json',
459
+ ...this.resolveHeaders(),
460
+ },
461
+ credentials: this.credentials,
462
+ body: JSON.stringify({ title }),
463
+ });
464
+
465
+ if (!response.ok) {
466
+ throw new Error(`[sveda] Failed to rename chat ${chatId}: ${response.status}`);
467
+ }
468
+ }
469
+
470
+ async deleteHistory(chatId: string): Promise<void> {
471
+ const response = await this.fetchImpl(this.historyUrl(chatId), {
472
+ method: 'DELETE',
473
+ headers: { Accept: 'application/json', ...this.resolveHeaders() },
474
+ credentials: this.credentials,
475
+ });
476
+
477
+ if (!response.ok) {
478
+ throw new Error(`[sveda] Failed to delete chat ${chatId}: ${response.status}`);
479
+ }
480
+ }
481
+
482
+ private normalizeHistory<T extends SvedaChatHistorySummary>(history: T): T {
483
+ const fromChatId = typeof history.chatId === 'string' ? history.chatId.trim() : '';
484
+ if (fromChatId !== '') {
485
+ return { ...history, chatId: fromChatId };
486
+ }
487
+
488
+ const fromId = typeof history.id === 'string' ? history.id.trim() : '';
489
+
490
+ return { ...history, chatId: fromId };
491
+ }
492
+ }