flare-chat-core 0.1.1 → 0.2.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "flare-chat-core",
3
3
  "private": false,
4
- "version": "0.1.1",
4
+ "version": "0.2.1",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "module": "./src/index.js",
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
+ PKG_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
6
+
7
+ level="${1:-quick}"
8
+ case "$level" in
9
+ quick|full) ;;
10
+ *) echo "Unsupported level: $level" >&2; exit 2 ;;
11
+ esac
12
+
13
+ find "$PKG_DIR/src" -type f -name '*.js' | while IFS= read -r file; do
14
+ node --check "$file"
15
+ done
@@ -1,5 +1,18 @@
1
1
  import { useState, useCallback } from 'react';
2
2
 
3
+ function buildAttachmentFile(file, index = 0, status = 'ready') {
4
+ return {
5
+ uid: file?.uid || `${Date.now()}-${index}`,
6
+ name: file?.name || `附件-${index + 1}`,
7
+ size: Number.isFinite(file?.size) ? file.size : null,
8
+ type: file?.type || '',
9
+ originFileObj: file?.originFileObj || file || null,
10
+ status: file?.status || status,
11
+ error_message: file?.error_message || '',
12
+ created_at: file?.created_at || new Date().toISOString(),
13
+ };
14
+ }
15
+
3
16
  export function useChatInput() {
4
17
  const [inputValue, setInputValue] = useState('');
5
18
  const [fileList, setFileList] = useState([]);
@@ -23,18 +36,37 @@ export function useChatInput() {
23
36
  setIsDragging(false);
24
37
 
25
38
  const files = Array.from(e.dataTransfer.files);
26
- const newFiles = files.map((file, index) => ({
27
- uid: `${Date.now()}-${index}`,
28
- name: file.name,
29
- size: file.size,
30
- type: file.type,
31
- originFileObj: file,
32
- }));
39
+ const newFiles = files.map((file, index) => buildAttachmentFile(file, index));
33
40
 
34
41
  setFileList((prev) => [...prev, ...newFiles]);
35
42
  return files.length;
36
43
  }, []);
37
44
 
