|
|
@ -73,6 +73,51 @@ const getInterruptStatus = (item = {}) => { |
|
|
const isTerminalInterrupt = (item = {}) => |
|
|
const isTerminalInterrupt = (item = {}) => |
|
|
TERMINAL_INTERRUPT_STATUSES.has(getInterruptStatus(item)); |
|
|
TERMINAL_INTERRUPT_STATUSES.has(getInterruptStatus(item)); |
|
|
|
|
|
|
|
|
|
|
|
const getInterruptInteractionKey = (item = {}) => { |
|
|
|
|
|
const payload = item?.value || item?.payload || item || {}; |
|
|
|
|
|
const interactionId = String( |
|
|
|
|
|
payload.interaction_id || |
|
|
|
|
|
payload.interactionId || |
|
|
|
|
|
item.interaction_id || |
|
|
|
|
|
item.interactionId || |
|
|
|
|
|
"" |
|
|
|
|
|
).trim(); |
|
|
|
|
|
if (!interactionId) return ""; |
|
|
|
|
|
|
|
|
|
|
|
const revision = payload.revision ?? item.revision ?? ""; |
|
|
|
|
|
return `${interactionId}:${String(revision)}`; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const parseMessageContent = (content) => { |
|
|
|
|
|
if (content && typeof content === "object") return content; |
|
|
|
|
|
if (typeof content !== "string") return null; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
return JSON.parse(content); |
|
|
|
|
|
} catch (_) { |
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const collectResolvedInterruptInteractions = (messages = []) => { |
|
|
|
|
|
const resolvedInteractions = new Set(); |
|
|
|
|
|
(Array.isArray(messages) ? messages : []).forEach((message) => { |
|
|
|
|
|
if (String(message?.type || "").toLowerCase() !== "tool") return; |
|
|
|
|
|
|
|
|
|
|
|
const result = parseMessageContent(message.content); |
|
|
|
|
|
const decisionType = String(result?.type || "").toLowerCase(); |
|
|
|
|
|
const interactionId = String(result?.interaction_id || "").trim(); |
|
|
|
|
|
const revision = result?.revision ?? ""; |
|
|
|
|
|
if ( |
|
|
|
|
|
interactionId && |
|
|
|
|
|
["approve", "edit", "reject"].includes(decisionType) |
|
|
|
|
|
) { |
|
|
|
|
|
resolvedInteractions.add(`${interactionId}:${String(revision)}`); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
return resolvedInteractions; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const isSuperAgentEventRequest = (input) => { |
|
|
const isSuperAgentEventRequest = (input) => { |
|
|
const requestUrl = String(input?.url || input || ""); |
|
|
const requestUrl = String(input?.url || input || ""); |
|
|
return /\/threads\/[^/]+\/stream\/events(?:\?|$)/.test(requestUrl); |
|
|
return /\/threads\/[^/]+\/stream\/events(?:\?|$)/.test(requestUrl); |
|
|
@ -394,6 +439,7 @@ const useSuperAgentStream = ({ |
|
|
const [runtimeInterrupts, setRuntimeInterrupts] = useState([]); |
|
|
const [runtimeInterrupts, setRuntimeInterrupts] = useState([]); |
|
|
const [dismissedInterruptIds, setDismissedInterruptIds] = useState(new Set()); |
|
|
const [dismissedInterruptIds, setDismissedInterruptIds] = useState(new Set()); |
|
|
const resolvedInterruptIdsRef = useRef(new Set()); |
|
|
const resolvedInterruptIdsRef = useRef(new Set()); |
|
|
|
|
|
const resolvedInterruptInteractionsRef = useRef(new Set()); |
|
|
const [currentInterruptId, setCurrentInterruptId] = useState(""); |
|
|
const [currentInterruptId, setCurrentInterruptId] = useState(""); |
|
|
const [hydratedThreadId, setHydratedThreadId] = useState(""); |
|
|
const [hydratedThreadId, setHydratedThreadId] = useState(""); |
|
|
const [transportStateRevision, setTransportStateRevision] = useState(0); |
|
|
const [transportStateRevision, setTransportStateRevision] = useState(0); |
|
|
@ -580,6 +626,7 @@ const useSuperAgentStream = ({ |
|
|
setRuntimeInterrupts([]); |
|
|
setRuntimeInterrupts([]); |
|
|
setDismissedInterruptIds(new Set()); |
|
|
setDismissedInterruptIds(new Set()); |
|
|
resolvedInterruptIdsRef.current.clear(); |
|
|
resolvedInterruptIdsRef.current.clear(); |
|
|
|
|
|
resolvedInterruptInteractionsRef.current.clear(); |
|
|
setCurrentInterruptId(""); |
|
|
setCurrentInterruptId(""); |
|
|
setHydratedThreadId(""); |
|
|
setHydratedThreadId(""); |
|
|
}, [activeSessionId]); |
|
|
}, [activeSessionId]); |
|
|
@ -671,7 +718,12 @@ const useSuperAgentStream = ({ |
|
|
if (event?.method === "input.requested") { |
|
|
if (event?.method === "input.requested") { |
|
|
const data = event.params?.data || {}; |
|
|
const data = event.params?.data || {}; |
|
|
const interruptId = String(data.interrupt_id || "").trim(); |
|
|
const interruptId = String(data.interrupt_id || "").trim(); |
|
|
if (interruptId && !resolvedInterruptIdsRef.current.has(interruptId)) { |
|
|
const interactionKey = getInterruptInteractionKey(data.payload); |
|
|
|
|
|
const isResolvedInterrupt = |
|
|
|
|
|
resolvedInterruptIdsRef.current.has(interruptId) || |
|
|
|
|
|
(interactionKey && |
|
|
|
|
|
resolvedInterruptInteractionsRef.current.has(interactionKey)); |
|
|
|
|
|
if (interruptId && !isResolvedInterrupt) { |
|
|
localRunActiveRef.current = false; |
|
|
localRunActiveRef.current = false; |
|
|
setIsLocalRunActive(false); |
|
|
setIsLocalRunActive(false); |
|
|
setCurrentInterruptId(interruptId); |
|
|
setCurrentInterruptId(interruptId); |
|
|
@ -763,6 +815,7 @@ const useSuperAgentStream = ({ |
|
|
setIsBusinessRunning(false); |
|
|
setIsBusinessRunning(false); |
|
|
setRuntimeInterrupts([]); |
|
|
setRuntimeInterrupts([]); |
|
|
resolvedInterruptIdsRef.current.clear(); |
|
|
resolvedInterruptIdsRef.current.clear(); |
|
|
|
|
|
resolvedInterruptInteractionsRef.current.clear(); |
|
|
|
|
|
|
|
|
// 会话切换只断开当前浏览器订阅,不能发送 stop/cancel 命令。
|
|
|
// 会话切换只断开当前浏览器订阅,不能发送 stop/cancel 命令。
|
|
|
// stopRun 只由用户点击停止按钮触发,否则后端会把原会话改为 suspended。
|
|
|
// stopRun 只由用户点击停止按钮触发,否则后端会把原会话改为 suspended。
|
|
|
@ -862,6 +915,12 @@ const useSuperAgentStream = ({ |
|
|
}).then((result) => { |
|
|
}).then((result) => { |
|
|
if (interruptId) { |
|
|
if (interruptId) { |
|
|
resolvedInterruptIdsRef.current.add(interruptId); |
|
|
resolvedInterruptIdsRef.current.add(interruptId); |
|
|
|
|
|
const interactionKey = getInterruptInteractionKey( |
|
|
|
|
|
response?.value || response?.payload || response |
|
|
|
|
|
); |
|
|
|
|
|
if (interactionKey) { |
|
|
|
|
|
resolvedInterruptInteractionsRef.current.add(interactionKey); |
|
|
|
|
|
} |
|
|
setRuntimeInterrupts((currentInterrupts) => |
|
|
setRuntimeInterrupts((currentInterrupts) => |
|
|
currentInterrupts.filter( |
|
|
currentInterrupts.filter( |
|
|
(item) => String(item?.id || "") !== interruptId |
|
|
(item) => String(item?.id || "") !== interruptId |
|
|
@ -952,6 +1011,19 @@ const useSuperAgentStream = ({ |
|
|
const hydratedMessages = Array.isArray(hydratedState?.values?.messages) |
|
|
const hydratedMessages = Array.isArray(hydratedState?.values?.messages) |
|
|
? hydratedState.values.messages |
|
|
? hydratedState.values.messages |
|
|
: []; |
|
|
: []; |
|
|
|
|
|
const historicalResolvedInteractionKeys = useMemo( |
|
|
|
|
|
() => |
|
|
|
|
|
collectResolvedInterruptInteractions([ |
|
|
|
|
|
...hydratedMessages, |
|
|
|
|
|
...rawStreamMessages, |
|
|
|
|
|
]), |
|
|
|
|
|
[hydratedMessages, rawStreamMessages] |
|
|
|
|
|
); |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
historicalResolvedInteractionKeys.forEach((interactionKey) => { |
|
|
|
|
|
resolvedInterruptInteractionsRef.current.add(interactionKey); |
|
|
|
|
|
}); |
|
|
|
|
|
}, [historicalResolvedInteractionKeys]); |
|
|
// 只有 hydrate 真正拿到消息后,才能解除首次提交的 thread 投影保护。
|
|
|
// 只有 hydrate 真正拿到消息后,才能解除首次提交的 thread 投影保护。
|
|
|
// 仅有 task 快照不代表消息已完成落库,否则 task 先到时会让当前消息再次被隐藏。
|
|
|
// 仅有 task 快照不代表消息已完成落库,否则 task 先到时会让当前消息再次被隐藏。
|
|
|
const hasHydratedSessionContent = hydratedMessages.length > 0; |
|
|
const hasHydratedSessionContent = hydratedMessages.length > 0; |
|
|
@ -1058,10 +1130,18 @@ const useSuperAgentStream = ({ |
|
|
})) |
|
|
})) |
|
|
: []), |
|
|
: []), |
|
|
]; |
|
|
]; |
|
|
const hasPersistedInterrupts = hydratedInterrupts.length > 0; |
|
|
const pendingHydratedInterrupts = hydratedInterrupts.filter((item) => { |
|
|
|
|
|
const interruptId = String(item?.id || item?.interruptId || "").trim(); |
|
|
|
|
|
const interactionKey = getInterruptInteractionKey(item); |
|
|
|
|
|
return ( |
|
|
|
|
|
!resolvedInterruptIdsRef.current.has(interruptId) && |
|
|
|
|
|
!historicalResolvedInteractionKeys.has(interactionKey) |
|
|
|
|
|
); |
|
|
|
|
|
}); |
|
|
|
|
|
const hasPersistedInterrupts = pendingHydratedInterrupts.length > 0; |
|
|
const persistedInterrupts = |
|
|
const persistedInterrupts = |
|
|
hydratedState |
|
|
hydratedState |
|
|
? hydratedInterrupts |
|
|
? pendingHydratedInterrupts |
|
|
: isStreamProjectionReady |
|
|
: isStreamProjectionReady |
|
|
? stream.interrupts |
|
|
? stream.interrupts |
|
|
: []; |
|
|
: []; |
|
|
@ -1132,14 +1212,25 @@ const useSuperAgentStream = ({ |
|
|
|
|
|
|
|
|
streamInterrupts.forEach((item) => appendInterrupt(item)); |
|
|
streamInterrupts.forEach((item) => appendInterrupt(item)); |
|
|
return [...merged.values()].filter( |
|
|
return [...merged.values()].filter( |
|
|
(item) => |
|
|
(item) => { |
|
|
!dismissedInterruptIds.has(String(item.id || "")) && |
|
|
const interruptId = String(item.id || ""); |
|
|
|
|
|
const interactionKey = getInterruptInteractionKey(item); |
|
|
|
|
|
const isHistoricalResolved = |
|
|
|
|
|
historicalResolvedInteractionKeys.has(interactionKey) || |
|
|
|
|
|
resolvedInterruptInteractionsRef.current.has(interactionKey); |
|
|
|
|
|
return ( |
|
|
|
|
|
!dismissedInterruptIds.has(interruptId) && |
|
|
|
|
|
!resolvedInterruptIdsRef.current.has(interruptId) && |
|
|
|
|
|
!isHistoricalResolved && |
|
|
!isTerminalInterrupt(item) |
|
|
!isTerminalInterrupt(item) |
|
|
); |
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
}, [ |
|
|
}, [ |
|
|
dismissedInterruptIds, |
|
|
dismissedInterruptIds, |
|
|
isInterruptProjectionReady, |
|
|
isInterruptProjectionReady, |
|
|
isStreamProjectionReady, |
|
|
isStreamProjectionReady, |
|
|
|
|
|
historicalResolvedInteractionKeys, |
|
|
streamInterrupts, |
|
|
streamInterrupts, |
|
|
]); |
|
|
]); |
|
|
|
|
|
|
|
|
@ -1170,7 +1261,7 @@ const useSuperAgentStream = ({ |
|
|
isPersistedThreadActive, |
|
|
isPersistedThreadActive, |
|
|
currentInterruptId: |
|
|
currentInterruptId: |
|
|
hydratedState && |
|
|
hydratedState && |
|
|
hydratedInterrupts.length === 0 && |
|
|
pendingHydratedInterrupts.length === 0 && |
|
|
runtimeInterrupts.length === 0 |
|
|
runtimeInterrupts.length === 0 |
|
|
? "" |
|
|
? "" |
|
|
: currentInterruptId, |
|
|
: currentInterruptId, |
|
|
|