@sinas/sdk 0.7.0 → 0.8.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/dist/index.js CHANGED
@@ -6,6 +6,9 @@ import {
6
6
  SinasNotFoundError,
7
7
  SinasPermissionError,
8
8
  SinasTimeoutError,
9
+ answerPendingInput,
10
+ answerResumeChannel,
11
+ approvalResumeChannel,
9
12
  approveToolCall,
10
13
  cancelBatch,
11
14
  checkPermissions,
@@ -18,11 +21,17 @@ import {
18
21
  getChat,
19
22
  getInfo,
20
23
  getMe,
24
+ invokeAgent,
21
25
  listBatchExecutions,
22
26
  listCollections,
23
27
  listFiles,
28
+ listPendingInputs,
24
29
  login,
25
30
  logout,
31
+ parseApproval,
32
+ parsePendingInput,
33
+ parsePendingInputs,
34
+ pendingInputFromChunk,
26
35
  refresh,
27
36
  sendMessage,
28
37
  stateDelete,
@@ -35,7 +44,7 @@ import {
35
44
  submitFunctionBatch,
36
45
  uploadFile,
37
46
  verifyOTP
38
- } from "./chunk-QDIOYYBA.js";
47
+ } from "./chunk-RHQVV5O6.js";
39
48
 
40
49
  // src/hooks/useQuery.ts
41
50
  import React2 from "react";
@@ -218,29 +227,172 @@ function useStateStore(ns, key) {
218
227
 
219
228
  // src/hooks/useChat.ts
220
229
  import React5 from "react";
230
+
231
+ // src/hooks/chatState.ts
232
+ function parseMessageChunk(data) {
233
+ let parsed;
234
+ try {
235
+ parsed = JSON.parse(data);
236
+ } catch {
237
+ return { text: data };
238
+ }
239
+ if (!parsed || typeof parsed !== "object") return { text: "" };
240
+ const p = parsed;
241
+ const out = { text: typeof p.content === "string" ? p.content : "" };
242
+ if (typeof p.thinking === "string") out.thinking = p.thinking;
243
+ return out;
244
+ }
245
+ function applyTextDelta(messages, text, chatId, now = /* @__PURE__ */ new Date()) {
246
+ if (!text) return messages;
247
+ const last = messages[messages.length - 1];
248
+ if (last && last.role === "assistant" && !last.toolCalls && last.streaming) {
249
+ const next = [...messages];
250
+ next[next.length - 1] = { ...last, content: (last.content || "") + text };
251
+ return next;
252
+ }
253
+ return [
254
+ ...messages,
255
+ {
256
+ id: `tmp-text-${now.getTime()}`,
257
+ chatId,
258
+ role: "assistant",
259
+ content: text,
260
+ toolCalls: null,
261
+ toolCallId: null,
262
+ name: null,
263
+ createdAt: now.toISOString(),
264
+ streaming: true
265
+ }
266
+ ];
267
+ }
268
+ function applyToolStart(messages, event, chatId, now = /* @__PURE__ */ new Date()) {
269
+ if (messages.some((m) => m.toolCalls?.some((tc) => tc.id === event.tool_call_id))) return messages;
270
+ const frozen = messages.map(
271
+ (m, i) => i === messages.length - 1 && m.streaming && m.role === "assistant" && !m.toolCalls ? { ...m, streaming: false } : m
272
+ );
273
+ return [
274
+ ...frozen,
275
+ {
276
+ id: `tmp-tc-${event.tool_call_id}`,
277
+ chatId,
278
+ role: "assistant",
279
+ content: null,
280
+ toolCalls: [
281
+ {
282
+ id: event.tool_call_id,
283
+ type: "function",
284
+ function: { name: event.name, arguments: event.arguments || "{}" },
285
+ description: event.description
286
+ }
287
+ ],
288
+ toolCallId: null,
289
+ name: null,
290
+ createdAt: now.toISOString(),
291
+ streaming: true
292
+ }
293
+ ];
294
+ }
295
+ function applyToolEnd(messages, event) {
296
+ if (event.result === void 0) return messages;
297
+ const content = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
298
+ return messages.map(
299
+ (m) => m.toolCalls?.some((tc) => tc.id === event.tool_call_id) ? { ...m, toolResults: { ...m.toolResults, [event.tool_call_id]: { content, name: event.name } } } : m
300
+ );
301
+ }
302
+ function addApproval(current, approval) {
303
+ return current.some((a) => a.toolCallId === approval.toolCallId) ? current : [...current, approval];
304
+ }
305
+ function mergePendingInputs(current, incoming) {
306
+ const seen = new Set(current.map((p) => p.toolCallId));
307
+ const added = incoming.filter((p) => !seen.has(p.toolCallId) && seen.add(p.toolCallId));
308
+ return added.length ? [...current, ...added] : current;
309
+ }
310
+ var NOT_THINKING = { active: false, rounds: [] };
311
+ function applyThinking(state, thinking) {
312
+ if (!state.active) return { active: true, rounds: [...state.rounds, thinking] };
313
+ if (!thinking) return state;
314
+ const rounds = [...state.rounds];
315
+ rounds[rounds.length - 1] += thinking;
316
+ return { active: true, rounds };
317
+ }
318
+ function stopThinking(state) {
319
+ return state.active ? { ...state, active: false } : state;
320
+ }
321
+ function thinkingView(state) {
322
+ return { active: state.active, text: state.rounds.filter((r) => r).join("\n\n") };
323
+ }
324
+ function createLatestGate() {
325
+ let latest = 0;
326
+ return {
327
+ next: () => ++latest,
328
+ isCurrent: (ticket) => ticket === latest
329
+ };
330
+ }
331
+ var PENDING_POLL_MAX_MS = 3e4;
332
+ var PENDING_POLL_MIN_MS = 1e3;
333
+ var EXPIRY_GRACE_MS = 5e3;
334
+ var OVERDUE_POLL_MS = 1e4;
335
+ function nextPendingPollMs(inputs, nowMs) {
336
+ let wait = PENDING_POLL_MAX_MS;
337
+ for (const input of inputs) {
338
+ const deadline = input.expiresAt ? Date.parse(input.expiresAt) : NaN;
339
+ if (Number.isNaN(deadline)) continue;
340
+ const untilDue = deadline + EXPIRY_GRACE_MS - nowMs;
341
+ wait = Math.min(wait, untilDue > 0 ? untilDue : OVERDUE_POLL_MS);
342
+ }
343
+ return Math.max(PENDING_POLL_MIN_MS, wait);
344
+ }
345
+ var TRAILING_POLLS_MS = [5e3, 15e3, 3e4];
346
+
347
+ // src/hooks/useChat.ts
221
348
  function useChat(agentRef, options) {
222
349
  const client = useClient();
350
+ const includeThinking = !!options?.includeThinking;
351
+ const pollPendingInputs = options?.pollPendingInputs !== false;
223
352
  const [chatId, setChatId] = React5.useState(options?.chatId ?? null);
224
353
  const [messages, setMessages] = React5.useState([]);
225
354
  const [loading, setLoading] = React5.useState(false);
226
355
  const [streaming, setStreaming] = React5.useState(false);
227
356
  const [error, setError] = React5.useState(null);
228
357
  const [pendingApprovals, setPendingApprovals] = React5.useState([]);
358
+ const [pendingInputs, setPendingInputs] = React5.useState([]);
359
+ const [thinking, setThinking] = React5.useState(NOT_THINKING);
360
+ const thinkingShown = React5.useMemo(() => thinkingView(thinking), [thinking]);
229
361
  const [toolStatus, setToolStatus] = React5.useState([]);
362
+ const [trailingPoll, setTrailingPoll] = React5.useState(TRAILING_POLLS_MS.length);
363
+ const [pollTick, setPollTick] = React5.useState(0);
230
364
  const abortRef = React5.useRef(null);
231
365
  const chatIdRef = React5.useRef(chatId);
232
366
  chatIdRef.current = chatId;
367
+ const [gate] = React5.useState(createLatestGate);
368
+ const refresh2 = React5.useCallback(
369
+ async (id) => {
370
+ const ticket = gate.next();
371
+ try {
372
+ const detail = await getChat(client, id);
373
+ if (!gate.isCurrent(ticket)) return null;
374
+ setMessages(detail.messages);
375
+ setPendingApprovals(detail.pendingApprovals ?? []);
376
+ const inputs = detail.pendingInputs ?? [];
377
+ setPendingInputs(inputs);
378
+ return inputs;
379
+ } catch {
380
+ return null;
381
+ }
382
+ },
383
+ [client, gate]
384
+ );
233
385
  React5.useEffect(() => {
234
386
  if (!options?.chatId) return;
235
387
  let cancelled = false;
388
+ const ticket = gate.next();
236
389
  setLoading(true);
237
390
  getChat(client, options.chatId).then((detail) => {
238
- if (cancelled) return;
391
+ if (cancelled || !gate.isCurrent(ticket)) return;
239
392
  setChatId(detail.id);
240
393
  setMessages(detail.messages);
241
- if (detail.pendingApprovals?.length) {
242
- setPendingApprovals(detail.pendingApprovals);
243
- }
394
+ setPendingApprovals(detail.pendingApprovals ?? []);
395
+ setPendingInputs(detail.pendingInputs ?? []);
244
396
  }).catch((e) => {
245
397
  if (cancelled) return;
246
398
  setError(e.message);
@@ -250,74 +402,52 @@ function useChat(agentRef, options) {
250
402
  return () => {
251
403
  cancelled = true;
252
404
  };
253
- }, [client, options?.chatId]);
405
+ }, [client, gate, options?.chatId]);
254
406
  React5.useEffect(() => {
255
407
  return () => {
256
408
  abortRef.current?.abort();
257
409
  };
258
410
  }, []);
259
- const handleStreamChunks = React5.useCallback(
411
+ React5.useEffect(() => {
412
+ if (!pollPendingInputs || streaming || !chatId) return;
413
+ let delay = null;
414
+ if (pendingInputs.length > 0) delay = nextPendingPollMs(pendingInputs, Date.now());
415
+ else if (trailingPoll < TRAILING_POLLS_MS.length) delay = TRAILING_POLLS_MS[trailingPoll];
416
+ if (delay === null) return;
417
+ const waiting = pendingInputs.map((p) => p.toolCallId);
418
+ const timer = setTimeout(async () => {
419
+ const now = await refresh2(chatId);
420
+ if (waiting.length === 0) setTrailingPoll((i) => i + 1);
421
+ else if (now && waiting.some((id) => !now.some((p) => p.toolCallId === id))) setTrailingPoll(0);
422
+ setPollTick((t) => t + 1);
423
+ }, delay);
424
+ return () => clearTimeout(timer);
425
+ }, [pollPendingInputs, chatId, streaming, pendingInputs, trailingPoll, pollTick, refresh2]);
426
+ const handleStreamChunk = React5.useCallback(
260
427
  (chunk) => {
428
+ const asked = pendingInputFromChunk(chunk);
429
+ if (asked) {
430
+ setThinking(stopThinking);
431
+ setPendingInputs((prev) => mergePendingInputs(prev, [asked]));
432
+ return;
433
+ }
434
+ const chatIdNow = chatIdRef.current || "";
261
435
  if (chunk.event === "message") {
262
- const newContent = (() => {
263
- try {
264
- const parsed = JSON.parse(chunk.data);
265
- return parsed.content || "";
266
- } catch {
267
- return chunk.data;
268
- }
269
- })();
270
- if (!newContent) return;
271
- setMessages((prev) => {
272
- const last = prev[prev.length - 1];
273
- if (last && last.role === "assistant" && !last.toolCalls && last.streaming) {
274
- const next = [...prev];
275
- next[next.length - 1] = { ...last, content: (last.content || "") + newContent };
276
- return next;
277
- }
278
- return [...prev, {
279
- id: `tmp-text-${Date.now()}`,
280
- chatId: chatIdRef.current || "",
281
- role: "assistant",
282
- content: newContent,
283
- toolCalls: null,
284
- toolCallId: null,
285
- name: null,
286
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
287
- streaming: true
288
- }];
289
- });
436
+ const { text, thinking: thought } = parseMessageChunk(chunk.data);
437
+ if (thought !== void 0) setThinking((prev) => applyThinking(prev, thought));
438
+ if (text) {
439
+ setThinking(stopThinking);
440
+ setMessages((prev) => applyTextDelta(prev, text, chatIdNow));
441
+ }
290
442
  } else if (chunk.event === "tool_start") {
443
+ setThinking(stopThinking);
291
444
  try {
292
445
  const parsed = JSON.parse(chunk.data);
293
446
  setToolStatus((prev) => [
294
447
  ...prev,
295
448
  { toolCallId: parsed.tool_call_id, name: parsed.name, description: parsed.description, status: "running" }
296
449
  ]);
297
- setMessages((prev) => {
298
- if (prev.some((m) => m.toolCalls?.some((tc) => tc.id === parsed.tool_call_id))) {
299
- return prev;
300
- }
301
- const next = prev.map(
302
- (m, i) => i === prev.length - 1 && m.streaming && m.role === "assistant" && !m.toolCalls ? { ...m, streaming: false } : m
303
- );
304
- return [...next, {
305
- id: `tmp-tc-${parsed.tool_call_id}`,
306
- chatId: chatIdRef.current || "",
307
- role: "assistant",
308
- content: null,
309
- toolCalls: [{
310
- id: parsed.tool_call_id,
311
- type: "function",
312
- function: { name: parsed.name, arguments: parsed.arguments || "{}" },
313
- description: parsed.description
314
- }],
315
- toolCallId: null,
316
- name: null,
317
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
318
- streaming: true
319
- }];
320
- });
450
+ setMessages((prev) => applyToolStart(prev, parsed, chatIdNow));
321
451
  } catch {
322
452
  }
323
453
  } else if (chunk.event === "tool_end") {
@@ -326,39 +456,18 @@ function useChat(agentRef, options) {
326
456
  setToolStatus(
327
457
  (prev) => prev.map((t) => t.toolCallId === parsed.tool_call_id ? { ...t, status: "complete" } : t)
328
458
  );
329
- if (parsed.result !== void 0) {
330
- const resultContent = typeof parsed.result === "string" ? parsed.result : JSON.stringify(parsed.result);
331
- setMessages(
332
- (prev) => prev.map((m) => {
333
- if (m.toolCalls?.some((tc) => tc.id === parsed.tool_call_id)) {
334
- return {
335
- ...m,
336
- toolResults: {
337
- ...m.toolResults,
338
- [parsed.tool_call_id]: { content: resultContent, name: parsed.name }
339
- }
340
- };
341
- }
342
- return m;
343
- })
344
- );
345
- }
459
+ setMessages((prev) => applyToolEnd(prev, parsed));
346
460
  } catch {
347
461
  }
348
462
  } else if (chunk.event === "approval_required") {
463
+ setThinking(stopThinking);
349
464
  try {
350
- const raw = JSON.parse(chunk.data);
351
- const parsed = {
352
- type: raw.type,
353
- toolCallId: raw.tool_call_id,
354
- functionNamespace: raw.function_namespace,
355
- functionName: raw.function_name,
356
- arguments: raw.arguments
357
- };
358
- setPendingApprovals((prev) => [...prev, parsed]);
465
+ const approval = parseApproval(JSON.parse(chunk.data));
466
+ if (approval) setPendingApprovals((prev) => addApproval(prev, approval));
359
467
  } catch {
360
468
  }
361
469
  } else if (chunk.event === "error") {
470
+ setThinking(stopThinking);
362
471
  try {
363
472
  const parsed = JSON.parse(chunk.data);
364
473
  setError(parsed.error || "Stream error");
@@ -369,18 +478,37 @@ function useChat(agentRef, options) {
369
478
  },
370
479
  []
371
480
  );
372
- const refreshMessages = React5.useCallback(
373
- async (id) => {
374
- try {
375
- const detail = await getChat(client, id);
376
- setMessages(detail.messages);
377
- if (detail.pendingApprovals?.length) {
378
- setPendingApprovals(detail.pendingApprovals);
379
- }
380
- } catch {
481
+ const streamCallbacks = React5.useCallback(
482
+ (id) => [
483
+ (_done) => {
484
+ setStreaming(false);
485
+ setToolStatus([]);
486
+ setThinking(stopThinking);
487
+ refresh2(id);
488
+ },
489
+ (err) => {
490
+ setError(err.message);
491
+ setStreaming(false);
492
+ setToolStatus([]);
493
+ setThinking(stopThinking);
381
494
  }
495
+ ],
496
+ [refresh2]
497
+ );
498
+ const followChannel = React5.useCallback(
499
+ (id, channelId, lastId) => {
500
+ abortRef.current?.abort();
501
+ gate.next();
502
+ setStreaming(true);
503
+ setError(null);
504
+ setThinking(NOT_THINKING);
505
+ const [onDone, onError] = streamCallbacks(id);
506
+ abortRef.current = streamFromChannel(client, id, channelId, handleStreamChunk, onDone, onError, {
507
+ includeThinking,
508
+ lastId
509
+ });
382
510
  },
383
- [client]
511
+ [client, gate, includeThinking, handleStreamChunk, streamCallbacks]
384
512
  );
385
513
  const send = React5.useCallback(
386
514
  (content) => {
@@ -402,10 +530,12 @@ function useChat(agentRef, options) {
402
530
  setLoading(false);
403
531
  }
404
532
  abortRef.current?.abort();
533
+ gate.next();
405
534
  setStreaming(true);
406
535
  setError(null);
407
536
  setPendingApprovals([]);
408
537
  setToolStatus([]);
538
+ setThinking(NOT_THINKING);
409
539
  const displayContent = typeof content === "string" ? content : JSON.stringify(content);
410
540
  const userMsg = {
411
541
  id: `tmp-user-${Date.now()}`,
@@ -418,80 +548,77 @@ function useChat(agentRef, options) {
418
548
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
419
549
  };
420
550
  setMessages((prev) => [...prev, userMsg]);
421
- const controller = streamMessage(
422
- client,
423
- activeChatId,
424
- content,
425
- handleStreamChunks,
426
- () => {
427
- setStreaming(false);
428
- setToolStatus([]);
429
- refreshMessages(activeChatId);
430
- },
431
- (err) => {
432
- setError(err.message);
433
- setStreaming(false);
434
- setToolStatus([]);
435
- }
436
- );
437
- abortRef.current = controller;
551
+ const [onDone, onError] = streamCallbacks(activeChatId);
552
+ abortRef.current = streamMessage(client, activeChatId, content, handleStreamChunk, onDone, onError, {
553
+ includeThinking
554
+ });
438
555
  };
439
556
  doSend();
440
557
  },
441
- [client, agentRef, options?.input, handleStreamChunks, refreshMessages]
558
+ [client, agentRef, options?.input, gate, includeThinking, handleStreamChunk, streamCallbacks]
442
559
  );
443
- const handleApproval = React5.useCallback(
444
- (toolCallId, approved) => {
445
- const currentChatId = chatIdRef.current;
446
- if (!currentChatId) return;
560
+ const decide = React5.useCallback(
561
+ (toolCallId, approved, alwaysAllow) => {
562
+ const id = chatIdRef.current;
563
+ if (!id) return;
564
+ gate.next();
447
565
  setPendingApprovals((prev) => prev.filter((a) => a.toolCallId !== toolCallId));
448
- setStreaming(true);
449
566
  setError(null);
450
567
  (async () => {
451
568
  try {
452
- const result = await approveToolCall(client, currentChatId, toolCallId, approved);
453
- if (!approved) {
454
- setStreaming(false);
455
- refreshMessages(currentChatId);
456
- return;
457
- }
458
- const controller = streamFromChannel(
459
- client,
460
- currentChatId,
461
- result.channelId,
462
- handleStreamChunks,
463
- () => {
464
- setStreaming(false);
465
- setToolStatus([]);
466
- refreshMessages(currentChatId);
467
- },
468
- (err) => {
469
- setError(err.message);
470
- setStreaming(false);
471
- setToolStatus([]);
472
- }
473
- );
474
- abortRef.current = controller;
569
+ const result = await approveToolCall(client, id, toolCallId, approved, { alwaysAllow });
570
+ const channelId = approvalResumeChannel(result, approved);
571
+ if (channelId) followChannel(id, channelId);
572
+ else if (result.resume === void 0) refresh2(id);
475
573
  } catch (e) {
476
574
  setError(e.message);
477
- setStreaming(false);
575
+ refresh2(id);
478
576
  }
479
577
  })();
480
578
  },
481
- [client, handleStreamChunks, refreshMessages]
579
+ [client, gate, followChannel, refresh2]
482
580
  );
483
581
  const approve = React5.useCallback(
484
- (toolCallId) => handleApproval(toolCallId, true),
485
- [handleApproval]
582
+ (toolCallId, opts) => decide(toolCallId, true, !!opts?.alwaysAllow),
583
+ [decide]
486
584
  );
487
585
  const reject = React5.useCallback(
488
- (toolCallId) => handleApproval(toolCallId, false),
489
- [handleApproval]
586
+ (toolCallId) => decide(toolCallId, false, false),
587
+ [decide]
588
+ );
589
+ const answer = React5.useCallback(
590
+ async (toolCallId, text) => {
591
+ const id = chatIdRef.current;
592
+ if (!id) return false;
593
+ setError(null);
594
+ try {
595
+ const result = await answerPendingInput(client, id, toolCallId, text);
596
+ gate.next();
597
+ setPendingInputs((prev) => prev.filter((p) => p.toolCallId !== toolCallId));
598
+ const channelId = answerResumeChannel(result);
599
+ if (channelId) followChannel(id, channelId);
600
+ return true;
601
+ } catch (e) {
602
+ setError(e.message);
603
+ refresh2(id);
604
+ return false;
605
+ }
606
+ },
607
+ [client, gate, followChannel, refresh2]
608
+ );
609
+ const follow = React5.useCallback(
610
+ (channelId, opts) => {
611
+ const id = chatIdRef.current;
612
+ if (!id) return;
613
+ followChannel(id, channelId, opts?.lastId);
614
+ },
615
+ [followChannel]
490
616
  );
491
617
  const stop = React5.useCallback(() => {
492
618
  abortRef.current?.abort();
493
619
  abortRef.current = null;
494
620
  setStreaming(false);
621
+ setThinking(stopThinking);
495
622
  }, []);
496
623
  const clearError = React5.useCallback(() => {
497
624
  setError(null);
@@ -503,10 +630,14 @@ function useChat(agentRef, options) {
503
630
  streaming,
504
631
  error,
505
632
  pendingApprovals,
633
+ pendingInputs,
634
+ thinking: thinkingShown,
506
635
  toolStatus,
507
636
  send,
508
637
  approve,
509
638
  reject,
639
+ answer,
640
+ follow,
510
641
  stop,
511
642
  clearError
512
643
  };
@@ -609,6 +740,9 @@ export {
609
740
  SinasPermissionError,
610
741
  SinasProvider,
611
742
  SinasTimeoutError,
743
+ answerPendingInput,
744
+ answerResumeChannel,
745
+ approvalResumeChannel,
612
746
  approveToolCall,
613
747
  cancelBatch,
614
748
  checkPermissions,
@@ -621,11 +755,16 @@ export {
621
755
  getChat,
622
756
  getInfo,
623
757
  getMe,
758
+ invokeAgent,
624
759
  listBatchExecutions,
625
760
  listCollections,
626
761
  listFiles,
762
+ listPendingInputs,
627
763
  login,
628
764
  logout,
765
+ parsePendingInput,
766
+ parsePendingInputs,
767
+ pendingInputFromChunk,
629
768
  refresh,
630
769
  sendMessage,
631
770
  stateDelete,