45
+ const appendFiles = useCallback((files = [], options = {}) => {
46
+ const normalized = Array.isArray(files)
47
+ ? files.map((file, index) => buildAttachmentFile(file, index, options.status || 'ready'))
48
+ : [];
49
+ if (normalized.length === 0) {
50
+ return;
51
+ }
52
+ setFileList((prev) => [...prev, ...normalized]);
53
+ }, []);
54
+
55
+ const updateFileStatus = useCallback((uid, status, errorMessage = '') => {
56
+ if (!uid) {
57
+ return;
58
+ }
59
+ setFileList((prev) => prev.map((file) => (
60
+ file.uid === uid
61
+ ? {
62
+ ...file,
63
+ status: status || file.status,
64
+ error_message: errorMessage || '',
65
+ }
66
+ : file
67
+ )));
68
+ }, []);
69
+
38
70
  const handleDragOver = useCallback((e) => {
39
71
  e.preventDefault();
40
72
  e.stopPropagation();
@@ -59,6 +91,8 @@ export function useChatInput() {
59
91
  handleInputChange,
60
92
  handleFileChange,
61
93
  handleRemoveFile,
94
+ appendFiles,
95
+ updateFileStatus,
62
96
  handleDrop,
63
97
  handleDragOver,
64
98
  handleDragLeave,
@@ -67,4 +101,3 @@ export function useChatInput() {
67
101
  }
68
102
 
69
103
  export default useChatInput;
70
-
@@ -1,8 +1,8 @@
1
- export function buildTimelineItems(state = {}) {
1
+ export function buildTimelineItems(state = {}, options = {}) {
2
2
  const messages = Array.isArray(state.messages) ? state.messages : [];
3
- const executionCards = Array.isArray(state.executionCards) ? state.executionCards : [];
4
3
  const uiCards = Array.isArray(state.uiCards) ? state.uiCards : [];
5
4
  const streaming = state.streaming || {};
5
+ const loading = Boolean(options?.loading);
6
6
 
7
7
  const items = [];
8
8
 
@@ -21,23 +21,15 @@ export function buildTimelineItems(state = {}) {
21
21
  role: msg.role,
22
22
  content: msg.content,
23
23
  createdAt: msg.created_at,
24
+ client_request_id: msg.client_request_id || '',
25
+ intake_session_id: msg.intake_session_id || '',
26
+ roundKey: msg.round_key || '',
24
27
  highlights: msg.highlights ?? [],
25
28
  sourceTypes: msg.sourceTypes ?? [],
29
+ attachments: Array.isArray(msg.attachments) ? msg.attachments : [],
26
30
  });
27
31
  });
28
32
 
29
- executionCards
30
- .filter(Boolean)
31
- .filter((card) => card.step_id !== streaming.executionTrace?.step_id)
32
- .forEach((card, index) => {
33
- const executionId = card.step_id || `execution-fallback-${index}`;
34
- items.push({
35
- id: executionId,
36
- type: 'execution',
37
- executionTrace: card,
38
- });
39
- });
40
-
41
33
  if (uiCards.length > 0) {
42
34
  items.push({
43
35
  id: 'conversation-ui-cards',
@@ -54,7 +46,8 @@ export function buildTimelineItems(state = {}) {
54
46
  });
55
47
  }
56
48
 
57
- if (streaming.agentStatus || streaming.thinkingTrace || streaming.executionTrace) {
49
+ const hasThinkingSignal = Boolean(streaming.agentStatus || streaming.thinkingTrace || streaming.executionTrace);
50
+ if (hasThinkingSignal) {
58
51
  items.push({
59
52
  id: 'thinking-bubble',
60
53
  type: 'thinking',
@@ -62,6 +55,15 @@ export function buildTimelineItems(state = {}) {
62
55
  thinkingTrace: streaming.thinkingTrace,
63
56
  executionTrace: streaming.executionTrace,
64
57
  });
58
+ } else if (loading && !streaming.content) {
59
+ items.push({
60
+ id: 'thinking-bubble-loading',
61
+ type: 'thinking',
62
+ agentStatus: {
63
+ status: 'running',
64
+ agent: '助手',
65
+ },
66
+ });
65
67
  }
66
68
 
67
69
  return {
@@ -71,4 +73,3 @@ export function buildTimelineItems(state = {}) {
71
73
  }
72
74
 
73
75
  export default buildTimelineItems;
74
-
@@ -8,6 +8,7 @@ const MESSAGES_REFRESHED = 'MESSAGES_REFRESHED';
8
8
  const MESSAGE_APPENDED = 'MESSAGE_APPENDED';
9
9
  const STREAMING_RESET = 'STREAMING_RESET';
10
10
  const STREAMING_CHUNK = 'STREAMING_CHUNK';
11
+ const STREAMING_REPLACE = 'STREAMING_REPLACE';
11
12
  const AGENT_STATUS_SET = 'AGENT_STATUS_SET';
12
13
  const THINKING_TRACE_SET = 'THINKING_TRACE_SET';
13
14
  const EXECUTION_TRACE_SET = 'EXECUTION_TRACE_SET';
@@ -19,6 +20,7 @@ const UI_CARDS_SET = 'UI_CARDS_SET';
19
20
  const LAST_USER_MESSAGE_SET = 'LAST_USER_MESSAGE_SET';
20
21
  const UI_CARD_UPDATED = 'UI_CARD_UPDATED';
21
22
  const INSTANCE_PROFILE_SET = 'INSTANCE_PROFILE_SET';
23
+ const SESSION_PROMOTED = 'SESSION_PROMOTED';
22
24
 
23
25
  const initialStreaming = {
24
26
  content: '',
@@ -31,6 +33,7 @@ export const initialState = {
31
33
  sessionId: null,
32
34
  sessionTitle: '',
33
35
  sessionStatus: 'active',
36
+ sessionDetail: null,
34
37
  instanceProfile: null,
35
38
  messages: [],
36
39
  executionCards: [],
@@ -48,12 +51,19 @@ export const initialState = {
48
51
  function reducer(state, { type, payload }) {
49
52
  switch (type) {
50
53
  case SESSION_LOADED: {
51
- const { sessionId, title, status, messages } = payload;
54
+ const {
55
+ sessionId,
56
+ title,
57
+ status,
58
+ messages,
59
+ detail,
60
+ } = payload;
52
61
  return {
53
62
  ...state,
54
63
  sessionId,
55
64
  sessionTitle: title,
56
65
  sessionStatus: status || 'active',
66
+ sessionDetail: (detail && typeof detail === 'object' && !Array.isArray(detail)) ? detail : null,
57
67
  instanceProfile: state.instanceProfile,
58
68
  messages,
59
69
  executionCards: [],
@@ -103,6 +113,40 @@ function reducer(state, { type, payload }) {
103
113
  case INSTANCE_PROFILE_SET:
104
114
  return { ...state, instanceProfile: payload };
105
115
 
116
+ case SESSION_PROMOTED: {
117
+ const sessionId = String(payload?.sessionId || '').trim();
118
+ if (!sessionId) {
119
+ return state;
120
+ }
121
+ const title = String(payload?.title || '').trim();
122
+ const detailPayload = (
123
+ payload?.detail
124
+ && typeof payload.detail === 'object'
125
+ && !Array.isArray(payload.detail)
126
+ ) ? payload.detail : null;
127
+ const nextDetail = detailPayload
128
+ ? {
129
+ ...(state.sessionDetail && typeof state.sessionDetail === 'object' ? state.sessionDetail : {}),
130
+ ...detailPayload,
131
+ sessionId,
132
+ }
133
+ : (
134
+ state.sessionDetail && typeof state.sessionDetail === 'object'
135
+ ? {
136
+ ...state.sessionDetail,
137
+ sessionId,
138
+ }
139
+ : null
140
+ );
141
+ return {
142
+ ...state,
143
+ sessionId,
144
+ sessionTitle: title || state.sessionTitle,
145
+ sessionStatus: String(payload?.status || state.sessionStatus || 'active'),
146
+ sessionDetail: nextDetail,
147
+ };
148
+ }
149
+
106
150
  case EXECUTION_CARD_UPSERTED: {
107
151
  const nextCard = payload;
108
152
  const cardKey = nextCard.step_id || `${nextCard.agent}-${nextCard.label}-${nextCard.stage}`;
@@ -135,6 +179,15 @@ function reducer(state, { type, payload }) {
135
179
  },
136
180
  };
137
181
 
182
+ case STREAMING_REPLACE:
183
+ return {
184
+ ...state,
185
+ streaming: {
186
+ ...state.streaming,
187
+ content: String(payload || ''),
188
+ },
189
+ };
190
+
138
191
  case AGENT_STATUS_SET:
139
192
  return {
140
193
  ...state,
@@ -223,8 +276,10 @@ export default function useChatSessionReducer(deps = {}) {
223
276
  title: detail.title,
224
277
  status: detail.status || 'active',
225
278
  messages: msgs.messages,
279
+ detail,
226
280
  },
227
281
  });
282
+ return detail;
228
283
  }, [sessionAPI, messageAPI]);
229
284
 
230
285
  const createSession = useCallback(async ({
@@ -242,6 +297,7 @@ export default function useChatSessionReducer(deps = {}) {
242
297
  title,
243
298
  status: response.status || 'active',
244
299
  messages: [],
300
+ detail: response,
245
301
  },
246
302
  });
247
303
 
@@ -295,6 +351,7 @@ export default function useChatSessionReducer(deps = {}) {
295
351
  title,
296
352
  status: detail.status || 'active',
297
353
  messages: msgs.messages,
354
+ detail,
298
355
  },
299
356
  });
300
357
 
@@ -311,6 +368,10 @@ export default function useChatSessionReducer(deps = {}) {
311
368
  dispatch({ type: TITLE_UPDATED, payload: title });
312
369
  }, [sessionAPI]);
313
370
 
371
+ const promoteSession = useCallback((payload = {}) => {
372
+ dispatch({ type: SESSION_PROMOTED, payload });
373
+ }, []);
374
+
314
375
  const refreshMessages = useCallback(async (sessionId) => {
315
376
  const messageApi = requireAPI(messageAPI, 'messageAPI');
316
377
  const response = await messageApi.list(sessionId);
@@ -321,7 +382,14 @@ export default function useChatSessionReducer(deps = {}) {
321
382
  dispatch({ type: MESSAGES_REFRESHED, payload: messagesWithIds });
322
383
  }, [messageAPI]);
323
384
 
324
- const appendUserMessage = useCallback((content) => {
385
+ const appendUserMessage = useCallback((content, metadata = {}) => {
386
+ const payloadMeta = (
387
+ metadata
388
+ && typeof metadata === 'object'
389
+ && !Array.isArray(metadata)
390
+ )
391
+ ? metadata
392
+ : {};
325
393
  dispatch({
326
394
  type: MESSAGE_APPENDED,
327
395
  payload: {
@@ -329,6 +397,7 @@ export default function useChatSessionReducer(deps = {}) {
329
397
  role: 'user',
330
398
  content,
331
399
  created_at: new Date().toISOString(),
400
+ ...payloadMeta,
332
401
  },
333
402
  });
334
403
  }, []);
@@ -353,6 +422,10 @@ export default function useChatSessionReducer(deps = {}) {
353
422
  dispatch({ type: STREAMING_CHUNK, payload: chunk });
354
423
  }, []);
355
424
 
425
+ const replaceStreamContent = useCallback((content) => {
426
+ dispatch({ type: STREAMING_REPLACE, payload: content });
427
+ }, []);
428
+
356
429
  const setAgentStatus = useCallback((agentStatus) => {
357
430
  dispatch({ type: AGENT_STATUS_SET, payload: agentStatus });
358
431
  }, []);
@@ -389,6 +462,7 @@ export default function useChatSessionReducer(deps = {}) {
389
462
  return useMemo(() => ({
390
463
  sessionId: state.sessionId,
391
464
  sessionTitle: state.sessionTitle,
465
+ sessionDetail: state.sessionDetail,
392
466
  instanceProfile: state.instanceProfile,
393
467
  messages: state.messages,
394
468
  executionCards: state.executionCards,
@@ -403,6 +477,7 @@ export default function useChatSessionReducer(deps = {}) {
403
477
  createOrLoadSession,
404
478
  resetSession,
405
479
  updateTitle,
480
+ promoteSession,
406
481
  refreshMessages,
407
482
  setSessionError,
408
483
  appendUserMessage,
@@ -411,6 +486,7 @@ export default function useChatSessionReducer(deps = {}) {
411
486
  setLastUserMessage,
412
487
  resetStreaming,
413
488
  appendStreamChunk,
489
+ replaceStreamContent,
414
490
  setAgentStatus,
415
491
  setThinkingTrace,
416
492
  setExecutionTrace,
@@ -425,6 +501,7 @@ export default function useChatSessionReducer(deps = {}) {
425
501
  createOrLoadSession,
426
502
  resetSession,
427
503
  updateTitle,
504
+ promoteSession,
428
505
  refreshMessages,
429
506
  setSessionError,
430
507
  appendUserMessage,
@@ -433,6 +510,7 @@ export default function useChatSessionReducer(deps = {}) {
433
510
  setLastUserMessage,
434
511
  resetStreaming,
435
512
  appendStreamChunk,
513
+ replaceStreamContent,
436
514
  setAgentStatus,
437
515
  setThinkingTrace,
438
516
  setExecutionTrace,
@@ -13,11 +13,19 @@ function joinUrl(baseUrl, endpoint) {
13
13
  return `${baseUrl.replace(/\/+$/, '')}/${endpoint.replace(/^\/+/, '')}`;
14
14
  }
15
15
 
16
+ function buildClientRequestId() {
17
+ const random = Math.random().toString(36).slice(2, 10);
18
+ return `req_${Date.now()}_${random}`;
19
+ }
20
+
16
21
  export class SSEClient {
17
22
  constructor({ endpoint = DEFAULT_ENDPOINT, baseUrl = API_BASE_URL } = {}) {
18
23
  this.abortController = null;
19
24
  this.endpoint = endpoint;
20
25
  this.baseUrl = baseUrl;
26
+ this.activeRequestId = '';
27
+ this.authReadyPromise = null;
28
+ this.authReadyAtMs = 0;
21
29
  }
22
30
 
23
31
  async sendMessage(sessionOrParams, contentArg, onEventArg, onCompleteArg, onErrorArg, optionsArg = {}) {
@@ -33,26 +41,124 @@ export class SSEClient {
33
41
  const sessionId = params.sessionId;
34
42
  const content = params.content;
35
43
  const enabledCapabilities = Array.isArray(params.enabledCapabilities) ? params.enabledCapabilities : [];
44
+ const modeKey = typeof params.modeKey === 'string' ? params.modeKey.trim() : '';
45
+ const manualModeKey = typeof params.manualModeKey === 'string' ? params.manualModeKey.trim() : '';
46
+ const payloadExtra = (
47
+ params.payloadExtra
48
+ && typeof params.payloadExtra === 'object'
49
+ && !Array.isArray(params.payloadExtra)
50
+ )
51
+ ? params.payloadExtra
52
+ : {};
36
53
  const onEvent = isObjectCall ? contentArg : onEventArg;
37
54
  const onComplete = isObjectCall ? onEventArg : onCompleteArg;
38
55
  const onError = isObjectCall ? onCompleteArg : onErrorArg;
39
56
  const url = params.url || joinUrl(params.baseUrl ?? this.baseUrl, params.endpoint || this.endpoint);
40
-
57
+ const command = typeof params.command === 'string' && params.command.trim()
58
+ ? params.command.trim()
59
+ : (typeof payloadExtra.command === 'string' && payloadExtra.command.trim() ? payloadExtra.command.trim() : 'send_message');
60
+ const clientRequestId = typeof params.clientRequestId === 'string' && params.clientRequestId.trim()
61
+ ? params.clientRequestId.trim()
62
+ : buildClientRequestId();
63
+ const contractVersion = typeof params.contractVersion === 'string' && params.contractVersion.trim()
64
+ ? params.contractVersion.trim()
65
+ : 'flare.v1';
66
+ const lastTurnId = typeof params.lastTurnId === 'string' ? params.lastTurnId.trim() : '';
67
+ const token = typeof params.token === 'string' ? params.token.trim() : '';
68
+ const onTiming = typeof params.onTiming === 'function' ? params.onTiming : null;
69
+ const authProvider = (
70
+ params.auth
71
+ && typeof params.auth === 'object'
72
+ && !Array.isArray(params.auth)
73
+ ) ? params.auth : null;
74
+
75
+ const nowMs = Date.now();
76
+ onTiming?.({
77
+ point: 't_submit',
78
+ at_ms: nowMs,
79
+ request_id: clientRequestId,
80
+ session_id: String(sessionId || '').trim(),
81
+ });
82
+
83
+ const waitAuthReady = async () => {
84
+ if (!authProvider || typeof authProvider.ensureReady !== 'function') {
85
+ return {
86
+ authorizationToken: token,
87
+ authReadyAtMs: nowMs,
88
+ };
89
+ }
90
+ if (!this.authReadyPromise) {
91
+ this.authReadyPromise = Promise.resolve()
92
+ .then(() => authProvider.ensureReady())
93
+ .then((resolved) => {
94
+ this.authReadyAtMs = Date.now();
95
+ return resolved || null;
96
+ })
97
+ .finally(() => {
98
+ this.authReadyPromise = null;
99
+ });
100
+ }
101
+ const authResult = await this.authReadyPromise;
102
+ const tokenFromAuthResult = typeof authResult?.token === 'string' ? authResult.token.trim() : '';
103
+ const tokenFromProvider = typeof authProvider.getToken === 'function'
104
+ ? String(authProvider.getToken() || '').trim()
105
+ : '';
106
+ return {
107
+ authorizationToken: tokenFromAuthResult || tokenFromProvider || token,
108
+ authReadyAtMs: this.authReadyAtMs || Date.now(),
109
+ };
110
+ };
111
+
112
+ const currentRequestId = clientRequestId;
113
+ if (this.abortController) {
114
+ this.abortController.abort();
115
+ this.abortController = null;
116
+ }
41
117
  this.abortController = new AbortController();
118
+ this.activeRequestId = currentRequestId;
42
119
 
43
120
  try {
121
+ const authResolved = await waitAuthReady();
122
+ onTiming?.({
123
+ point: 't_auth_ready',
124
+ at_ms: authResolved.authReadyAtMs || Date.now(),
125
+ request_id: currentRequestId,
126
+ session_id: String(sessionId || '').trim(),
127
+ });
128
+ if (this.activeRequestId !== currentRequestId) {
129
+ return;
130
+ }
44
131
  const response = await fetch(url, {
45
132
  method: 'POST',
46
133
  headers: {
47
134
  'Content-Type': 'application/json',
135
+ ...(authResolved.authorizationToken ? { Authorization: `Bearer ${authResolved.authorizationToken}` } : {}),
48
136
  },
49
137
  body: JSON.stringify({
138
+ contract_version: contractVersion,
139
+ client_request_id: clientRequestId,
140
+ command,
141
+ last_turn_id: lastTurnId || null,
50
142
  message: content,
51
143
  session_id: sessionId,
52
144
  enabled_capabilities: enabledCapabilities,
145
+ ...(modeKey ? { mode: modeKey } : {}),
146
+ ...(manualModeKey ? { manual_mode: manualModeKey } : {}),
147
+ payload: {
148
+ message: content,
149
+ ...(modeKey ? { mode: modeKey } : {}),
150
+ ...(manualModeKey ? { manual_mode: manualModeKey } : {}),
151
+ ...payloadExtra,
152
+ },
53
153
  }),
54
154
  signal: this.abortController.signal,
55
155
  });
156
+ onTiming?.({
157
+ point: 't_stream_connected',
158
+ at_ms: Date.now(),
159
+ request_id: currentRequestId,
160
+ session_id: String(sessionId || '').trim(),
161
+ });
56
162
 
57
163
  if (!response.ok) {
58
164
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -88,13 +194,20 @@ export class SSEClient {
88
194
 
89
195
  try {
90
196
  const eventData = JSON.parse(data);
197
+ if (this.activeRequestId !== currentRequestId) {
198
+ return;
199
+ }
91
200
  onEvent?.({
92
201
  type: currentEvent,
93
202
  data: eventData,
94
203
  });
95
204
 
96
- if (currentEvent === 'complete') {
97
- onComplete?.();
205
+ if (currentEvent === 'complete' || currentEvent === 'done') {
206
+ await onComplete?.();
207
+ if (this.activeRequestId === currentRequestId) {
208
+ this.abortController = null;
209
+ this.activeRequestId = '';
210
+ }
98
211
  return;
99
212
  }
100
213
  } catch (error) {
@@ -103,13 +216,21 @@ export class SSEClient {
103
216
  }
104
217
  }
105
218
 
106
- onComplete?.();
219
+ await onComplete?.();
220
+ if (this.activeRequestId === currentRequestId) {
221
+ this.abortController = null;
222
+ this.activeRequestId = '';
223
+ }
107
224
  } catch (error) {
108
225
  if (error?.name === 'AbortError') {
109
226
  return;
110
227
  }
111
228
 
112
229
  onError?.(error instanceof Error ? error.message : '发送消息失败');
230
+ if (this.activeRequestId === currentRequestId) {
231
+ this.abortController = null;
232
+ this.activeRequestId = '';
233
+ }
113
234
  }
114
235
  }
115
236
 
@@ -118,6 +239,6 @@ export class SSEClient {
118
239
  this.abortController.abort();
119
240
  this.abortController = null;
120
241
  }
242
+ this.activeRequestId = '';
121
243
  }
122
244
  }
123
-
@@ -145,6 +145,49 @@ export const parseContent = (raw) => ({
145
145
  chunk: raw?.content ?? '',
146
146
  });
147
147
 
148
+ export const parseAck = (raw) => ({
149
+ trace_id: String(raw?.trace_id || '').trim(),
150
+ session_id: String(raw?.session_id || '').trim(),
151
+ request_id: String(raw?.request_id || '').trim(),
152
+ mode: String(raw?.mode || '').trim(),
153
+ ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
154
+ });
155
+
156
+ export const parsePhaseEvent = (raw) => ({
157
+ phase: String(raw?.phase || '').trim(),
158
+ label: String(raw?.label || '').trim(),
159
+ status: String(raw?.status || '').trim(),
160
+ progress: Number.isFinite(Number(raw?.progress)) ? Number(raw.progress) : null,
161
+ meta: raw?.meta && typeof raw.meta === 'object' && !Array.isArray(raw.meta) ? raw.meta : {},
162
+ ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
163
+ });
164
+
165
+ export const parsePatch = (raw) => ({
166
+ scope: String(raw?.scope || '').trim(),
167
+ payload: raw?.payload && typeof raw.payload === 'object' && !Array.isArray(raw.payload) ? raw.payload : {},
168
+ ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
169
+ });
170
+
171
+ export const parseTextDelta = (raw) => ({
172
+ channel: String(raw?.channel || 'assistant').trim() || 'assistant',
173
+ delta: String(raw?.delta || ''),
174
+ ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
175
+ });
176
+
177
+ export const parseTextReplace = (raw) => ({
178
+ channel: String(raw?.channel || 'assistant').trim() || 'assistant',
179
+ content: String(raw?.content || ''),
180
+ ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
181
+ });
182
+
183
+ export const parseDone = (raw) => ({
184
+ trace_id: String(raw?.trace_id || '').trim(),
185
+ session_id: String(raw?.session_id || '').trim(),
186
+ request_id: String(raw?.request_id || '').trim(),
187
+ status: String(raw?.status || 'done').trim() || 'done',
188
+ ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
189
+ });
190
+
148
191
  export const parseTrace = (raw) => ({
149
192
  trace: raw?.trace ?? raw?.detail ?? '',
150
193
  summary: raw?.summary ?? raw?.label ?? '',
@@ -169,9 +212,49 @@ function pickFirstArray(payload, keys) {
169
212
  return [];
170
213
  }
171
214
 
215
+ function parseGuidedSession(rawGuidedSession) {
216
+ const payload = (rawGuidedSession && typeof rawGuidedSession === 'object' && !Array.isArray(rawGuidedSession))
217
+ ? rawGuidedSession
218
+ : {};
219
+ const questionQueue = (payload.question_queue && typeof payload.question_queue === 'object' && !Array.isArray(payload.question_queue))
220
+ ? payload.question_queue
221
+ : {};
222
+ const summary = (payload.summary && typeof payload.summary === 'object' && !Array.isArray(payload.summary))
223
+ ? payload.summary
224
+ : {};
225
+ const decisionPoint = (payload.decision_point && typeof payload.decision_point === 'object' && !Array.isArray(payload.decision_point))
226
+ ? payload.decision_point
227
+ : {};
228
+ return {
229
+ state: payload?.state ?? '',
230
+ active: payload?.active === true,
231
+ current_question: payload?.current_question && typeof payload.current_question === 'object' && !Array.isArray(payload.current_question)
232
+ ? payload.current_question
233
+ : null,
234
+ question_queue: {
235
+ total: Number.isFinite(Number(questionQueue?.total)) ? Number(questionQueue.total) : 0,
236
+ answered: Number.isFinite(Number(questionQueue?.answered)) ? Number(questionQueue.answered) : 0,
237
+ remaining: Number.isFinite(Number(questionQueue?.remaining)) ? Number(questionQueue.remaining) : 0,
238
+ pending_fields: Array.isArray(questionQueue?.pending_fields) ? questionQueue.pending_fields : [],
239
+ },
240
+ summary: {
241
+ text: summary?.text ?? '',
242
+ ready: summary?.ready === true,
243
+ },
244
+ decision_point: {
245
+ active: decisionPoint?.active === true,
246
+ actions: Array.isArray(decisionPoint?.actions) ? decisionPoint.actions : [],
247
+ },
248
+ };
249
+ }
250
+
172
251
  export const parseFieldProgress = (raw) => {
173
252
  const payload = resolveModeEventPayload(raw);
174
253
  return {
254
+ intake_session_id: payload?.intake_session_id ?? '',
255
+ intake_session_state: payload?.intake_session_state ?? '',
256
+ intake_session_completed: payload?.intake_session_completed === true,
257
+ flow_state: payload?.flow_state ?? payload?.collection_phase ?? '',
175
258
  fields: payload?.fields ?? {},
176
259
  sources: payload?.sources ?? {},
177
260
  field_entries: Array.isArray(payload?.field_entries) ? payload.field_entries : [],
@@ -197,9 +280,29 @@ export const parseFieldProgress = (raw) => {
197
280
  required_fields: Array.isArray(payload?.required_fields) ? payload.required_fields : [],
198
281
  recommended_fields: Array.isArray(payload?.recommended_fields) ? payload.recommended_fields : [],
199
282
  optional_fields: Array.isArray(payload?.optional_fields) ? payload.optional_fields : [],
283
+ intake_core_fields: Array.isArray(payload?.intake_core_fields) ? payload.intake_core_fields : [],
284
+ intake_supplementary_fields: Array.isArray(payload?.intake_supplementary_fields) ? payload.intake_supplementary_fields : [],
285
+ analysis_enrichment_fields: Array.isArray(payload?.analysis_enrichment_fields) ? payload.analysis_enrichment_fields : [],
200
286
  field_priorities: payload?.field_priorities && typeof payload.field_priorities === 'object' && !Array.isArray(payload.field_priorities)
201
287
  ? payload.field_priorities
202
288
  : {},
289
+ current_question: payload?.current_question && typeof payload.current_question === 'object' && !Array.isArray(payload.current_question)
290
+ ? payload.current_question
291
+ : null,
292
+ question_progress: payload?.question_progress && typeof payload.question_progress === 'object' && !Array.isArray(payload.question_progress)
293
+ ? payload.question_progress
294
+ : { current: 0, total: 0 },
295
+ collection_phase: payload?.collection_phase ?? '',
296
+ required_missing_count: Number.isFinite(payload?.required_missing_count) ? payload.required_missing_count : null,
297
+ required_coverage: Number.isFinite(Number(payload?.required_coverage)) ? Number(payload.required_coverage) : 0,
298
+ total_coverage: Number.isFinite(Number(payload?.total_coverage)) ? Number(payload.total_coverage) : 0,
299
+ analysis_entry_threshold: Number.isFinite(Number(payload?.analysis_entry_threshold)) ? Number(payload.analysis_entry_threshold) : 0.8,
300
+ analysis_entry_eligible: payload?.analysis_entry_eligible === true,
301
+ active_collecting: payload?.active_collecting === true,
302
+ ready_for_submit: payload?.ready_for_submit === true,
303
+ decision_point_active: payload?.decision_point_active === true,
304
+ has_active_question: payload?.has_active_question === true,
305
+ guided_session: parseGuidedSession(payload?.guided_session),
203
306
  };
204
307
  };
205
308
 
@@ -222,6 +325,9 @@ export const parseRequirementDraft = (raw) => {
222
325
  : {};
223
326
 
224
327
  return {
328
+ intake_session_id: payload?.intake_session_id ?? '',
329
+ intake_session_state: payload?.intake_session_state ?? '',
330
+ intake_session_completed: payload?.intake_session_completed === true,
225
331
  mode_key: payload?.mode_key ?? '',
226
332
  project_id: payload?.project_id ?? null,
227
333
  message_excerpt: payload?.message_excerpt ?? '',
@@ -239,7 +345,11 @@ export const parseNextActions = (raw) => {
239
345
  ? payload.actions
240
346
  : (Array.isArray(payload?.next_actions) ? payload.next_actions : []);
241
347
  return {
348
+ intake_session_id: payload?.intake_session_id ?? '',
349
+ intake_session_state: payload?.intake_session_state ?? '',
350
+ intake_session_completed: payload?.intake_session_completed === true,
242
351
  mode_key: payload?.mode_key ?? '',
352
+ flow_state: payload?.flow_state ?? payload?.collection_phase ?? '',
243
353
  project_id: payload?.project_id ?? null,
244
354
  message_excerpt: payload?.message_excerpt ?? '',
245
355
  actions,
@@ -247,10 +357,37 @@ export const parseNextActions = (raw) => {
247
357
  required_missing: Array.isArray(payload?.required_missing) ? payload.required_missing : [],
248
358
  recommended_missing: Array.isArray(payload?.recommended_missing) ? payload.recommended_missing : [],
249
359
  optional_missing: Array.isArray(payload?.optional_missing) ? payload.optional_missing : [],
360
+ intake_core_fields: Array.isArray(payload?.intake_core_fields) ? payload.intake_core_fields : [],
361
+ intake_supplementary_fields: Array.isArray(payload?.intake_supplementary_fields) ? payload.intake_supplementary_fields : [],
362
+ analysis_enrichment_fields: Array.isArray(payload?.analysis_enrichment_fields) ? payload.analysis_enrichment_fields : [],
250
363
  ready_for_sourcing: payload?.ready_for_sourcing ?? null,
251
364
  status: payload?.status ?? '',
252
365
  reason: payload?.reason ?? '',
366
+ chooser_required: payload?.chooser_required === true,
367
+ blocking_reason: payload?.blocking_reason ?? '',
368
+ blocking: payload?.blocking && typeof payload.blocking === 'object' && !Array.isArray(payload.blocking)
369
+ ? payload.blocking
370
+ : null,
371
+ target_field: payload?.target_field ?? '',
372
+ degrade_reason: Array.isArray(payload?.degrade_reason) ? payload.degrade_reason : [],
373
+ current_question: payload?.current_question && typeof payload.current_question === 'object' && !Array.isArray(payload.current_question)
374
+ ? payload.current_question
375
+ : null,
376
+ question_progress: payload?.question_progress && typeof payload.question_progress === 'object' && !Array.isArray(payload.question_progress)
377
+ ? payload.question_progress
378
+ : { current: 0, total: 0 },
379
+ collection_phase: payload?.collection_phase ?? '',
380
+ required_missing_count: Number.isFinite(payload?.required_missing_count) ? payload.required_missing_count : null,
253
381
  render_hint: payload?.render_hint ?? 'next_actions',
382
+ required_coverage: Number.isFinite(Number(payload?.required_coverage)) ? Number(payload.required_coverage) : 0,
383
+ total_coverage: Number.isFinite(Number(payload?.total_coverage)) ? Number(payload.total_coverage) : 0,
384
+ analysis_entry_threshold: Number.isFinite(Number(payload?.analysis_entry_threshold)) ? Number(payload.analysis_entry_threshold) : 0.8,
385
+ analysis_entry_eligible: payload?.analysis_entry_eligible === true,
386
+ active_collecting: payload?.active_collecting === true,
387
+ ready_for_submit: payload?.ready_for_submit === true,
388
+ decision_point_active: payload?.decision_point_active === true,
389
+ has_active_question: payload?.has_active_question === true,
390
+ guided_session: parseGuidedSession(payload?.guided_session),
254
391
  };
255
392
  };
256
393
 
@@ -378,6 +515,151 @@ export const parseEvaluationReportReady = (raw) => {
378
515
  };
379
516
  };
380
517
 
518
+ export const parsePlanBlock = (raw) => {
519
+ const payload = resolveModeEventPayload(raw);
520
+ return {
521
+ ...payload,
522
+ constraints: Array.isArray(payload?.constraints) ? payload.constraints : [],
523
+ plan: Array.isArray(payload?.plan) ? payload.plan : [],
524
+ acceptance: Array.isArray(payload?.acceptance) ? payload.acceptance : [],
525
+ actions: Array.isArray(payload?.actions) ? payload.actions : [],
526
+ };
527
+ };
528
+
529
+ export const parseCanvasState = (raw) => {
530
+ const payload = resolveModeEventPayload(raw);
531
+ const canvasState = payload?.canvas_state && typeof payload.canvas_state === 'object' && !Array.isArray(payload.canvas_state)
532
+ ? payload.canvas_state
533
+ : {};
534
+ return {
535
+ ...payload,
536
+ canvas_state: {
537
+ ...canvasState,
538
+ versions: Array.isArray(canvasState.versions) ? canvasState.versions : [],
539
+ },
540
+ };
541
+ };
542
+
543
+ export const parseCanvasRevision = (raw) => {
544
+ const payload = resolveModeEventPayload(raw);
545
+ return {
546
+ ...payload,
547
+ actions: Array.isArray(payload?.actions) ? payload.actions : [],
548
+ };
549
+ };
550
+
551
+ export const parseKnowledgeSearch = (raw) => {
552
+ const payload = resolveModeEventPayload(raw);
553
+ const sourceBreakdown = payload?.source_breakdown && typeof payload.source_breakdown === 'object'
554
+ && !Array.isArray(payload.source_breakdown)
555
+ ? payload.source_breakdown
556
+ : {};
557
+ return {
558
+ ...payload,
559
+ run_id: payload?.run_id ?? '',
560
+ query: payload?.query ?? '',
561
+ result_count: Number.isFinite(payload?.result_count) ? payload.result_count : null,
562
+ source_breakdown: {
563
+ local: Number.isFinite(sourceBreakdown.local) ? sourceBreakdown.local : 0,
564
+ mcp: Number.isFinite(sourceBreakdown.mcp) ? sourceBreakdown.mcp : 0,
565
+ web: Number.isFinite(sourceBreakdown.web) ? sourceBreakdown.web : 0,
566
+ },
567
+ results: Array.isArray(payload?.results) ? payload.results : [],
568
+ };
569
+ };
570
+
571
+ export const parseKnowledgeCitation = (raw) => {
572
+ const payload = resolveModeEventPayload(raw);
573
+ return {
574
+ ...payload,
575
+ run_id: payload?.run_id ?? '',
576
+ citations: Array.isArray(payload?.citations) ? payload.citations : [],
577
+ };
578
+ };
579
+
580
+ export const parseOrchestrationStatus = (raw) => {
581
+ const payload = resolveModeEventPayload(raw);
582
+ const activeWorkItem = payload?.active_work_item && typeof payload.active_work_item === 'object' && !Array.isArray(payload.active_work_item)
583
+ ? payload.active_work_item
584
+ : null;
585
+ return {
586
+ trace_id: payload?.trace_id ?? '',
587
+ intake_session_id: payload?.intake_session_id ?? '',
588
+ intake_session_state: payload?.intake_session_state ?? '',
589
+ intake_session_completed: payload?.intake_session_completed === true,
590
+ current_stage: payload?.current_stage ?? '',
591
+ detected_intent: payload?.detected_intent && typeof payload.detected_intent === 'object' && !Array.isArray(payload.detected_intent)
592
+ ? payload.detected_intent
593
+ : {},
594
+ confirmed_updates: Array.isArray(payload?.confirmed_updates) ? payload.confirmed_updates : [],
595
+ open_fields: Array.isArray(payload?.open_fields) ? payload.open_fields : [],
596
+ search_actions: Array.isArray(payload?.search_actions) ? payload.search_actions : [],
597
+ workspace_updates: payload?.workspace_updates && typeof payload.workspace_updates === 'object' && !Array.isArray(payload.workspace_updates)
598
+ ? payload.workspace_updates
599
+ : {},
600
+ next_action: payload?.next_action && typeof payload.next_action === 'object' && !Array.isArray(payload.next_action)
601
+ ? payload.next_action
602
+ : {},
603
+ next_actions: Array.isArray(payload?.next_actions) ? payload.next_actions : [],
604
+ active_collecting: payload?.active_collecting === true,
605
+ ready_for_submit: payload?.ready_for_submit === true,
606
+ required_coverage: Number.isFinite(Number(payload?.required_coverage)) ? Number(payload.required_coverage) : 0,
607
+ total_coverage: Number.isFinite(Number(payload?.total_coverage)) ? Number(payload.total_coverage) : 0,
608
+ analysis_entry_threshold: Number.isFinite(Number(payload?.analysis_entry_threshold)) ? Number(payload.analysis_entry_threshold) : 0.8,
609
+ analysis_entry_eligible: payload?.analysis_entry_eligible === true,
610
+ decision_point_active: payload?.decision_point_active === true,
611
+ has_active_question: payload?.has_active_question === true,
612
+ guided_session: parseGuidedSession(payload?.guided_session),
613
+ active_work_item: activeWorkItem,
614
+ active_fields: Array.isArray(payload?.active_fields) ? payload.active_fields : [],
615
+ hypotheses: Array.isArray(payload?.hypotheses) ? payload.hypotheses : [],
616
+ planner: payload?.planner && typeof payload.planner === 'object' && !Array.isArray(payload.planner)
617
+ ? payload.planner
618
+ : {},
619
+ chooser: payload?.chooser && typeof payload.chooser === 'object' && !Array.isArray(payload.chooser)
620
+ ? payload.chooser
621
+ : null,
622
+ stage_transition: payload?.stage_transition && typeof payload.stage_transition === 'object' && !Array.isArray(payload.stage_transition)
623
+ ? payload.stage_transition
624
+ : {},
625
+ decision_basis: Array.isArray(payload?.decision_basis) ? payload.decision_basis : [],
626
+ blocking: payload?.blocking && typeof payload.blocking === 'object' && !Array.isArray(payload.blocking)
627
+ ? payload.blocking
628
+ : {},
629
+ node: payload?.node && typeof payload.node === 'object' && !Array.isArray(payload.node)
630
+ ? payload.node
631
+ : {},
632
+ node_id: payload?.node_id ?? '',
633
+ node_state: payload?.node_state ?? '',
634
+ node_reason: payload?.node_reason ?? '',
635
+ needs_user_choice: payload?.needs_user_choice === true,
636
+ conversation_intent_state: payload?.conversation_intent_state ?? '',
637
+ session_intent_stage: payload?.session_intent_stage ?? '',
638
+ intent_type: payload?.intent_type ?? '',
639
+ procurement_relevance: payload?.procurement_relevance === true,
640
+ consultative_procurement_qa: payload?.consultative_procurement_qa === true,
641
+ escalation_target: payload?.escalation_target ?? '',
642
+ intake_candidate: payload?.intake_candidate === true,
643
+ clarification_recommended: payload?.clarification_recommended === true,
644
+ analysis_ready: payload?.analysis_ready === true,
645
+ draft_seeded: payload?.draft_seeded === true,
646
+ missing_key_fields: Array.isArray(payload?.missing_key_fields) ? payload.missing_key_fields : [],
647
+ recommended_next_step: payload?.recommended_next_step ?? '',
648
+ intent_and_escalation_confidence: Number.isFinite(Number(payload?.intent_and_escalation_confidence))
649
+ ? Number(payload.intent_and_escalation_confidence)
650
+ : 0,
651
+ intent_and_escalation_reason: payload?.intent_and_escalation_reason ?? '',
652
+ intent_escalation_policy: payload?.intent_escalation_policy && typeof payload.intent_escalation_policy === 'object' && !Array.isArray(payload.intent_escalation_policy)
653
+ ? payload.intent_escalation_policy
654
+ : {},
655
+ intent_escalation_policy_source: payload?.intent_escalation_policy_source ?? '',
656
+ degrade_reason: Array.isArray(payload?.degrade_reason) ? payload.degrade_reason : [],
657
+ content_policy: payload?.content_policy && typeof payload.content_policy === 'object' && !Array.isArray(payload.content_policy)
658
+ ? payload.content_policy
659
+ : {},
660
+ };
661
+ };
662
+
381
663
  export const parseWorkspaceActivation = (raw) => ({
382
664
  function_type: raw?.function_type ?? '',
383
665
  mode: raw?.mode ?? '',
@@ -420,6 +702,166 @@ export const parseInstanceProfile = (raw) => ({
420
702
  ui_labels: raw?.instance_profile?.ui_labels ?? raw?.ui_labels ?? {},
421
703
  });
422
704
 
705
+ export const parseModeRuntime = (raw) => ({
706
+ mode_key: raw?.mode_key ?? '',
707
+ mode_state: raw?.mode_state ?? '',
708
+ });
709
+
710
+ export const parseAgentRuntime = (raw) => ({
711
+ trace_id: raw?.trace_id ?? '',
712
+ session_id: raw?.session_id ?? '',
713
+ intent: raw?.intent ?? '',
714
+ function_type: raw?.function_type ?? '',
715
+ mode_key: raw?.mode_key ?? '',
716
+ mode_state: raw?.mode_state ?? '',
717
+ agent_step_count: Number.isFinite(raw?.agent_step_count) ? raw.agent_step_count : 0,
718
+ agent_step: Array.isArray(raw?.agent_step) ? raw.agent_step : [],
719
+ });
720
+
721
+ export const parseSkillRuntime = (raw) => ({
722
+ trace_id: raw?.trace_id ?? '',
723
+ session_id: raw?.session_id ?? '',
724
+ intent: raw?.intent ?? '',
725
+ function_type: raw?.function_type ?? '',
726
+ mode_key: raw?.mode_key ?? '',
727
+ mode_state: raw?.mode_state ?? '',
728
+ skill_call_count: Number.isFinite(raw?.skill_call_count) ? raw.skill_call_count : 0,
729
+ skill_call: Array.isArray(raw?.skill_call) ? raw.skill_call : [],
730
+ });
731
+
732
+ export const parseModeSwitchReason = (raw) => ({
733
+ mode_key: raw?.mode_key ?? '',
734
+ intent: raw?.intent ?? '',
735
+ source: raw?.source ?? '',
736
+ previous_mode: raw?.previous_mode ?? '',
737
+ manual_mode: raw?.manual_mode ?? '',
738
+ current_mode: raw?.current_mode ?? '',
739
+ intent_override: raw?.intent_override ?? '',
740
+ summary: raw?.summary ?? '',
741
+ });
742
+
423
743
  export const parseSSEError = (raw) => ({
424
744
  message: raw?.message ?? '发生错误',
425
745
  });
746
+
747
+ export const PATCH_SCOPE_AUTHORITATIVE = Object.freeze([
748
+ 'session',
749
+ 'assistant_reply',
750
+ 'primary_flow',
751
+ 'checkpoint',
752
+ 'intent_state',
753
+ 'question',
754
+ 'confirmed_fields',
755
+ 'missing_fields',
756
+ 'next_actions',
757
+ 'analysis',
758
+ 'observation',
759
+ 'capabilities.field_retrieval',
760
+ 'errors',
761
+ ]);
762
+
763
+ export const PATCH_SCOPE_LEGACY_COMPAT = Object.freeze([
764
+ 'current_question',
765
+ 'recommendation_summary',
766
+ ]);
767
+
768
+ const PATCH_SCOPE_ALLOWLIST = new Set([
769
+ ...PATCH_SCOPE_AUTHORITATIVE,
770
+ ...PATCH_SCOPE_LEGACY_COMPAT,
771
+ ]);
772
+
773
+ function getByScopePath(payload, scope) {
774
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
775
+ return undefined;
776
+ }
777
+ const segments = String(scope || '').split('.').filter(Boolean);
778
+ if (!segments.length) {
779
+ return undefined;
780
+ }
781
+ let current = payload;
782
+ for (const segment of segments) {
783
+ if (!current || typeof current !== 'object' || Array.isArray(current) || !(segment in current)) {
784
+ return undefined;
785
+ }
786
+ current = current[segment];
787
+ }
788
+ return current;
789
+ }
790
+
791
+ export const parsePatchEvent = (raw) => {
792
+ const envelope = (raw && typeof raw === 'object' && !Array.isArray(raw)) ? raw : {};
793
+ const patchScope = Array.isArray(envelope.patch_scope)
794
+ ? envelope.patch_scope.map((item) => String(item || '').trim()).filter(Boolean)
795
+ : [];
796
+ const payload = (envelope.payload && typeof envelope.payload === 'object' && !Array.isArray(envelope.payload))
797
+ ? envelope.payload
798
+ : {};
799
+ const hasPayload = Object.keys(payload).length > 0;
800
+ const hasUnknownScope = patchScope.some((item) => !(PATCH_SCOPE_ALLOWLIST.has(item) || item.startsWith('confirmed_fields.')));
801
+ const hasScopePayloadMismatch = patchScope.some((scope) => {
802
+ const direct = payload[scope];
803
+ const byPath = getByScopePath(payload, scope);
804
+ const lastSegment = String(scope).split('.').slice(-1)[0];
805
+ const leaf = payload[lastSegment];
806
+ return direct === undefined && byPath === undefined && leaf === undefined;
807
+ });
808
+ const legacyPatchScope = patchScope.filter((scope) => PATCH_SCOPE_LEGACY_COMPAT.includes(scope));
809
+ const authoritativePatchScope = patchScope.filter(
810
+ (scope) => PATCH_SCOPE_AUTHORITATIVE.includes(scope) || scope.startsWith('confirmed_fields.')
811
+ );
812
+ const invalidReason = (() => {
813
+ if (envelope.message_type !== 'patch_event') {
814
+ return 'invalid_message_type';
815
+ }
816
+ if (!['ack', 'patch', 'error', 'final'].includes(String(envelope.event_type || '').trim())) {
817
+ return 'invalid_event_type';
818
+ }
819
+ if (hasPayload && patchScope.length === 0) {
820
+ return 'payload_without_patch_scope';
821
+ }
822
+ if (hasUnknownScope) {
823
+ return 'unknown_patch_scope';
824
+ }
825
+ if (patchScope.length > 0 && hasScopePayloadMismatch) {
826
+ return 'patch_scope_payload_mismatch';
827
+ }
828
+ return '';
829
+ })();
830
+ return {
831
+ contract_version: String(envelope.contract_version || 'flare.v1'),
832
+ message_type: 'patch_event',
833
+ event_type: String(envelope.event_type || '').trim(),
834
+ sequence: Number.isFinite(Number(envelope.sequence)) ? Number(envelope.sequence) : 0,
835
+ trace_id: String(envelope.trace_id || '').trim(),
836
+ client_request_id: String(envelope.client_request_id || '').trim(),
837
+ intake_session_id: String(
838
+ payload?.primary_flow?.intake_session_id
839
+ || payload?.checkpoint?.intake_session_id
840
+ || payload?.next_actions?.intake_session_id
841
+ || payload?.question?.intake_session_id
842
+ || payload?.intake_session_id
843
+ || ''
844
+ ).trim(),
845
+ intake_session_state: String(
846
+ payload?.primary_flow?.intake_session_state
847
+ || payload?.next_actions?.intake_session_state
848
+ || payload?.intake_session_state
849
+ || ''
850
+ ).trim(),
851
+ intake_session_completed: Boolean(
852
+ payload?.primary_flow?.intake_session_completed
853
+ || payload?.next_actions?.intake_session_completed
854
+ || payload?.intake_session_completed
855
+ ),
856
+ session: (
857
+ envelope.session && typeof envelope.session === 'object' && !Array.isArray(envelope.session)
858
+ ) ? envelope.session : { session_id: '', turn_id: '' },
859
+ patch_scope: patchScope,
860
+ authoritative_patch_scope: authoritativePatchScope,
861
+ legacy_patch_scope: legacyPatchScope,
862
+ payload,
863
+ final: envelope.final === true,
864
+ invalid: Boolean(invalidReason),
865
+ invalid_reason: invalidReason,
866
+ };
867
+ };
@@ -1,10 +1,16 @@
1
- import { useRef, useState, useCallback } from 'react';
1
+ import { useRef, useState, useCallback, useEffect } from 'react';
2
2
  import { SSEClient } from './sse-client.js';
3
3
  import {
4
+ parseAck,
4
5
  parseAgentStatus,
5
6
  parseThinkingTrace,
6
7
  parseExecutionTrace,
7
8
  parseContent,
9
+ parseDone,
10
+ parsePatch,
11
+ parsePhaseEvent,
12
+ parseTextDelta,
13
+ parseTextReplace,
8
14
  parseTrace,
9
15
  parseFieldProgress,
10
16
  parseDoc,
@@ -21,6 +27,17 @@ import {
21
27
  parseCapabilitySuggestion,
22
28
  parseFieldsUpdated,
23
29
  parseInstanceProfile,
30
+ parseModeRuntime,
31
+ parseAgentRuntime,
32
+ parseSkillRuntime,
33
+ parseModeSwitchReason,
34
+ parseKnowledgeSearch,
35
+ parseKnowledgeCitation,
36
+ parseOrchestrationStatus,
37
+ parseCanvasState,
38
+ parseCanvasRevision,
39
+ parsePlanBlock,
40
+ parsePatchEvent,
24
41
  parseSSEError,
25
42
  } from './sse-events.js';
26
43
 
@@ -42,19 +59,28 @@ export function useSSEStream(sessionId) {
42
59
  const lastMessageRef = useRef({ content: '', handlers: {} });
43
60
 
44
61
  const send = useCallback(async (content, handlers = {}, options = {}) => {
45
- const resolvedSessionId = options.sessionIdOverride || sessionId;
46
- if (!resolvedSessionId || !content.trim()) return;
62
+ const hasSessionOverride = Object.prototype.hasOwnProperty.call(options, 'sessionIdOverride');
63
+ const resolvedSessionId = hasSessionOverride ? options.sessionIdOverride : sessionId;
64
+ if (!content.trim()) return;
47
65
 
48
66
  lastMessageRef.current = { content, handlers, options };
49
67
  setError(null);
50
68
  accumulatedRef.current = '';
51
69
  setLoading(true);
70
+ let latestSessionId = String(resolvedSessionId || '').trim();
52
71
 
53
72
  const {
73
+ onAck,
54
74
  onAgentStatus,
55
75
  onThinkingTrace,
56
76
  onExecutionTrace,
57
77
  onContent,
78
+ onTextReplace,
79
+ onPhaseStart,
80
+ onPhaseUpdate,
81
+ onPhaseEnd,
82
+ onPatch,
83
+ onDone,
58
84
  onTrace,
59
85
  onWorkspaceActivation,
60
86
  onUICards,
@@ -69,14 +95,40 @@ export function useSSEStream(sessionId) {
69
95
  onCategoryIdentified,
70
96
  onFieldsUpdated,
71
97
  onInstanceProfile,
98
+ onModeRuntime,
99
+ onAgentRuntime,
100
+ onSkillRuntime,
101
+ onModeSwitchReason,
102
+ onKnowledgeSearch,
103
+ onKnowledgeCitation,
104
+ onOrchestrationStatus,
105
+ onCanvasState,
106
+ onCanvasRevision,
107
+ onPlanBlock,
72
108
  onDoc,
73
109
  onCapabilitySuggestion,
110
+ onPatchEvent,
74
111
  onError,
75
112
  onComplete,
76
113
  } = handlers;
77
114
 
78
115
  const eventHandler = (event) => {
79
116
  switch (event.type) {
117
+ case 'ack':
118
+ onAck?.(parseAck(event.data));
119
+ break;
120
+ case 'phase.start':
121
+ onPhaseStart?.(parsePhaseEvent(event.data));
122
+ break;
123
+ case 'phase.update':
124
+ onPhaseUpdate?.(parsePhaseEvent(event.data));
125
+ break;
126
+ case 'phase.end':
127
+ onPhaseEnd?.(parsePhaseEvent(event.data));
128
+ break;
129
+ case 'patch':
130
+ onPatch?.(parsePatch(event.data));
131
+ break;
80
132
  case 'agent_status':
81
133
  onAgentStatus?.(parseAgentStatus(event.data));
82
134
  break;
@@ -96,6 +148,25 @@ export function useSSEStream(sessionId) {
96
148
  onContent?.(chunk);
97
149
  break;
98
150
  }
151
+ case 'text.delta': {
152
+ const parsed = parseTextDelta(event.data);
153
+ if (parsed.channel === 'assistant' && parsed.delta) {
154
+ accumulatedRef.current += parsed.delta;
155
+ onContent?.(parsed.delta);
156
+ }
157
+ break;
158
+ }
159
+ case 'text.replace': {
160
+ const parsed = parseTextReplace(event.data);
161
+ if (parsed.channel === 'assistant') {
162
+ accumulatedRef.current = parsed.content;
163
+ onTextReplace?.(parsed.content);
164
+ }
165
+ break;
166
+ }
167
+ case 'done':
168
+ onDone?.(parseDone(event.data));
169
+ break;
99
170
  case 'trace':
100
171
  onTrace?.(parseTrace(event.data));
101
172
  break;
@@ -138,12 +209,52 @@ export function useSSEStream(sessionId) {
138
209
  case 'instance_profile':
139
210
  onInstanceProfile?.(parseInstanceProfile(event.data));
140
211
  break;
212
+ case 'mode_runtime':
213
+ onModeRuntime?.(parseModeRuntime(event.data));
214
+ break;
215
+ case 'agent_runtime':
216
+ onAgentRuntime?.(parseAgentRuntime(event.data));
217
+ break;
218
+ case 'skill_runtime':
219
+ onSkillRuntime?.(parseSkillRuntime(event.data));
220
+ break;
221
+ case 'mode_switch_reason':
222
+ onModeSwitchReason?.(parseModeSwitchReason(event.data));
223
+ break;
224
+ case 'knowledge_search':
225
+ onKnowledgeSearch?.(parseKnowledgeSearch(event.data));
226
+ break;
227
+ case 'knowledge_citation':
228
+ onKnowledgeCitation?.(parseKnowledgeCitation(event.data));
229
+ break;
230
+ case 'orchestration_status':
231
+ onOrchestrationStatus?.(parseOrchestrationStatus(event.data));
232
+ break;
233
+ case 'canvas_state':
234
+ onCanvasState?.(parseCanvasState(event.data));
235
+ break;
236
+ case 'canvas_revision':
237
+ onCanvasRevision?.(parseCanvasRevision(event.data));
238
+ break;
239
+ case 'plan_block':
240
+ onPlanBlock?.(parsePlanBlock(event.data));
241
+ break;
141
242
  case 'doc':
142
243
  onDoc?.(parseDoc(event.data));
143
244
  break;
144
245
  case 'capability_suggestion':
145
246
  onCapabilitySuggestion?.(parseCapabilitySuggestion(event.data));
146
247
  break;
248
+ case 'patch_event':
249
+ {
250
+ const parsedPatchEvent = parsePatchEvent(event.data);
251
+ const patchSessionId = String(parsedPatchEvent?.session?.session_id || '').trim();
252
+ if (patchSessionId) {
253
+ latestSessionId = patchSessionId;
254
+ }
255
+ onPatchEvent?.(parsedPatchEvent);
256
+ }
257
+ break;
147
258
  case 'error':
148
259
  onError?.(parseSSEError(event.data));
149
260
  break;
@@ -152,9 +263,11 @@ export function useSSEStream(sessionId) {
152
263
  }
153
264
  };
154
265
 
155
- const completeHandler = () => {
266
+ const completeHandler = async () => {
156
267
  setLoading(false);
157
- onComplete?.(accumulatedRef.current);
268
+ await onComplete?.(accumulatedRef.current, {
269
+ sessionId: latestSessionId || null,
270
+ });
158
271
  accumulatedRef.current = '';
159
272
  };
160
273
 
@@ -167,21 +280,51 @@ export function useSSEStream(sessionId) {
167
280
  };
168
281
 
169
282
  const requestOptions = {
170
- sessionId: resolvedSessionId,
283
+ sessionId: String(resolvedSessionId || '').trim() || null,
171
284
  content,
172
285
  enabledCapabilities: Array.isArray(options.enabledCapabilities) ? options.enabledCapabilities : [],
286
+ modeKey: typeof options.modeKey === 'string' ? options.modeKey : '',
287
+ manualModeKey: typeof options.manualModeKey === 'string' ? options.manualModeKey : '',
288
+ command: typeof options.command === 'string' ? options.command : '',
289
+ clientRequestId: typeof options.clientRequestId === 'string' ? options.clientRequestId : '',
290
+ contractVersion: typeof options.contractVersion === 'string' ? options.contractVersion : '',
291
+ lastTurnId: typeof options.lastTurnId === 'string' ? options.lastTurnId : '',
292
+ payloadExtra: (
293
+ options.payloadExtra
294
+ && typeof options.payloadExtra === 'object'
295
+ && !Array.isArray(options.payloadExtra)
296
+ )
297
+ ? options.payloadExtra
298
+ : {},
173
299
  endpoint: options.endpoint,
174
300
  baseUrl: options.baseUrl,
175
301
  url: options.url,
302
+ token: typeof options.token === 'string' ? options.token : '',
303
+ auth: (
304
+ options.auth
305
+ && typeof options.auth === 'object'
306
+ && !Array.isArray(options.auth)
307
+ ) ? options.auth : undefined,
308
+ onTiming: typeof options.onTiming === 'function' ? options.onTiming : undefined,
176
309
  };
177
310
 
178
311
  if (!requestOptions.enabledCapabilities.length) {
179
312
  delete requestOptions.enabledCapabilities;
180
313
  }
314
+ if (!requestOptions.modeKey) delete requestOptions.modeKey;
315
+ if (!requestOptions.manualModeKey) delete requestOptions.manualModeKey;
316
+ if (!requestOptions.command) delete requestOptions.command;
317
+ if (!requestOptions.clientRequestId) delete requestOptions.clientRequestId;
318
+ if (!requestOptions.contractVersion) delete requestOptions.contractVersion;
319
+ if (!requestOptions.lastTurnId) delete requestOptions.lastTurnId;
320
+ if (!Object.keys(requestOptions.payloadExtra || {}).length) delete requestOptions.payloadExtra;
181
321
 
182
322
  if (!requestOptions.endpoint) delete requestOptions.endpoint;
183
323
  if (!requestOptions.baseUrl) delete requestOptions.baseUrl;
184
324
  if (!requestOptions.url) delete requestOptions.url;
325
+ if (!requestOptions.token) delete requestOptions.token;
326
+ if (!requestOptions.auth) delete requestOptions.auth;
327
+ if (!requestOptions.onTiming) delete requestOptions.onTiming;
185
328
 
186
329
  await clientRef.current.sendMessage(
187
330
  requestOptions,
@@ -203,6 +346,10 @@ export function useSSEStream(sessionId) {
203
346
  accumulatedRef.current = '';
204
347
  }, []);
205
348
 
349
+ useEffect(() => () => {
350
+ clientRef.current.abort();
351
+ }, []);
352
+
206
353
  return { send, loading, error, retry, abort };
207
354
  }
208
355