@surf-kit/agent 0.2.2 → 0.4.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.
Files changed (51) hide show
  1. package/dist/chat/index.cjs +733 -222
  2. package/dist/chat/index.cjs.map +1 -1
  3. package/dist/chat/index.d.cts +13 -7
  4. package/dist/chat/index.d.ts +13 -7
  5. package/dist/chat/index.js +715 -204
  6. package/dist/chat/index.js.map +1 -1
  7. package/dist/{chat-ChYl2XjV.d.cts → chat-BRY3xGg_.d.cts} +11 -2
  8. package/dist/{chat--OifhIRe.d.ts → chat-CcKc6OAR.d.ts} +11 -2
  9. package/dist/{hooks-BGs8-4GK.d.ts → hooks-BLeiVk-x.d.ts} +22 -5
  10. package/dist/{hooks-DLfF18IU.d.cts → hooks-CSGGLd7j.d.cts} +22 -5
  11. package/dist/hooks.cjs +160 -85
  12. package/dist/hooks.cjs.map +1 -1
  13. package/dist/hooks.d.cts +3 -3
  14. package/dist/hooks.d.ts +3 -3
  15. package/dist/hooks.js +160 -85
  16. package/dist/hooks.js.map +1 -1
  17. package/dist/index.cjs +794 -283
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +3 -3
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.js +758 -247
  22. package/dist/index.js.map +1 -1
  23. package/dist/layouts/index.cjs +754 -243
  24. package/dist/layouts/index.cjs.map +1 -1
  25. package/dist/layouts/index.d.cts +1 -1
  26. package/dist/layouts/index.d.ts +1 -1
  27. package/dist/layouts/index.js +733 -222
  28. package/dist/layouts/index.js.map +1 -1
  29. package/dist/mcp/index.cjs +1 -1
  30. package/dist/mcp/index.cjs.map +1 -1
  31. package/dist/mcp/index.js +2 -2
  32. package/dist/mcp/index.js.map +1 -1
  33. package/dist/response/index.cjs +100 -15
  34. package/dist/response/index.cjs.map +1 -1
  35. package/dist/response/index.d.cts +2 -2
  36. package/dist/response/index.d.ts +2 -2
  37. package/dist/response/index.js +99 -14
  38. package/dist/response/index.js.map +1 -1
  39. package/dist/sources/index.cjs +30 -1
  40. package/dist/sources/index.cjs.map +1 -1
  41. package/dist/sources/index.js +30 -1
  42. package/dist/sources/index.js.map +1 -1
  43. package/dist/streaming/index.cjs +213 -93
  44. package/dist/streaming/index.cjs.map +1 -1
  45. package/dist/streaming/index.d.cts +4 -3
  46. package/dist/streaming/index.d.ts +4 -3
  47. package/dist/streaming/index.js +183 -73
  48. package/dist/streaming/index.js.map +1 -1
  49. package/dist/{streaming-DfT22A0z.d.cts → streaming-BHPXnwwo.d.cts} +3 -1
  50. package/dist/{streaming-DbQxScpi.d.ts → streaming-C6mbU7My.d.ts} +3 -1
  51. package/package.json +17 -5
@@ -1,5 +1,5 @@
1
- import { a as ChatMessage, C as ChatError, b as ConversationSummary } from './chat--OifhIRe.js';
2
- import { a as StreamState, S as StreamEvent } from './streaming-DbQxScpi.js';
1
+ import { A as Attachment, a as ChatMessage, C as ChatError, b as ConversationSummary } from './chat-CcKc6OAR.js';
2
+ import { a as StreamState, S as StreamEvent } from './streaming-C6mbU7My.js';
3
3
  import { A as AgentInfo } from './agent-BNSmiexZ.js';
4
4
 
