@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.cjs CHANGED
@@ -38,6 +38,9 @@ __export(src_exports, {
38
38
  SinasPermissionError: () => SinasPermissionError,
39
39
  SinasProvider: () => SinasProvider,
40
40
  SinasTimeoutError: () => SinasTimeoutError,
41
+ answerPendingInput: () => answerPendingInput,
42
+ answerResumeChannel: () => answerResumeChannel,
43
+ approvalResumeChannel: () => approvalResumeChannel,
41
44
  approveToolCall: () => approveToolCall,
42
45
  cancelBatch: () => cancelBatch,
43
46
  checkPermissions: () => checkPermissions,
@@ -50,11 +53,16 @@ __export(src_exports, {
50
53
  getChat: () => getChat,
51
54
  getInfo: () => getInfo,
52
55
  getMe: () => getMe,
56
+ invokeAgent: () => invokeAgent,
53
57
  listBatchExecutions: () => listBatchExecutions,
54
58
  listCollections: () => listCollections,
55
59
  listFiles: () => listFiles,
60
+ listPendingInputs: () => listPendingInputs,
56
61
  login: () => login,
57
62
  logout: () => logout,
63
+ parsePendingInput: () => parsePendingInput,
64
+ parsePendingInputs: () => parsePendingInputs,
65
+ pendingInputFromChunk: () => pendingInputFromChunk,
58
66
  refresh: () => refresh,
59
67
  sendMessage: () => sendMessage,
60
68
  stateDelete: () => stateDelete,
@@ -537,12 +545,111 @@ async function stateList(client, ns) {
537
545
  return res.json();
538
546
  }
539
547
 
548
+ // src/client/chatEvents.ts
549
+ var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
550
+ var str = (v) => typeof v === "string" ? v : null;
551
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
552
+ var optionsOf = (v) => Array.isArray(v) && v.length > 0 && v.every((o) => typeof o === "string") ? v : null;
553
+ function parsePendingInput(raw) {
554
+ if (!isObject(raw)) return null;
555
+ if (typeof raw.tool_call_id !== "string" || !raw.tool_call_id) return null;
556
+ return {
557
+ toolCallId: raw.tool_call_id,
558
+ question: str(raw.question) ?? "",
559
+ options: optionsOf(raw.options),
560
+ expiresAt: str(raw.expires_at),
561
+ createdAt: str(raw.created_at)
562
+ };
563
+ }
564
+ function parsePendingInputs(raw) {
565
+ return Array.isArray(raw) ? raw.map(parsePendingInput).filter((p) => p !== null) : [];
566
+ }
567
+ function pendingInputFromChunk(chunk) {
568
+ if (chunk.event !== "input_required" && chunk.event !== "message") return null;
569
+ let parsed;
570
+ try {
571
+ parsed = JSON.parse(chunk.data);
572
+ } catch {
573
+ return null;
574
+ }
575
+ if (chunk.event === "message" && (!isObject(parsed) || parsed.type !== "input_required")) return null;
576
+ return parsePendingInput(parsed);
577
+ }
578
+ function parseApproval(raw) {
579
+ if (!isObject(raw) || typeof raw.tool_call_id !== "string") return null;
580
+ return {
581
+ type: str(raw.type) ?? "approval_required",
582
+ toolCallId: raw.tool_call_id,
583
+ functionNamespace: str(raw.function_namespace) ?? "",
584
+ functionName: str(raw.function_name) ?? "",
585
+ arguments: isObject(raw.arguments) ? raw.arguments : {}
586
+ };
587
+ }
588
+ function parseUsage(raw) {
589
+ if (!isObject(raw)) return null;
590
+ return {
591
+ calls: num(raw.calls),
592
+ promptTokens: num(raw.prompt_tokens),
593
+ completionTokens: num(raw.completion_tokens),
594
+ totalTokens: num(raw.total_tokens),
595
+ cacheReadTokens: num(raw.cache_read_tokens),
596
+ cacheWriteTokens: num(raw.cache_write_tokens),
597
+ costUsd: typeof raw.cost_usd === "number" ? raw.cost_usd : null,
598
+ unpricedCalls: num(raw.unpriced_calls),
599
+ errors: num(raw.errors)
600
+ };
601
+ }
602
+ var TURN_STATUSES = ["completed", "input_required", "in_progress", "injected"];
603
+ function parseTurnState(raw) {
604
+ const status = TURN_STATUSES.includes(raw.status) ? raw.status : "completed";
605
+ return {
606
+ status,
607
+ pendingInputs: parsePendingInputs(raw.pending_inputs),
608
+ channelId: str(raw.channel_id),
609
+ usage: parseUsage(raw.usage)
610
+ };
611
+ }
612
+ function parseMessage(data) {
613
+ const toolCalls = data.tool_calls;
614
+ return {
615
+ id: data.id,
616
+ chatId: data.chat_id,
617
+ role: data.role,
618
+ content: data.content ?? null,
619
+ toolCalls: toolCalls ?? null,
620
+ toolCallId: data.tool_call_id ?? null,
621
+ name: data.name ?? null,
622
+ createdAt: data.created_at
623
+ };
624
+ }
625
+ function parseDone(data) {
626
+ try {
627
+ const parsed = JSON.parse(data);
628
+ if (isObject(parsed) && typeof parsed.status === "string") return { status: parsed.status };
629
+ } catch {
630
+ }
631
+ return { status: "completed" };
632
+ }
633
+ function approvalResumeChannel(result, approved = true) {
634
+ if (result.resume === "waiting") return null;
635
+ if (result.resume === void 0 && !approved) return null;
636
+ return result.channelId || null;
637
+ }
638
+ function answerResumeChannel(result) {
639
+ if (!result.resumed && !result.resumePending) return null;
640
+ return result.channelId || null;
641
+ }
642
+
540
643
  // src/client/chat.ts
541
- async function createChat(client, agentRef, input) {
644
+ function splitAgentRef(agentRef) {
542
645
  const [ns, name] = agentRef.split("/");
543
646
  if (!ns || !name) {
544
647
  throw new Error(`Invalid agent ref "${agentRef}": expected "namespace/name"`);
545
648
  }
649
+ return [ns, name];
650
+ }
651
+ async function createChat(client, agentRef, input) {
652
+ const [ns, name] = splitAgentRef(agentRef);
546
653
  const config = client.getConfig();
547
654
  const res = await client.fetch(`${config.apiBase}/agents/${ns}/${name}/chats`, {
548
655
  method: "POST",
@@ -568,66 +675,117 @@ async function sendMessage(client, chatId, content) {
568
675
  chatId: data.chat_id,
569
676
  role: data.role,
570
677
  content: data.content,
571
- createdAt: data.created_at
678
+ createdAt: data.created_at,
679
+ ...parseTurnState(data)
572
680
  };
573
681
  }
574
- function mapMessage(data) {
575
- const toolCalls = data.tool_calls;
682
+ async function invokeAgent(client, agentRef, message, options = {}) {
683
+ const [ns, name] = splitAgentRef(agentRef);
684
+ const config = client.getConfig();
685
+ const body = { message };
686
+ if (options.sessionKey !== void 0) body.session_key = options.sessionKey;
687
+ if (options.reset !== void 0) body.reset = options.reset;
688
+ if (options.input !== void 0) body.input = options.input;
689
+ const res = await client.fetch(`${config.apiBase}/agents/${ns}/${name}/invoke`, {
690
+ method: "POST",
691
+ body: JSON.stringify(body)
692
+ });
693
+ const data = await res.json();
576
694
  return {
577
- id: data.id,
695
+ reply: typeof data.reply === "string" ? data.reply : "",
578
696
  chatId: data.chat_id,
579
- role: data.role,
580
- content: data.content ?? null,
581
- toolCalls: toolCalls ?? null,
582
- toolCallId: data.tool_call_id ?? null,
583
- name: data.name ?? null,
584
- createdAt: data.created_at
697
+ sessionKey: data.session_key ?? null,
698
+ ...parseTurnState(data)
585
699
  };
586
700
  }
587
701
  async function getChat(client, chatId) {
588
702
  const config = client.getConfig();
589
703
  const res = await client.fetch(`${config.apiBase}/chats/${chatId}`);
590
704
  const data = await res.json();
591
- return {
705
+ const detail = {
592
706
  id: data.id,
593
707
  agentNamespace: data.agent_namespace,
594
708
  agentName: data.agent_name,
595
709
  title: data.title,
596
- messages: (data.messages || []).map(mapMessage),
597
- pendingApprovals: (data.pending_approvals || []).map((pa) => ({
598
- type: "approval_required",
599
- toolCallId: pa.tool_call_id,
600
- functionNamespace: pa.function_namespace,
601
- functionName: pa.function_name,
602
- arguments: pa.arguments || {}
603
- }))
710
+ messages: (data.messages || []).map(parseMessage),
711
+ pendingApprovals: (data.pending_approvals || []).map(parseApproval).filter((a) => a !== null)
604
712
  };
713
+ if (Array.isArray(data.pending_inputs)) {
714
+ detail.pendingInputs = parsePendingInputs(data.pending_inputs);
715
+ }
716
+ return detail;
605
717
  }
606
- async function approveToolCall(client, chatId, toolCallId, approved) {
718
+ async function approveToolCall(client, chatId, toolCallId, approved, options = {}) {
607
719
  const config = client.getConfig();
720
+ const body = { approved };
721
+ if (approved && options.alwaysAllow) body.always_allow = true;
608
722
  const res = await client.fetch(
609
- `${config.apiBase}/chats/${chatId}/approve-tool/${toolCallId}`,
610
- { method: "POST", body: JSON.stringify({ approved }) }
723
+ `${config.apiBase}/chats/${chatId}/approve-tool/${encodeURIComponent(toolCallId)}`,
724
+ { method: "POST", body: JSON.stringify(body) }
611
725
  );
612
726
  const data = await res.json();
613
- return {
727
+ const result = {
614
728
  status: data.status,
615
729
  toolCallId: data.tool_call_id,
616
730
  channelId: data.channel_id
617
731
  };
732
+ if (data.resume === "waiting" || data.resume === "queued") result.resume = data.resume;
733
+ if (typeof data.message === "string") result.message = data.message;
734
+ return result;
618
735
  }
619
- function streamFromChannel(client, chatId, channelId, onChunk, onDone, onError) {
620
- const controller = new AbortController();
736
+ async function listPendingInputs(client, chatId) {
737
+ const config = client.getConfig();
738
+ const res = await client.fetch(`${config.apiBase}/chats/${chatId}/pending-input`);
739
+ return parsePendingInputs(await res.json());
740
+ }
741
+ async function answerPendingInput(client, chatId, toolCallId, answer, options = {}) {
621
742
  const config = client.getConfig();
743
+ const body = { answer };
744
+ if (options.wait) body.wait = true;
745
+ const res = await client.fetch(
746
+ `${config.apiBase}/chats/${chatId}/pending-input/${encodeURIComponent(toolCallId)}`,
747
+ { method: "POST", body: JSON.stringify(body) }
748
+ );
749
+ const data = await res.json();
750
+ if (options.wait) {
751
+ return {
752
+ toolCallId: data.tool_call_id,
753
+ chatId: data.chat_id,
754
+ reply: typeof data.reply === "string" ? data.reply : "",
755
+ message: data.message && typeof data.message === "object" ? parseMessage(data.message) : null,
756
+ ...parseTurnState(data)
757
+ };
758
+ }
759
+ return {
760
+ status: "answered",
761
+ toolCallId: data.tool_call_id,
762
+ resumed: !!data.resumed,
763
+ resumePending: !!data.resume_pending,
764
+ channelId: data.channel_id ?? null,
765
+ message: typeof data.message === "string" ? data.message : ""
766
+ };
767
+ }
768
+ function streamFromChannel(client, chatId, channelId, onChunk, onDone, onError, options = {}) {
769
+ const config = client.getConfig();
770
+ const params = new URLSearchParams({ last_id: options.lastId ?? "0" });
771
+ if (options.includeThinking) params.set("include_thinking", "true");
772
+ return runSSE(
773
+ `${config.apiBase}/chats/${chatId}/stream/${encodeURIComponent(channelId)}?${params}`,
774
+ (signal) => ({ headers: client.getAuthHeaders(), signal }),
775
+ "Stream reconnect failed",
776
+ onChunk,
777
+ onDone,
778
+ onError
779
+ );
780
+ }
781
+ function runSSE(url, init, failure, onChunk, onDone, onError) {
782
+ const controller = new AbortController();
622
783
  (async () => {
623
784
  try {
624
- const res = await fetch(
625
- `${config.apiBase}/chats/${chatId}/stream/${channelId}?last_id=0`,
626
- { headers: client.getAuthHeaders(), signal: controller.signal }
627
- );
785
+ const res = await fetch(url, init(controller.signal));
628
786
  if (!res.ok) {
629
787
  const body = await res.json().catch(() => ({ detail: res.statusText }));
630
- throw new Error(body.detail || "Stream reconnect failed");
788
+ throw new Error(body.detail || failure);
631
789
  }
632
790
  const reader = res.body.getReader();
633
791
  const decoder = new TextDecoder();
@@ -641,7 +799,7 @@ function streamFromChannel(client, chatId, channelId, onChunk, onDone, onError)
641
799
  for (const chunk of chunks) {
642
800
  onChunk(chunk);
643
801
  if (chunk.event === "done") {
644
- onDone?.();
802
+ onDone?.(parseDone(chunk.data));
645
803
  return;
646
804
  }
647
805
  }
@@ -650,9 +808,13 @@ function streamFromChannel(client, chatId, channelId, onChunk, onDone, onError)
650
808
  const { chunks } = parseSSE(buffer + "\n\n");
651
809
  for (const chunk of chunks) {
652
810
  onChunk(chunk);
811
+ if (chunk.event === "done") {
812
+ onDone?.(parseDone(chunk.data));
813
+ return;
814
+ }
653
815
  }
654
816
  }
655
- onDone?.();
817
+ onDone?.({ status: "closed" });
656
818
  } catch (err) {
657
819
  if (err.name === "AbortError") return;
658
820
  onError?.(err);
@@ -663,75 +825,33 @@ function streamFromChannel(client, chatId, channelId, onChunk, onDone, onError)
663
825
  function parseSSE(buffer) {
664
826
  const chunks = [];
665
827
  const normalized = buffer.replace(/\r\n/g, "\n");
666
- const lines = normalized.split("\n");
667
- let event = "";
668
- let data = "";
669
- let remaining = "";
670
- for (let i = 0; i < lines.length; i++) {
671
- const line = lines[i];
672
- if (i === lines.length - 1 && !normalized.endsWith("\n")) {
673
- remaining = line;
674
- break;
675
- }
676
- if (line.startsWith("event:")) {
677
- event = line.slice(6).trim();
678
- } else if (line.startsWith("data:")) {
679
- data = line.slice(5).trim();
680
- } else if (line === "") {
681
- if (event || data) {
682
- chunks.push({ event: event || "message", data });
683
- event = "";
684
- data = "";
685
- }
828
+ let start = 0;
829
+ let end;
830
+ while ((end = normalized.indexOf("\n\n", start)) !== -1) {
831
+ const block = normalized.slice(start, end);
832
+ start = end + 2;
833
+ let event = "";
834
+ const data = [];
835
+ for (const line of block.split("\n")) {
836
+ if (line.startsWith("event:")) event = line.slice(6).trim();
837
+ else if (line.startsWith("data:")) data.push(line.slice(5).trim());
686
838
  }
839
+ if (event || data.length) chunks.push({ event: event || "message", data: data.join("\n") });
687
840
  }
688
- return { chunks, remaining };
841
+ return { chunks, remaining: normalized.slice(start) };
689
842
  }
690
- function streamMessage(client, chatId, content, onChunk, onDone, onError) {
691
- const controller = new AbortController();
843
+ function streamMessage(client, chatId, content, onChunk, onDone, onError, options = {}) {
692
844
  const config = client.getConfig();
693
- (async () => {
694
- try {
695
- const res = await fetch(`${config.apiBase}/chats/${chatId}/messages/stream`, {
696
- method: "POST",
697
- headers: client.getAuthHeaders(),
698
- body: JSON.stringify({ content }),
699
- signal: controller.signal
700
- });
701
- if (!res.ok) {
702
- const body = await res.json().catch(() => ({ detail: res.statusText }));
703
- throw new Error(body.detail || "Stream request failed");
704
- }
705
- const reader = res.body.getReader();
706
- const decoder = new TextDecoder();
707
- let buffer = "";
708
- while (true) {
709
- const { done, value } = await reader.read();
710
- if (done) break;
711
- buffer += decoder.decode(value, { stream: true });
712
- const { chunks, remaining } = parseSSE(buffer);
713
- buffer = remaining;
714
- for (const chunk of chunks) {
715
- onChunk(chunk);
716
- if (chunk.event === "done") {
717
- onDone?.();
718
- return;
719
- }
720
- }
721
- }
722
- if (buffer.trim()) {
723
- const { chunks } = parseSSE(buffer + "\n\n");
724
- for (const chunk of chunks) {
725
- onChunk(chunk);
726
- }
727
- }
728
- onDone?.();
729
- } catch (err) {
730
- if (err.name === "AbortError") return;
731
- onError?.(err);
732
- }
733
- })();
734
- return controller;
845
+ const body = { content };
846
+ if (options.includeThinking) body.include_thinking = true;
847
+ return runSSE(
848
+ `${config.apiBase}/chats/${chatId}/messages/stream`,
849
+ (signal) => ({ method: "POST", headers: client.getAuthHeaders(), body: JSON.stringify(body), signal }),
850
+ "Stream request failed",
851
+ onChunk,
852
+ onDone,
853
+ onError
854
+ );
735
855
  }
736
856
 
737
857
  // src/client/collections.ts
@@ -946,29 +1066,172 @@ function useStateStore(ns, key) {
946
1066
 
947
1067
  // src/hooks/useChat.ts
948
1068
  var import_react5 = __toESM(require("react"), 1);
1069
+
1070
+ // src/hooks/chatState.ts
1071
+ function parseMessageChunk(data) {
1072
+ let parsed;
1073
+ try {
1074
+ parsed = JSON.parse(data);
1075
+ } catch {
1076
+ return { text: data };
1077
+ }
1078
+ if (!parsed || typeof parsed !== "object") return { text: "" };
1079
+ const p = parsed;
1080
+ const out = { text: typeof p.content === "string" ? p.content : "" };
1081
+ if (typeof p.thinking === "string") out.thinking = p.thinking;
1082
+ return out;
1083
+ }
1084
+ function applyTextDelta(messages, text, chatId, now = /* @__PURE__ */ new Date()) {
1085
+ if (!text) return messages;
1086
+ const last = messages[messages.length - 1];
1087
+ if (last && last.role === "assistant" && !last.toolCalls && last.streaming) {
1088
+ const next = [...messages];
1089
+ next[next.length - 1] = { ...last, content: (last.content || "") + text };
1090
+ return next;
1091
+ }
1092
+ return [
1093
+ ...messages,
1094
+ {
1095
+ id: `tmp-text-${now.getTime()}`,
1096
+ chatId,
1097
+ role: "assistant",
1098
+ content: text,
1099
+ toolCalls: null,
1100
+ toolCallId: null,
1101
+ name: null,
1102
+ createdAt: now.toISOString(),
1103
+ streaming: true
1104
+ }
1105
+ ];
1106
+ }
1107
+ function applyToolStart(messages, event, chatId, now = /* @__PURE__ */ new Date()) {
1108
+ if (messages.some((m) => m.toolCalls?.some((tc) => tc.id === event.tool_call_id))) return messages;
1109
+ const frozen = messages.map(
1110
+ (m, i) => i === messages.length - 1 && m.streaming && m.role === "assistant" && !m.toolCalls ? { ...m, streaming: false } : m
1111
+ );
1112
+ return [
1113
+ ...frozen,
1114
+ {
1115
+ id: `tmp-tc-${event.tool_call_id}`,
1116
+ chatId,
1117
+ role: "assistant",
1118
+ content: null,
1119
+ toolCalls: [
1120
+ {
1121
+ id: event.tool_call_id,
1122
+ type: "function",
1123
+ function: { name: event.name, arguments: event.arguments || "{}" },
1124
+ description: event.description
1125
+ }
1126
+ ],
1127
+ toolCallId: null,
1128
+ name: null,
1129
+ createdAt: now.toISOString(),
1130
+ streaming: true
1131
+ }
1132
+ ];
1133
+ }
1134
+ function applyToolEnd(messages, event) {
1135
+ if (event.result === void 0) return messages;
1136
+ const content = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
1137
+ return messages.map(
1138
+ (m) => m.toolCalls?.some((tc) => tc.id === event.tool_call_id) ? { ...m, toolResults: { ...m.toolResults, [event.tool_call_id]: { content, name: event.name } } } : m
1139
+ );
1140
+ }
1141
+ function addApproval(current, approval) {
1142
+ return current.some((a) => a.toolCallId === approval.toolCallId) ? current : [...current, approval];
1143
+ }
1144
+ function mergePendingInputs(current, incoming) {
1145
+ const seen = new Set(current.map((p) => p.toolCallId));
1146
+ const added = incoming.filter((p) => !seen.has(p.toolCallId) && seen.add(p.toolCallId));
1147
+ return added.length ? [...current, ...added] : current;
1148
+ }
1149
+ var NOT_THINKING = { active: false, rounds: [] };
1150
+ function applyThinking(state, thinking) {
1151
+ if (!state.active) return { active: true, rounds: [...state.rounds, thinking] };
1152
+ if (!thinking) return state;
1153
+ const rounds = [...state.rounds];
1154
+ rounds[rounds.length - 1] += thinking;
1155
+ return { active: true, rounds };
1156
+ }
1157
+ function stopThinking(state) {
1158
+ return state.active ? { ...state, active: false } : state;
1159
+ }
1160
+ function thinkingView(state) {
1161
+ return { active: state.active, text: state.rounds.filter((r) => r).join("\n\n") };
1162
+ }
1163
+ function createLatestGate() {
1164
+ let latest = 0;
1165
+ return {
1166
+ next: () => ++latest,
1167
+ isCurrent: (ticket) => ticket === latest
1168
+ };
1169
+ }
1170
+ var PENDING_POLL_MAX_MS = 3e4;
1171
+ var PENDING_POLL_MIN_MS = 1e3;
1172
+ var EXPIRY_GRACE_MS = 5e3;
1173
+ var OVERDUE_POLL_MS = 1e4;
1174
+ function nextPendingPollMs(inputs, nowMs) {
1175
+ let wait = PENDING_POLL_MAX_MS;
1176
+ for (const input of inputs) {
1177
+ const deadline = input.expiresAt ? Date.parse(input.expiresAt) : NaN;
1178
+ if (Number.isNaN(deadline)) continue;
1179
+ const untilDue = deadline + EXPIRY_GRACE_MS - nowMs;
1180
+ wait = Math.min(wait, untilDue > 0 ? untilDue : OVERDUE_POLL_MS);
1181
+ }
1182
+ return Math.max(PENDING_POLL_MIN_MS, wait);
1183
+ }
1184
+ var TRAILING_POLLS_MS = [5e3, 15e3, 3e4];
1185
+
1186
+ // src/hooks/useChat.ts
949
1187
  function useChat(agentRef, options) {
950
1188
  const client = useClient();
1189
+ const includeThinking = !!options?.includeThinking;
1190
+ const pollPendingInputs = options?.pollPendingInputs !== false;
951
1191
  const [chatId, setChatId] = import_react5.default.useState(options?.chatId ?? null);
952
1192
  const [messages, setMessages] = import_react5.default.useState([]);
953
1193
  const [loading, setLoading] = import_react5.default.useState(false);
954
1194
  const [streaming, setStreaming] = import_react5.default.useState(false);
955
1195
  const [error, setError] = import_react5.default.useState(null);
956
1196
  const [pendingApprovals, setPendingApprovals] = import_react5.default.useState([]);
1197
+ const [pendingInputs, setPendingInputs] = import_react5.default.useState([]);
1198
+ const [thinking, setThinking] = import_react5.default.useState(NOT_THINKING);
1199
+ const thinkingShown = import_react5.default.useMemo(() => thinkingView(thinking), [thinking]);
957
1200
  const [toolStatus, setToolStatus] = import_react5.default.useState([]);
1201
+ const [trailingPoll, setTrailingPoll] = import_react5.default.useState(TRAILING_POLLS_MS.length);
1202
+ const [pollTick, setPollTick] = import_react5.default.useState(0);
958
1203
  const abortRef = import_react5.default.useRef(null);
959
1204
  const chatIdRef = import_react5.default.useRef(chatId);
960
1205
  chatIdRef.current = chatId;
1206
+ const [gate] = import_react5.default.useState(createLatestGate);
1207
+ const refresh2 = import_react5.default.useCallback(
1208
+ async (id) => {
1209
+ const ticket = gate.next();
1210
+ try {
1211
+ const detail = await getChat(client, id);
1212
+ if (!gate.isCurrent(ticket)) return null;
1213
+ setMessages(detail.messages);
1214
+ setPendingApprovals(detail.pendingApprovals ?? []);
1215
+ const inputs = detail.pendingInputs ?? [];
1216
+ setPendingInputs(inputs);
1217
+ return inputs;
1218
+ } catch {
1219
+ return null;
1220
+ }
1221
+ },
1222
+ [client, gate]
1223
+ );
961
1224
  import_react5.default.useEffect(() => {
962
1225
  if (!options?.chatId) return;
963
1226
  let cancelled = false;
1227
+ const ticket = gate.next();
964
1228
  setLoading(true);
965
1229
  getChat(client, options.chatId).then((detail) => {
966
- if (cancelled) return;
1230
+ if (cancelled || !gate.isCurrent(ticket)) return;
967
1231
  setChatId(detail.id);
968
1232
  setMessages(detail.messages);
969
- if (detail.pendingApprovals?.length) {
970
- setPendingApprovals(detail.pendingApprovals);
971
- }
1233
+ setPendingApprovals(detail.pendingApprovals ?? []);
1234
+ setPendingInputs(detail.pendingInputs ?? []);
972
1235
  }).catch((e) => {
973
1236
  if (cancelled) return;
974
1237
  setError(e.message);
@@ -978,74 +1241,52 @@ function useChat(agentRef, options) {
978
1241
  return () => {
979
1242
  cancelled = true;
980
1243
  };
981
- }, [client, options?.chatId]);
1244
+ }, [client, gate, options?.chatId]);
982
1245
  import_react5.default.useEffect(() => {
983
1246
  return () => {
984
1247
  abortRef.current?.abort();
985
1248
  };
986
1249
  }, []);
987
- const handleStreamChunks = import_react5.default.useCallback(
1250
+ import_react5.default.useEffect(() => {
1251
+ if (!pollPendingInputs || streaming || !chatId) return;
1252
+ let delay = null;
1253
+ if (pendingInputs.length > 0) delay = nextPendingPollMs(pendingInputs, Date.now());
1254
+ else if (trailingPoll < TRAILING_POLLS_MS.length) delay = TRAILING_POLLS_MS[trailingPoll];
1255
+ if (delay === null) return;
1256
+ const waiting = pendingInputs.map((p) => p.toolCallId);
1257
+ const timer = setTimeout(async () => {
1258
+ const now = await refresh2(chatId);
1259
+ if (waiting.length === 0) setTrailingPoll((i) => i + 1);
1260
+ else if (now && waiting.some((id) => !now.some((p) => p.toolCallId === id))) setTrailingPoll(0);
1261
+ setPollTick((t) => t + 1);
1262
+ }, delay);
1263
+ return () => clearTimeout(timer);
1264
+ }, [pollPendingInputs, chatId, streaming, pendingInputs, trailingPoll, pollTick, refresh2]);
1265
+ const handleStreamChunk = import_react5.default.useCallback(
988
1266
  (chunk) => {
1267
+ const asked = pendingInputFromChunk(chunk);
1268
+ if (asked) {
1269
+ setThinking(stopThinking);
1270
+ setPendingInputs((prev) => mergePendingInputs(prev, [asked]));
1271
+ return;
1272
+ }
1273
+ const chatIdNow = chatIdRef.current || "";
989
1274
  if (chunk.event === "message") {
990
- const newContent = (() => {
991
- try {
992
- const parsed = JSON.parse(chunk.data);
993
- return parsed.content || "";
994
- } catch {
995
- return chunk.data;
996
- }
997
- })();
998
- if (!newContent) return;
999
- setMessages((prev) => {
1000
- const last = prev[prev.length - 1];
1001
- if (last && last.role === "assistant" && !last.toolCalls && last.streaming) {
1002
- const next = [...prev];
1003
- next[next.length - 1] = { ...last, content: (last.content || "") + newContent };
1004
- return next;
1005
- }
1006
- return [...prev, {
1007
- id: `tmp-text-${Date.now()}`,
1008
- chatId: chatIdRef.current || "",
1009
- role: "assistant",
1010
- content: newContent,
1011
- toolCalls: null,
1012
- toolCallId: null,
1013
- name: null,
1014
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1015
- streaming: true
1016
- }];
1017
- });
1275
+ const { text, thinking: thought } = parseMessageChunk(chunk.data);
1276
+ if (thought !== void 0) setThinking((prev) => applyThinking(prev, thought));
1277
+ if (text) {
1278
+ setThinking(stopThinking);
1279
+ setMessages((prev) => applyTextDelta(prev, text, chatIdNow));
1280
+ }
1018
1281
  } else if (chunk.event === "tool_start") {
1282
+ setThinking(stopThinking);
1019
1283
  try {
1020
1284
  const parsed = JSON.parse(chunk.data);
1021
1285
  setToolStatus((prev) => [
1022
1286
  ...prev,
1023
1287
  { toolCallId: parsed.tool_call_id, name: parsed.name, description: parsed.description, status: "running" }
1024
1288
  ]);
1025
- setMessages((prev) => {
1026
- if (prev.some((m) => m.toolCalls?.some((tc) => tc.id === parsed.tool_call_id))) {
1027
- return prev;
1028
- }
1029
- const next = prev.map(
1030
- (m, i) => i === prev.length - 1 && m.streaming && m.role === "assistant" && !m.toolCalls ? { ...m, streaming: false } : m
1031
- );
1032
- return [...next, {
1033
- id: `tmp-tc-${parsed.tool_call_id}`,
1034
- chatId: chatIdRef.current || "",
1035
- role: "assistant",
1036
- content: null,
1037
- toolCalls: [{
1038
- id: parsed.tool_call_id,
1039
- type: "function",
1040
- function: { name: parsed.name, arguments: parsed.arguments || "{}" },
1041
- description: parsed.description
1042
- }],
1043
- toolCallId: null,
1044
- name: null,
1045
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1046
- streaming: true
1047
- }];
1048
- });
1289
+ setMessages((prev) => applyToolStart(prev, parsed, chatIdNow));
1049
1290
  } catch {
1050
1291
  }
1051
1292
  } else if (chunk.event === "tool_end") {
@@ -1054,39 +1295,18 @@ function useChat(agentRef, options) {
1054
1295
  setToolStatus(
1055
1296
  (prev) => prev.map((t) => t.toolCallId === parsed.tool_call_id ? { ...t, status: "complete" } : t)
1056
1297
  );
1057
- if (parsed.result !== void 0) {
1058
- const resultContent = typeof parsed.result === "string" ? parsed.result : JSON.stringify(parsed.result);
1059
- setMessages(
1060
- (prev) => prev.map((m) => {
1061
- if (m.toolCalls?.some((tc) => tc.id === parsed.tool_call_id)) {
1062
- return {
1063
- ...m,
1064
- toolResults: {
1065
- ...m.toolResults,
1066
- [parsed.tool_call_id]: { content: resultContent, name: parsed.name }
1067
- }
1068
- };
1069
- }
1070
- return m;
1071
- })
1072
- );
1073
- }
1298
+ setMessages((prev) => applyToolEnd(prev, parsed));
1074
1299
  } catch {
1075
1300
  }
1076
1301
  } else if (chunk.event === "approval_required") {
1302
+ setThinking(stopThinking);
1077
1303
  try {
1078
- const raw = JSON.parse(chunk.data);
1079
- const parsed = {
1080
- type: raw.type,
1081
- toolCallId: raw.tool_call_id,
1082
- functionNamespace: raw.function_namespace,
1083
- functionName: raw.function_name,
1084
- arguments: raw.arguments
1085
- };
1086
- setPendingApprovals((prev) => [...prev, parsed]);
1304
+ const approval = parseApproval(JSON.parse(chunk.data));
1305
+ if (approval) setPendingApprovals((prev) => addApproval(prev, approval));
1087
1306
  } catch {
1088
1307
  }
1089
1308
  } else if (chunk.event === "error") {
1309
+ setThinking(stopThinking);
1090
1310
  try {
1091
1311
  const parsed = JSON.parse(chunk.data);
1092
1312
  setError(parsed.error || "Stream error");
@@ -1097,18 +1317,37 @@ function useChat(agentRef, options) {
1097
1317
  },
1098
1318
  []
1099
1319
  );
1100
- const refreshMessages = import_react5.default.useCallback(
1101
- async (id) => {
1102
- try {
1103
- const detail = await getChat(client, id);
1104
- setMessages(detail.messages);
1105
- if (detail.pendingApprovals?.length) {
1106
- setPendingApprovals(detail.pendingApprovals);
1107
- }
1108
- } catch {
1320
+ const streamCallbacks = import_react5.default.useCallback(
1321
+ (id) => [
1322
+ (_done) => {
1323
+ setStreaming(false);
1324
+ setToolStatus([]);
1325
+ setThinking(stopThinking);
1326
+ refresh2(id);
1327
+ },
1328
+ (err) => {
1329
+ setError(err.message);
1330
+ setStreaming(false);
1331
+ setToolStatus([]);
1332
+ setThinking(stopThinking);
1109
1333
  }
1334
+ ],
1335
+ [refresh2]
1336
+ );
1337
+ const followChannel = import_react5.default.useCallback(
1338
+ (id, channelId, lastId) => {
1339
+ abortRef.current?.abort();
1340
+ gate.next();
1341
+ setStreaming(true);
1342
+ setError(null);
1343
+ setThinking(NOT_THINKING);
1344
+ const [onDone, onError] = streamCallbacks(id);
1345
+ abortRef.current = streamFromChannel(client, id, channelId, handleStreamChunk, onDone, onError, {
1346
+ includeThinking,
1347
+ lastId
1348
+ });
1110
1349
  },
1111
- [client]
1350
+ [client, gate, includeThinking, handleStreamChunk, streamCallbacks]
1112
1351
  );
1113
1352
  const send = import_react5.default.useCallback(
1114
1353
  (content) => {
@@ -1130,10 +1369,12 @@ function useChat(agentRef, options) {
1130
1369
  setLoading(false);
1131
1370
  }
1132
1371
  abortRef.current?.abort();
1372
+ gate.next();
1133
1373
  setStreaming(true);
1134
1374
  setError(null);
1135
1375
  setPendingApprovals([]);
1136
1376
  setToolStatus([]);
1377
+ setThinking(NOT_THINKING);
1137
1378
  const displayContent = typeof content === "string" ? content : JSON.stringify(content);
1138
1379
  const userMsg = {
1139
1380
  id: `tmp-user-${Date.now()}`,
@@ -1146,80 +1387,77 @@ function useChat(agentRef, options) {
1146
1387
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1147
1388
  };
1148
1389
  setMessages((prev) => [...prev, userMsg]);
1149
- const controller = streamMessage(
1150
- client,
1151
- activeChatId,
1152
- content,
1153
- handleStreamChunks,
1154
- () => {
1155
- setStreaming(false);
1156
- setToolStatus([]);
1157
- refreshMessages(activeChatId);
1158
- },
1159
- (err) => {
1160
- setError(err.message);
1161
- setStreaming(false);
1162
- setToolStatus([]);
1163
- }
1164
- );
1165
- abortRef.current = controller;
1390
+ const [onDone, onError] = streamCallbacks(activeChatId);
1391
+ abortRef.current = streamMessage(client, activeChatId, content, handleStreamChunk, onDone, onError, {
1392
+ includeThinking
1393
+ });
1166
1394
  };
1167
1395
  doSend();
1168
1396
  },
1169
- [client, agentRef, options?.input, handleStreamChunks, refreshMessages]
1397
+ [client, agentRef, options?.input, gate, includeThinking, handleStreamChunk, streamCallbacks]
1170
1398
  );
1171
- const handleApproval = import_react5.default.useCallback(
1172
- (toolCallId, approved) => {
1173
- const currentChatId = chatIdRef.current;
1174
- if (!currentChatId) return;
1399
+ const decide = import_react5.default.useCallback(
1400
+ (toolCallId, approved, alwaysAllow) => {
1401
+ const id = chatIdRef.current;
1402
+ if (!id) return;
1403
+ gate.next();
1175
1404
  setPendingApprovals((prev) => prev.filter((a) => a.toolCallId !== toolCallId));
1176
- setStreaming(true);
1177
1405
  setError(null);
1178
1406
  (async () => {
1179
1407
  try {
1180
- const result = await approveToolCall(client, currentChatId, toolCallId, approved);
1181
- if (!approved) {
1182
- setStreaming(false);
1183
- refreshMessages(currentChatId);
1184
- return;
1185
- }
1186
- const controller = streamFromChannel(
1187
- client,
1188
- currentChatId,
1189
- result.channelId,
1190
- handleStreamChunks,
1191
- () => {
1192
- setStreaming(false);
1193
- setToolStatus([]);
1194
- refreshMessages(currentChatId);
1195
- },
1196
- (err) => {
1197
- setError(err.message);
1198
- setStreaming(false);
1199
- setToolStatus([]);
1200
- }
1201
- );
1202
- abortRef.current = controller;
1408
+ const result = await approveToolCall(client, id, toolCallId, approved, { alwaysAllow });
1409
+ const channelId = approvalResumeChannel(result, approved);
1410
+ if (channelId) followChannel(id, channelId);
1411
+ else if (result.resume === void 0) refresh2(id);
1203
1412
  } catch (e) {
1204
1413
  setError(e.message);
1205
- setStreaming(false);
1414
+ refresh2(id);
1206
1415
  }
1207
1416
  })();
1208
1417
  },
1209
- [client, handleStreamChunks, refreshMessages]
1418
+ [client, gate, followChannel, refresh2]
1210
1419
  );
1211
1420
  const approve = import_react5.default.useCallback(
1212
- (toolCallId) => handleApproval(toolCallId, true),
1213
- [handleApproval]
1421
+ (toolCallId, opts) => decide(toolCallId, true, !!opts?.alwaysAllow),
1422
+ [decide]
1214
1423
  );
1215
1424
  const reject = import_react5.default.useCallback(
1216
- (toolCallId) => handleApproval(toolCallId, false),
1217
- [handleApproval]
1425
+ (toolCallId) => decide(toolCallId, false, false),
1426
+ [decide]
1427
+ );
1428
+ const answer = import_react5.default.useCallback(
1429
+ async (toolCallId, text) => {
1430
+ const id = chatIdRef.current;
1431
+ if (!id) return false;
1432
+ setError(null);
1433
+ try {
1434
+ const result = await answerPendingInput(client, id, toolCallId, text);
1435
+ gate.next();
1436
+ setPendingInputs((prev) => prev.filter((p) => p.toolCallId !== toolCallId));
1437
+ const channelId = answerResumeChannel(result);
1438
+ if (channelId) followChannel(id, channelId);
1439
+ return true;
1440
+ } catch (e) {
1441
+ setError(e.message);
1442
+ refresh2(id);
1443
+ return false;
1444
+ }
1445
+ },
1446
+ [client, gate, followChannel, refresh2]
1447
+ );
1448
+ const follow = import_react5.default.useCallback(
1449
+ (channelId, opts) => {
1450
+ const id = chatIdRef.current;
1451
+ if (!id) return;
1452
+ followChannel(id, channelId, opts?.lastId);
1453
+ },
1454
+ [followChannel]
1218
1455
  );
1219
1456
  const stop = import_react5.default.useCallback(() => {
1220
1457
  abortRef.current?.abort();
1221
1458
  abortRef.current = null;
1222
1459
  setStreaming(false);
1460
+ setThinking(stopThinking);
1223
1461
  }, []);
1224
1462
  const clearError = import_react5.default.useCallback(() => {
1225
1463
  setError(null);
@@ -1231,10 +1469,14 @@ function useChat(agentRef, options) {
1231
1469
  streaming,
1232
1470
  error,
1233
1471
  pendingApprovals,
1472
+ pendingInputs,
1473
+ thinking: thinkingShown,
1234
1474
  toolStatus,
1235
1475
  send,
1236
1476
  approve,
1237
1477
  reject,
1478
+ answer,
1479
+ follow,
1238
1480
  stop,
1239
1481
  clearError
1240
1482
  };
@@ -1338,6 +1580,9 @@ function useAuth(options = {}) {
1338
1580
  SinasPermissionError,
1339
1581
  SinasProvider,
1340
1582
  SinasTimeoutError,
1583
+ answerPendingInput,
1584
+ answerResumeChannel,
1585
+ approvalResumeChannel,
1341
1586
  approveToolCall,
1342
1587
  cancelBatch,
1343
1588
  checkPermissions,
@@ -1350,11 +1595,16 @@ function useAuth(options = {}) {
1350
1595
  getChat,
1351
1596
  getInfo,
1352
1597
  getMe,
1598
+ invokeAgent,
1353
1599
  listBatchExecutions,
1354
1600
  listCollections,
1355
1601
  listFiles,
1602
+ listPendingInputs,
1356
1603
  login,
1357
1604
  logout,
1605
+ parsePendingInput,
1606
+ parsePendingInputs,
1607
+ pendingInputFromChunk,
1358
1608
  refresh,
1359
1609
  sendMessage,
1360
1610
  stateDelete,