5
5
  interface AgentChatConfig {
@@ -11,14 +11,29 @@ interface AgentChatConfig {
11
11
  feedbackPath?: string;
12
12
  /** Conversations endpoint path (appended to apiUrl) */
13
13
  conversationsPath?: string;
14
- /** Request headers (e.g. Authorization) */
15
- headers?: Record<string, string>;
14
+ /** Request headers (e.g. Authorization). Can be a static object or an async function that returns headers (useful for refreshing auth tokens). */
15
+ headers?: Record<string, string> | (() => Promise<Record<string, string>>);
16
16
  /** Request timeout in milliseconds */
17
17
  timeout?: number;
18
18
  /** Enable localStorage persistence for conversations */
19
19
  persistConversations?: boolean;
20
20
  /** Map of agent IDs to their display config */
21
21
  agentThemes?: Record<string, AgentInfo>;
22
+ /** Extra fields merged into every request body (e.g. `{ agent: "research" }`).
23
+ * Keys here are shallow-merged after the default fields (message, conversation_id, attachments). */
24
+ bodyExtra?: Record<string, unknown>;
25
+ /** Custom stream reader for environments without ReadableStream (e.g. React Native).
26
+ * When provided, this function handles sending the request and parsing SSE events
27
+ * instead of the default fetch + getReader() approach. */
28
+ streamAdapter?: (url: string, options: {
29
+ method: string;
30
+ headers: Record<string, string>;
31
+ body: string;
32
+ signal: AbortSignal;
33
+ }, onEvent: (event: {
34
+ type: string;
35
+ [key: string]: unknown;
36
+ }) => void) => Promise<void>;
22
37
  }
23
38
 
24
39
  interface AgentChatState {
@@ -29,13 +44,15 @@ interface AgentChatState {
29
44
  inputValue: string;
30
45
  streamPhase: StreamState['phase'];
31
46
  streamingContent: string;
47
+ streamingAgent: string | null;
32
48
  }
33
49
  interface AgentChatActions {
34
- sendMessage: (content: string) => Promise<void>;
50
+ sendMessage: (content: string, attachments?: Attachment[]) => Promise<void>;
35
51
  setInputValue: (value: string) => void;
36
52
  loadConversation: (conversationId: string, messages: ChatMessage[]) => void;
37
53
  submitFeedback: (messageId: string, rating: 'positive' | 'negative', comment?: string) => Promise<void>;
38
54
  retry: () => Promise<void>;
55
+ stop: () => void;
39
56
  reset: () => void;
40
57
  }
41
58
  declare function useAgentChat(config: AgentChatConfig): {
@@ -1,5 +1,5 @@
1
- import { a as ChatMessage, C as ChatError, b as ConversationSummary } from './chat-ChYl2XjV.cjs';
2
- import { a as StreamState, S as StreamEvent } from './streaming-DfT22A0z.cjs';
1
+ import { A as Attachment, a as ChatMessage, C as ChatError, b as ConversationSummary } from './chat-BRY3xGg_.cjs';
2
+ import { a as StreamState, S as StreamEvent } from './streaming-BHPXnwwo.cjs';
3
3
  import { A as AgentInfo } from './agent-BNSmiexZ.cjs';
4
4
 
5
5
  interface AgentChatConfig {
@@ -11,14 +11,29 @@ interface AgentChatConfig {
11
11
  feedbackPath?: string;
12
12
  /** Conversations endpoint path (appended to apiUrl) */
13
13
  conversationsPath?: string;
14
- /** Request headers (e.g. Authorization) */
15
- headers?: Record<string, string>;
14
+ /** Request headers (e.g. Authorization). Can be a static object or an async function that returns headers (useful for refreshing auth tokens). */
15
+ headers?: Record<string, string> | (() => Promise<Record<string, string>>);
16
16
  /** Request timeout in milliseconds */
17
17
  timeout?: number;
18
18
  /** Enable localStorage persistence for conversations */
19
19
  persistConversations?: boolean;
20
20
  /** Map of agent IDs to their display config */
21
21
  agentThemes?: Record<string, AgentInfo>;
22
+ /** Extra fields merged into every request body (e.g. `{ agent: "research" }`).
23
+ * Keys here are shallow-merged after the default fields (message, conversation_id, attachments). */
24
+ bodyExtra?: Record<string, unknown>;
25
+ /** Custom stream reader for environments without ReadableStream (e.g. React Native).
26
+ * When provided, this function handles sending the request and parsing SSE events
27
+ * instead of the default fetch + getReader() approach. */
28
+ streamAdapter?: (url: string, options: {
29
+ method: string;
30
+ headers: Record<string, string>;
31
+ body: string;
32
+ signal: AbortSignal;
33
+ }, onEvent: (event: {
34
+ type: string;
35
+ [key: string]: unknown;
36
+ }) => void) => Promise<void>;
22
37
  }
23
38
 
24
39
  interface AgentChatState {
@@ -29,13 +44,15 @@ interface AgentChatState {
29
44
  inputValue: string;
30
45
  streamPhase: StreamState['phase'];
31
46
  streamingContent: string;
47
+ streamingAgent: string | null;
32
48
  }
33
49
  interface AgentChatActions {
34
- sendMessage: (content: string) => Promise<void>;
50
+ sendMessage: (content: string, attachments?: Attachment[]) => Promise<void>;
35
51
  setInputValue: (value: string) => void;
36
52
  loadConversation: (conversationId: string, messages: ChatMessage[]) => void;
37
53
  submitFeedback: (messageId: string, rating: 'positive' | 'negative', comment?: string) => Promise<void>;
38
54
  retry: () => Promise<void>;
55
+ stop: () => void;
39
56
  reset: () => void;
40
57
  }
41
58
  declare function useAgentChat(config: AgentChatConfig): {
package/dist/hooks.cjs CHANGED
@@ -39,7 +39,8 @@ var initialState = {
39
39
  error: null,
40
40
  inputValue: "",
41
41
  streamPhase: "idle",
42
- streamingContent: ""
42
+ streamingContent: "",
43
+ streamingAgent: null
43
44
  };
44
45
  function reducer(state, action) {
45
46
  switch (action.type) {
@@ -53,12 +54,17 @@ function reducer(state, action) {
53
54
  error: null,
54
55
  inputValue: "",
55
56
  streamPhase: "thinking",
56
- streamingContent: ""
57
+ streamingContent: "",
58
+ streamingAgent: null
57
59
  };
58
60
  case "STREAM_PHASE":
59
61
  return { ...state, streamPhase: action.phase };
60
62
  case "STREAM_CONTENT":
61
63
  return { ...state, streamingContent: state.streamingContent + action.content };
64
+ case "STREAM_CONTENT_RESET":
65
+ return { ...state, streamingContent: "" };
66
+ case "STREAM_AGENT":
67
+ return { ...state, streamingAgent: action.agent };
62
68
  case "SEND_SUCCESS":
63
69
  return {
64
70
  ...state,
@@ -74,7 +80,8 @@ function reducer(state, action) {
74
80
  isLoading: false,
75
81
  error: action.error,
76
82
  streamPhase: "idle",
77
- streamingContent: ""
83
+ streamingContent: "",
84
+ streamingAgent: null
78
85
  };
79
86
  case "LOAD_CONVERSATION":
80
87
  return {
@@ -100,115 +107,172 @@ function useAgentChat(config) {
100
107
  const configRef = (0, import_react.useRef)(config);
101
108
  configRef.current = config;
102
109
  const lastUserMessageRef = (0, import_react.useRef)(null);
110
+ const lastUserAttachmentsRef = (0, import_react.useRef)(void 0);
111
+ const abortControllerRef = (0, import_react.useRef)(null);
103
112
  const sendMessage = (0, import_react.useCallback)(
104
- async (content) => {
105
- const { apiUrl, streamPath = "/chat/stream", headers = {}, timeout = 3e4 } = configRef.current;
113
+ async (content, attachments) => {
114
+ const { apiUrl, streamPath = "/chat/stream", headers: headersOrFn, timeout = 3e4, bodyExtra } = configRef.current;
115
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
106
116
  lastUserMessageRef.current = content;
117
+ lastUserAttachmentsRef.current = attachments;
107
118
  const userMessage = {
108
119
  id: generateMessageId(),
109
120
  role: "user",
110
121
  content,
122
+ attachments,
111
123
  timestamp: /* @__PURE__ */ new Date()
112
124
  };
113
125
  dispatch({ type: "SEND_START", message: userMessage });
114
126
  const controller = new AbortController();
127
+ abortControllerRef.current = controller;
115
128
  const timeoutId = setTimeout(() => controller.abort(), timeout);
129
+ const ctx = {
130
+ accumulatedContent: "",
131
+ agentResponse: null,
132
+ capturedAgent: null,
133
+ capturedConversationId: null,
134
+ hadStreamError: false
135
+ };
116
136
  try {
117
- const response = await fetch(`${apiUrl}${streamPath}`, {
118
- method: "POST",
119
- headers: {
120
- "Content-Type": "application/json",
121
- Accept: "text/event-stream",
122
- ...headers
123
- },
124
- body: JSON.stringify({
125
- message: content,
126
- conversation_id: state.conversationId
127
- }),
128
- signal: controller.signal
129
- });
130
- clearTimeout(timeoutId);
131
- if (!response.ok) {
132
- dispatch({
133
- type: "SEND_ERROR",
134
- error: {
135
- code: "API_ERROR",
136
- message: `HTTP ${response.status}: ${response.statusText}`,
137
- retryable: response.status >= 500
138
- }
139
- });
140
- return;
137
+ const url = `${apiUrl}${streamPath}`;
138
+ const mergedHeaders = {
139
+ "Content-Type": "application/json",
140
+ Accept: "text/event-stream",
141
+ ...headers
142
+ };
143
+ const requestBody = {
144
+ message: content,
145
+ conversation_id: state.conversationId,
146
+ ...bodyExtra
147
+ };
148
+ if (attachments && attachments.length > 0) {
149
+ requestBody.attachments = attachments.map((a) => ({
150
+ filename: a.filename,
151
+ content_type: a.content_type,
152
+ data: a.data
153
+ }));
141
154
  }
142
- const reader = response.body?.getReader();
143
- if (!reader) {
144
- dispatch({
145
- type: "SEND_ERROR",
146
- error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
155
+ const body = JSON.stringify(requestBody);
156
+ const handleEvent = (event) => {
157
+ switch (event.type) {
158
+ case "agent":
159
+ ctx.capturedAgent = event.agent;
160
+ dispatch({ type: "STREAM_AGENT", agent: ctx.capturedAgent });
161
+ break;
162
+ case "phase":
163
+ dispatch({ type: "STREAM_PHASE", phase: event.phase });
164
+ break;
165
+ case "delta":
166
+ ctx.accumulatedContent += event.content;
167
+ dispatch({ type: "STREAM_CONTENT", content: event.content });
168
+ break;
169
+ case "delta_reset":
170
+ ctx.accumulatedContent = "";
171
+ dispatch({ type: "STREAM_CONTENT_RESET" });
172
+ break;
173
+ case "done":
174
+ ctx.agentResponse = event.response;
175
+ ctx.capturedConversationId = event.conversation_id ?? null;
176
+ break;
177
+ case "error":
178
+ ctx.hadStreamError = true;
179
+ dispatch({ type: "SEND_ERROR", error: event.error });
180
+ break;
181
+ }
182
+ };
183
+ const { streamAdapter } = configRef.current;
184
+ if (streamAdapter) {
185
+ await streamAdapter(
186
+ url,
187
+ { method: "POST", headers: mergedHeaders, body, signal: controller.signal },
188
+ handleEvent
189
+ );
190
+ clearTimeout(timeoutId);
191
+ } else {
192
+ const response = await fetch(url, {
193
+ method: "POST",
194
+ headers: mergedHeaders,
195
+ body,
196
+ signal: controller.signal
147
197
  });
148
- return;
149
- }
150
- const decoder = new TextDecoder();
151
- let buffer = "";
152
- let accumulatedContent = "";
153
- let agentResponse = null;
154
- let capturedAgent = null;
155
- let capturedConversationId = null;
156
- while (true) {
157
- const { done, value } = await reader.read();
158
- if (done) break;
159
- buffer += decoder.decode(value, { stream: true });
160
- const lines = buffer.split("\n");
161
- buffer = lines.pop() ?? "";
162
- for (const line of lines) {
163
- if (!line.startsWith("data: ")) continue;
164
- const data = line.slice(6).trim();
165
- if (data === "[DONE]") continue;
166
- try {
167
- const event = JSON.parse(data);
168
- switch (event.type) {
169
- case "agent":
170
- capturedAgent = event.agent;
171
- break;
172
- case "phase":
173
- dispatch({ type: "STREAM_PHASE", phase: event.phase });
174
- break;
175
- case "delta":
176
- accumulatedContent += event.content;
177
- dispatch({ type: "STREAM_CONTENT", content: event.content });
178
- break;
179
- case "done":
180
- agentResponse = event.response;
181
- capturedConversationId = event.conversation_id ?? null;
182
- break;
183
- case "error":
184
- dispatch({ type: "SEND_ERROR", error: event.error });
185
- return;
198
+ clearTimeout(timeoutId);
199
+ if (!response.ok) {
200
+ dispatch({
201
+ type: "SEND_ERROR",
202
+ error: {
203
+ code: "API_ERROR",
204
+ message: `HTTP ${response.status}: ${response.statusText}`,
205
+ retryable: response.status >= 500
206
+ }
207
+ });
208
+ return;
209
+ }
210
+ const reader = response.body?.getReader();
211
+ if (!reader) {
212
+ dispatch({
213
+ type: "SEND_ERROR",
214
+ error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
215
+ });
216
+ return;
217
+ }
218
+ const decoder = new TextDecoder();
219
+ let buffer = "";
220
+ while (true) {
221
+ const { done, value } = await reader.read();
222
+ if (done) break;
223
+ buffer += decoder.decode(value, { stream: true });
224
+ const lines = buffer.split("\n");
225
+ buffer = lines.pop() ?? "";
226
+ for (const line of lines) {
227
+ if (!line.startsWith("data: ")) continue;
228
+ const data = line.slice(6).trim();
229
+ if (data === "[DONE]") continue;
230
+ try {
231
+ const event = JSON.parse(data);
232
+ handleEvent(event);
233
+ } catch {
186
234
  }
187
- } catch {
188
235
  }
189
236
  }
190
237
  }
238
+ if (ctx.hadStreamError) return;
191
239
  const assistantMessage = {
192
240
  id: generateMessageId(),
193
241
  role: "assistant",
194
- content: agentResponse?.message ?? accumulatedContent,
195
- response: agentResponse ?? void 0,
196
- agent: capturedAgent ?? void 0,
242
+ content: ctx.agentResponse?.message ?? ctx.accumulatedContent,
243
+ response: ctx.agentResponse ?? void 0,
244
+ agent: ctx.capturedAgent ?? void 0,
197
245
  timestamp: /* @__PURE__ */ new Date()
198
246
  };
199
247
  dispatch({
200
248
  type: "SEND_SUCCESS",
201
249
  message: assistantMessage,
202
- streamingContent: accumulatedContent,
203
- conversationId: capturedConversationId
250
+ streamingContent: ctx.accumulatedContent,
251
+ conversationId: ctx.capturedConversationId
204
252
  });
205
253
  } catch (err) {
206
254
  clearTimeout(timeoutId);
207
255
  if (err.name === "AbortError") {
208
- dispatch({
209
- type: "SEND_ERROR",
210
- error: { code: "TIMEOUT", message: "Request timed out", retryable: true }
211
- });
256
+ if (ctx.accumulatedContent) {
257
+ const partialMessage = {
258
+ id: generateMessageId(),
259
+ role: "assistant",
260
+ content: ctx.accumulatedContent,
261
+ agent: ctx.capturedAgent ?? void 0,
262
+ timestamp: /* @__PURE__ */ new Date()
263
+ };
264
+ dispatch({
265
+ type: "SEND_SUCCESS",
266
+ message: partialMessage,
267
+ streamingContent: ctx.accumulatedContent,
268
+ conversationId: ctx.capturedConversationId
269
+ });
270
+ } else {
271
+ dispatch({
272
+ type: "SEND_ERROR",
273
+ error: { code: "ABORTED", message: "Request stopped", retryable: true }
274
+ });
275
+ }
212
276
  } else {
213
277
  dispatch({
214
278
  type: "SEND_ERROR",
@@ -219,6 +283,8 @@ function useAgentChat(config) {
219
283
  }
220
284
  });
221
285
  }
286
+ } finally {
287
+ abortControllerRef.current = null;
222
288
  }
223
289
  },
224
290
  [state.conversationId]
@@ -231,7 +297,8 @@ function useAgentChat(config) {
231
297
  }, []);
232
298
  const submitFeedback = (0, import_react.useCallback)(
233
299
  async (messageId, rating, comment) => {
234
- const { apiUrl, feedbackPath = "/feedback", headers = {} } = configRef.current;
300
+ const { apiUrl, feedbackPath = "/feedback", headers: headersOrFn } = configRef.current;
301
+ const headers = typeof headersOrFn === "function" ? await headersOrFn() : headersOrFn ?? {};
235
302
  await fetch(`${apiUrl}${feedbackPath}`, {
236
303
  method: "POST",
237
304
  headers: { "Content-Type": "application/json", ...headers },
@@ -242,12 +309,16 @@ function useAgentChat(config) {
242
309
  );
243
310
  const retry = (0, import_react.useCallback)(async () => {
244
311
  if (lastUserMessageRef.current) {
245
- await sendMessage(lastUserMessageRef.current);
312
+ await sendMessage(lastUserMessageRef.current, lastUserAttachmentsRef.current);
246
313
  }
247
314
  }, [sendMessage]);
315
+ const stop = (0, import_react.useCallback)(() => {
316
+ abortControllerRef.current?.abort();
317
+ }, []);
248
318
  const reset = (0, import_react.useCallback)(() => {
249
319
  dispatch({ type: "RESET" });
250
320
  lastUserMessageRef.current = null;
321
+ lastUserAttachmentsRef.current = void 0;
251
322
  }, []);
252
323
  const actions = {
253
324
  sendMessage,
@@ -255,6 +326,7 @@ function useAgentChat(config) {
255
326
  loadConversation,
256
327
  submitFeedback,
257
328
  retry,
329
+ stop,
258
330
  reset
259
331
  };
260
332
  return { state, actions };
@@ -607,7 +679,10 @@ function useCharacterDrain(target, msPerChar = 15) {
607
679
  const elapsed = now - lastTimeRef.current;
608
680
  const charsToAdvance = Math.floor(elapsed / msPerCharRef.current);
609
681
  if (charsToAdvance > 0 && indexRef.current < currentTarget.length) {
610
- const nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
682
+ let nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
683
+ while (nextIndex < currentTarget.length && currentTarget[nextIndex - 1].trim() === "") {
684
+ nextIndex++;
685
+ }
611
686
  indexRef.current = nextIndex;
612
687
  lastTimeRef.current = now;
613
688
  setDisplayed(currentTarget.slice(0, nextIndex));