feat(goal-run): add multi-checkpoint goal orchestration with H5 and admin surfaces.
Persist goal runs in MySQL, bind agent runs to checkpoints, expose awaiting-approval UX in chat, and add admin inspection routes with local verify scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -42,12 +42,14 @@ function AuthenticatedApp({
|
||||
user,
|
||||
capabilities,
|
||||
grantedSkills,
|
||||
goalRunEnabled,
|
||||
onUserUpdate,
|
||||
onLogout,
|
||||
}: {
|
||||
user: PortalUser | null;
|
||||
capabilities?: CapabilityMap;
|
||||
grantedSkills?: string[];
|
||||
goalRunEnabled?: boolean;
|
||||
onUserUpdate: (user: PortalUser) => void;
|
||||
onLogout: () => void;
|
||||
}) {
|
||||
@@ -98,6 +100,7 @@ function AuthenticatedApp({
|
||||
user={user}
|
||||
capabilities={capabilities}
|
||||
grantedSkills={grantedSkills}
|
||||
goalRunEnabled={goalRunEnabled}
|
||||
onUserUpdate={onUserUpdate}
|
||||
>
|
||||
<Routes>
|
||||
@@ -123,6 +126,7 @@ export function App() {
|
||||
const [user, setUser] = useState<PortalUser | null>(null);
|
||||
const [capabilities, setCapabilities] = useState<CapabilityMap | undefined>();
|
||||
const [grantedSkills, setGrantedSkills] = useState<string[] | undefined>();
|
||||
const [goalRunEnabled, setGoalRunEnabled] = useState<boolean | undefined>(undefined);
|
||||
const [legacyMode, setLegacyMode] = useState(false);
|
||||
const [authUnavailable, setAuthUnavailable] = useState<string | null>(null);
|
||||
useProductAnalytics(user?.id);
|
||||
@@ -148,6 +152,7 @@ export function App() {
|
||||
setUser(status.user ?? null);
|
||||
setCapabilities(status.capabilities);
|
||||
setGrantedSkills(status.grantedSkills);
|
||||
setGoalRunEnabled(status.goalRun?.enabled);
|
||||
if (status.authenticated) void loadBlockedWords();
|
||||
});
|
||||
return () => setUnauthorizedHandler(null);
|
||||
@@ -207,6 +212,9 @@ export function App() {
|
||||
setUser(nextUser ?? null);
|
||||
setCapabilities(nextCapabilities);
|
||||
setGrantedSkills(nextSkills);
|
||||
void checkAuth().then((status) => {
|
||||
setGoalRunEnabled(status.goalRun?.enabled);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -217,6 +225,7 @@ export function App() {
|
||||
user={user}
|
||||
capabilities={capabilities}
|
||||
grantedSkills={grantedSkills}
|
||||
goalRunEnabled={goalRunEnabled}
|
||||
onUserUpdate={setUser}
|
||||
onLogout={() => {
|
||||
clearAllStoredSessionIds();
|
||||
|
||||
@@ -1508,6 +1508,7 @@ export async function createAgentRun(
|
||||
...(options.selectedAssetIds?.length
|
||||
? { selected_asset_ids: options.selectedAssetIds }
|
||||
: {}),
|
||||
...(options.goalRunId ? { goal_run_id: options.goalRunId } : {}),
|
||||
}),
|
||||
},
|
||||
{ timeoutMs: AGENT_CONNECT_TIMEOUT_MS },
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { GoalRun } from '../types';
|
||||
import type { AgentRun } from './client';
|
||||
import { apiFetch, ApiError } from './core';
|
||||
|
||||
export type GoalRunAwaitingItem = {
|
||||
goal: GoalRun;
|
||||
checkpoint: GoalRun['checkpoints'][number];
|
||||
};
|
||||
|
||||
export function findAwaitingApprovalCheckpoint(goal: GoalRun) {
|
||||
return goal.checkpoints.find((item) => item.status === 'awaiting_approval') ?? null;
|
||||
}
|
||||
|
||||
export function listAwaitingGoalRuns(goals: GoalRun[]): GoalRunAwaitingItem[] {
|
||||
return goals
|
||||
.map((goal) => {
|
||||
const checkpoint = findAwaitingApprovalCheckpoint(goal);
|
||||
return checkpoint ? { goal, checkpoint } : null;
|
||||
})
|
||||
.filter((item): item is GoalRunAwaitingItem => item != null);
|
||||
}
|
||||
|
||||
export async function listGoalRuns(options?: {
|
||||
statuses?: string[];
|
||||
limit?: number;
|
||||
}): Promise<GoalRun[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.statuses?.length) {
|
||||
params.set('status', options.statuses.join(','));
|
||||
}
|
||||
if (options?.limit != null) {
|
||||
params.set('limit', String(options.limit));
|
||||
}
|
||||
const qs = params.size ? `?${params.toString()}` : '';
|
||||
const result = await apiFetch<{ goals: GoalRun[] }>(`/goals${qs}`);
|
||||
return result.goals ?? [];
|
||||
}
|
||||
|
||||
export async function getGoalRun(goalRunId: string): Promise<GoalRun | null> {
|
||||
try {
|
||||
const result = await apiFetch<{ goal: GoalRun }>(`/goals/${encodeURIComponent(goalRunId)}`);
|
||||
return result.goal ?? null;
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 404) return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function approveGoalCheckpoint(
|
||||
goalRunId: string,
|
||||
checkpointId: string,
|
||||
feedback?: string | null,
|
||||
sessionId?: string | null,
|
||||
): Promise<{ goal: GoalRun; run?: AgentRun | null }> {
|
||||
const result = await apiFetch<{ goal: GoalRun; run?: AgentRun | null }>(
|
||||
`/goals/${encodeURIComponent(goalRunId)}/checkpoints/${encodeURIComponent(checkpointId)}/approve`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
feedback: feedback ?? null,
|
||||
...(sessionId ? { session_id: sessionId } : {}),
|
||||
}),
|
||||
},
|
||||
);
|
||||
return { goal: result.goal, run: result.run ?? null };
|
||||
}
|
||||
|
||||
export async function cancelGoalRun(goalRunId: string): Promise<GoalRun> {
|
||||
const result = await apiFetch<{ goal: GoalRun }>(`/goals/${encodeURIComponent(goalRunId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
return result.goal;
|
||||
}
|
||||
|
||||
export function isGoalRunApiAvailableError(err: unknown): boolean {
|
||||
return err instanceof ApiError && (err.status === 403 || err.status === 503);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useChat } from '../context/ChatProvider';
|
||||
import { INSUFFICIENT_BALANCE_NOTICE } from '../hooks/useTKMindChat';
|
||||
import { useGoalRunBanner } from '../hooks/useGoalRunBanner';
|
||||
import type { CapabilityMap, PortalUser, SessionSummary } from '../types';
|
||||
import { useNetworkStatus } from '../hooks/useNetworkStatus';
|
||||
import { getSessionDisplayName } from '../utils/sessions';
|
||||
@@ -15,6 +16,7 @@ import { WechatBindPrompt } from './WechatBindPrompt';
|
||||
import { WechatAccountButton } from './WechatAccountButton';
|
||||
import { ChatHeaderMoreMenu } from './ChatHeaderMoreMenu';
|
||||
import { NotificationCenter } from './NotificationCenter';
|
||||
import { GoalRunAwaitingBanner } from './GoalRunAwaitingBanner';
|
||||
import type { MindSpaceSaveCategory } from '../types';
|
||||
|
||||
function RecentSessionRail({
|
||||
@@ -212,8 +214,18 @@ export function ChatView({
|
||||
openSubscribe,
|
||||
uploadChatImage,
|
||||
uploadChatAttachment,
|
||||
followAgentRun,
|
||||
goalRunEnabled,
|
||||
} = useChat();
|
||||
const online = useNetworkStatus();
|
||||
const goalRunBanner = useGoalRunBanner({
|
||||
userId: user?.id,
|
||||
sessionId: session?.id,
|
||||
chatState,
|
||||
submit,
|
||||
followAgentRun,
|
||||
featureEnabled: goalRunEnabled,
|
||||
});
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [packagePanelSession, setPackagePanelSession] = useState<SessionSummary | null>(null);
|
||||
const [armedSessionId, setArmedSessionId] = useState<string | null>(null);
|
||||
@@ -487,6 +499,16 @@ export function ChatView({
|
||||
|
||||
{user && <WechatBindPrompt returnTo={window.location.pathname} />}
|
||||
|
||||
{goalRunBanner.enabled && (
|
||||
<GoalRunAwaitingBanner
|
||||
items={goalRunBanner.items}
|
||||
busy={goalRunBanner.bannerBusy}
|
||||
onContinue={goalRunBanner.handleContinue}
|
||||
onDismiss={goalRunBanner.dismissItem}
|
||||
onRefresh={goalRunBanner.refresh}
|
||||
/>
|
||||
)}
|
||||
|
||||
{typeof balanceCents === 'number' && balanceCents <= 0 && (
|
||||
<div className="banner banner-error">
|
||||
<span>余额不足,请充值后继续使用</span>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useState } from 'react';
|
||||
import { cancelGoalRun, type GoalRunAwaitingItem } from '../api/goalRun';
|
||||
|
||||
type GoalRunAwaitingBannerProps = {
|
||||
items: GoalRunAwaitingItem[];
|
||||
busy: boolean;
|
||||
onContinue: (input: {
|
||||
goalRunId: string;
|
||||
checkpointId: string;
|
||||
feedback?: string | null;
|
||||
}) => Promise<void>;
|
||||
onDismiss: (goalRunId: string) => void;
|
||||
onRefresh: () => Promise<void>;
|
||||
};
|
||||
|
||||
function GoalRunAwaitingCard({
|
||||
item,
|
||||
busy,
|
||||
onContinue,
|
||||
onDismiss,
|
||||
onRefresh,
|
||||
}: {
|
||||
item: GoalRunAwaitingItem;
|
||||
busy: boolean;
|
||||
onContinue: GoalRunAwaitingBannerProps['onContinue'];
|
||||
onDismiss: (goalRunId: string) => void;
|
||||
onRefresh: () => Promise<void>;
|
||||
}) {
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [action, setAction] = useState<'continue' | 'cancel' | null>(null);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
const { goal, checkpoint } = item;
|
||||
const summary = checkpoint.outputSummary?.trim();
|
||||
|
||||
const handleContinue = async () => {
|
||||
setLocalError(null);
|
||||
setAction('continue');
|
||||
try {
|
||||
await onContinue({
|
||||
goalRunId: goal.id,
|
||||
checkpointId: checkpoint.id,
|
||||
feedback: feedback.trim() || null,
|
||||
});
|
||||
onDismiss(goal.id);
|
||||
} catch (err) {
|
||||
setLocalError(err instanceof Error ? err.message : String(err));
|
||||
await onRefresh();
|
||||
} finally {
|
||||
setAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
setLocalError(null);
|
||||
setAction('cancel');
|
||||
try {
|
||||
await cancelGoalRun(goal.id);
|
||||
onDismiss(goal.id);
|
||||
} catch (err) {
|
||||
setLocalError(err instanceof Error ? err.message : String(err));
|
||||
await onRefresh();
|
||||
} finally {
|
||||
setAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const cardBusy = busy || action != null;
|
||||
|
||||
return (
|
||||
<div className="banner banner-info goal-run-awaiting-banner">
|
||||
<div className="goal-run-awaiting-banner-body">
|
||||
<strong>{goal.title}</strong>
|
||||
<span>
|
||||
阶段「{checkpoint.title}」已完成,请确认后继续下一阶段。
|
||||
</span>
|
||||
{summary ? <span className="goal-run-awaiting-summary">{summary}</span> : null}
|
||||
<textarea
|
||||
className="goal-run-awaiting-feedback"
|
||||
rows={2}
|
||||
placeholder="可选:补充反馈或修改方向"
|
||||
value={feedback}
|
||||
disabled={cardBusy}
|
||||
onChange={(event) => setFeedback(event.target.value)}
|
||||
/>
|
||||
{localError ? <span className="goal-run-awaiting-error">{localError}</span> : null}
|
||||
</div>
|
||||
<div className="goal-run-awaiting-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="banner-action"
|
||||
disabled={cardBusy}
|
||||
onClick={() => void handleContinue()}
|
||||
>
|
||||
{action === 'continue' ? '处理中…' : '确认并继续'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="banner-dismiss"
|
||||
disabled={cardBusy}
|
||||
onClick={() => void handleCancel()}
|
||||
>
|
||||
{action === 'cancel' ? '取消中…' : '取消目标'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GoalRunAwaitingBanner({
|
||||
items,
|
||||
busy,
|
||||
onContinue,
|
||||
onDismiss,
|
||||
onRefresh,
|
||||
}: GoalRunAwaitingBannerProps) {
|
||||
if (!items.length) return null;
|
||||
|
||||
return (
|
||||
<div className="goal-run-awaiting-stack">
|
||||
{items.map((item) => (
|
||||
<GoalRunAwaitingCard
|
||||
key={item.goal.id}
|
||||
item={item}
|
||||
busy={busy}
|
||||
onContinue={onContinue}
|
||||
onDismiss={onDismiss}
|
||||
onRefresh={onRefresh}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,19 +12,21 @@ export function ChatProvider({
|
||||
user,
|
||||
capabilities,
|
||||
grantedSkills,
|
||||
goalRunEnabled,
|
||||
onUserUpdate,
|
||||
children,
|
||||
}: {
|
||||
user?: PortalUser | null;
|
||||
capabilities?: CapabilityMap | null;
|
||||
grantedSkills?: string[];
|
||||
goalRunEnabled?: boolean;
|
||||
onUserUpdate?: (user: PortalUser) => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const chat = useTKMindChat(user, onUserUpdate, capabilities, grantedSkills);
|
||||
|
||||
return (
|
||||
<ChatContext.Provider value={chat}>
|
||||
<ChatContext.Provider value={{ ...chat, goalRunEnabled }}>
|
||||
{children}
|
||||
{typeof chat.balanceCents === 'number' && chat.rechargePrompt && (
|
||||
<RechargeModal
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
isGoalRunApiAvailableError,
|
||||
listAwaitingGoalRuns,
|
||||
listGoalRuns,
|
||||
type GoalRunAwaitingItem,
|
||||
} from '../api/goalRun';
|
||||
import type { ChatState } from '../types';
|
||||
|
||||
export function useGoalRunAwaiting({
|
||||
userId,
|
||||
sessionId,
|
||||
chatState,
|
||||
featureEnabled,
|
||||
}: {
|
||||
userId?: string | null;
|
||||
sessionId?: string | null;
|
||||
chatState: ChatState;
|
||||
featureEnabled?: boolean;
|
||||
}) {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [items, setItems] = useState<GoalRunAwaitingItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const unavailableRef = useRef(featureEnabled === false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!userId || unavailableRef.current) return;
|
||||
if (featureEnabled === false) {
|
||||
unavailableRef.current = true;
|
||||
setEnabled(false);
|
||||
setItems([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const goals = await listGoalRuns({
|
||||
statuses: ['awaiting_user'],
|
||||
limit: 20,
|
||||
});
|
||||
setEnabled(true);
|
||||
const awaiting = listAwaitingGoalRuns(goals);
|
||||
if (sessionId) {
|
||||
awaiting.sort((left, right) => {
|
||||
const leftMatch = left.goal.sourceSessionId === sessionId ? 1 : 0;
|
||||
const rightMatch = right.goal.sourceSessionId === sessionId ? 1 : 0;
|
||||
return rightMatch - leftMatch;
|
||||
});
|
||||
}
|
||||
setItems(awaiting);
|
||||
} catch (err) {
|
||||
if (isGoalRunApiAvailableError(err)) {
|
||||
unavailableRef.current = true;
|
||||
setEnabled(false);
|
||||
setItems([]);
|
||||
return;
|
||||
}
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [featureEnabled, sessionId, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
unavailableRef.current = featureEnabled === false;
|
||||
if (!userId || featureEnabled === false) {
|
||||
setEnabled(false);
|
||||
setItems([]);
|
||||
return;
|
||||
}
|
||||
void refresh();
|
||||
}, [featureEnabled, refresh, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId || unavailableRef.current) return;
|
||||
if (chatState === 'idle' || chatState === 'error') {
|
||||
void refresh();
|
||||
}
|
||||
}, [chatState, refresh, userId]);
|
||||
|
||||
const dismissItem = useCallback((goalRunId: string) => {
|
||||
setItems((prev) => prev.filter((item) => item.goal.id !== goalRunId));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
enabled,
|
||||
items,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
dismissItem,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { AgentRun } from '../api/client';
|
||||
import { approveGoalCheckpoint } from '../api/goalRun';
|
||||
import type { ChatState } from '../types';
|
||||
import { useGoalRunAwaiting } from './useGoalRunAwaiting';
|
||||
|
||||
export function useGoalRunBanner({
|
||||
userId,
|
||||
sessionId,
|
||||
chatState,
|
||||
submit,
|
||||
followAgentRun,
|
||||
featureEnabled,
|
||||
}: {
|
||||
userId?: string | null;
|
||||
sessionId?: string | null;
|
||||
chatState: ChatState;
|
||||
submit: (
|
||||
text: string,
|
||||
options?: { goalRunId?: string },
|
||||
) => Promise<void>;
|
||||
followAgentRun?: (run: AgentRun) => Promise<void>;
|
||||
featureEnabled?: boolean;
|
||||
}) {
|
||||
const awaiting = useGoalRunAwaiting({ userId, sessionId, chatState, featureEnabled });
|
||||
|
||||
const handleContinue = useCallback(
|
||||
async ({
|
||||
goalRunId,
|
||||
checkpointId,
|
||||
feedback,
|
||||
}: {
|
||||
goalRunId: string;
|
||||
checkpointId: string;
|
||||
feedback?: string | null;
|
||||
}) => {
|
||||
const result = await approveGoalCheckpoint(
|
||||
goalRunId,
|
||||
checkpointId,
|
||||
feedback,
|
||||
sessionId,
|
||||
);
|
||||
if (result.run && followAgentRun) {
|
||||
await followAgentRun(result.run);
|
||||
return;
|
||||
}
|
||||
const continueText = feedback?.trim()
|
||||
? `继续下一阶段。补充说明:${feedback.trim()}`
|
||||
: '继续执行下一阶段';
|
||||
await submit(continueText, { goalRunId });
|
||||
},
|
||||
[followAgentRun, sessionId, submit],
|
||||
);
|
||||
|
||||
const bannerBusy =
|
||||
chatState === 'waiting' ||
|
||||
chatState === 'streaming' ||
|
||||
chatState === 'loading' ||
|
||||
chatState === 'connecting';
|
||||
|
||||
return {
|
||||
...awaiting,
|
||||
handleContinue,
|
||||
bannerBusy,
|
||||
};
|
||||
}
|
||||
@@ -1041,7 +1041,7 @@ export function useTKMindChat(
|
||||
return;
|
||||
}
|
||||
},
|
||||
[clearActiveRequestMissingTimer, rememberRecentContext, syncSessionMessages],
|
||||
[canUseLongTermMemory, clearActiveRequestMissingTimer, rememberRecentContext, syncSessionMessages],
|
||||
);
|
||||
|
||||
const subscribeToSession = useCallback(
|
||||
@@ -1473,6 +1473,7 @@ export function useTKMindChat(
|
||||
imageGenerationMode?: ImageGenerationMode;
|
||||
selectedChatSkill?: string;
|
||||
fileAttachments?: ChatFileAttachment[];
|
||||
goalRunId?: string;
|
||||
},
|
||||
imageUrls?: string[],
|
||||
previewImageUrls?: string[],
|
||||
@@ -1571,6 +1572,7 @@ export function useTKMindChat(
|
||||
.filter(Boolean),
|
||||
}
|
||||
: {}),
|
||||
...(options?.goalRunId ? { goalRunId: options.goalRunId } : {}),
|
||||
},
|
||||
);
|
||||
const runSessionId = createdRun.sessionId ?? activeSessionId;
|
||||
@@ -1774,6 +1776,87 @@ export function useTKMindChat(
|
||||
[session, pendingTool],
|
||||
);
|
||||
|
||||
const followAgentRun = useCallback(
|
||||
async (createdRun: AgentRun) => {
|
||||
if (
|
||||
chatStateRef.current === 'streaming' ||
|
||||
chatStateRef.current === 'loading' ||
|
||||
chatStateRef.current === 'connecting' ||
|
||||
chatStateRef.current === 'waiting'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const submitToken = connectTokenRef.current;
|
||||
let activeSessionId = sessionRef.current?.id ?? createdRun.sessionId ?? null;
|
||||
if (createdRun.requestId) {
|
||||
activeRequestId.current = createdRun.requestId;
|
||||
}
|
||||
setChatState('waiting');
|
||||
chatStateRef.current = 'waiting';
|
||||
setError(null);
|
||||
agentRunPendingRef.current = true;
|
||||
|
||||
try {
|
||||
const finishedRun =
|
||||
createdRun.status === 'succeeded'
|
||||
? createdRun
|
||||
: await waitForAgentRunWithDirectChatPreview(createdRun.id, {
|
||||
isCancelled: () => submitToken !== connectTokenRef.current,
|
||||
onSessionId: (sessionId) => {
|
||||
if (submitToken !== connectTokenRef.current) return;
|
||||
if (activeSessionId !== sessionId) {
|
||||
activeSessionId = sessionId;
|
||||
const nextSession: Session = {
|
||||
id: sessionId,
|
||||
name: 'New Chat',
|
||||
message_count: messagesRef.current.length,
|
||||
working_dir: '',
|
||||
};
|
||||
writeStoredSessionId(userRef.current?.id, sessionId);
|
||||
setSession(nextSession);
|
||||
setSessions((prev) => prependUnique(prev, nextSession));
|
||||
}
|
||||
if (!isDirectChatSessionId(sessionId)) {
|
||||
subscribeToSession(sessionId);
|
||||
if (shouldPromoteSessionIdToStreaming(chatStateRef.current)) {
|
||||
setChatState('streaming');
|
||||
}
|
||||
}
|
||||
},
|
||||
onMessages: (snapshotMessages) => {
|
||||
if (submitToken !== connectTokenRef.current) return;
|
||||
messagesRef.current = mergeConversationSnapshot(
|
||||
messagesRef.current,
|
||||
snapshotMessages,
|
||||
);
|
||||
messageHistoryLoadedCountRef.current = messagesRef.current.length;
|
||||
setMessages(messagesRef.current);
|
||||
},
|
||||
});
|
||||
if (submitToken !== connectTokenRef.current) return;
|
||||
agentRunPendingRef.current = false;
|
||||
activeSessionId = finishedRun.sessionId ?? activeSessionId;
|
||||
if (!activeSessionId) {
|
||||
clearActiveRequestMissingTimer();
|
||||
activeRequestId.current = null;
|
||||
setChatState('idle');
|
||||
return;
|
||||
}
|
||||
subscribeToSession(activeSessionId);
|
||||
setChatState('streaming');
|
||||
scheduleReplyRecoverySync(activeSessionId, submitToken);
|
||||
} catch (err) {
|
||||
agentRunPendingRef.current = false;
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
setChatState('error');
|
||||
clearActiveRequestMissingTimer();
|
||||
activeRequestId.current = null;
|
||||
}
|
||||
},
|
||||
[clearActiveRequestMissingTimer, scheduleReplyRecoverySync, subscribeToSession],
|
||||
);
|
||||
|
||||
const newSession = useCallback(async () => {
|
||||
const token = ++connectTokenRef.current;
|
||||
const previousSession = sessionRef.current;
|
||||
@@ -1974,6 +2057,7 @@ export function useTKMindChat(
|
||||
grantedSkills,
|
||||
submit,
|
||||
stop,
|
||||
followAgentRun,
|
||||
approveTool,
|
||||
newSession,
|
||||
rememberCurrentContext,
|
||||
|
||||
@@ -181,6 +181,40 @@ export type UserMemorySyncResponse = {
|
||||
syncedToSession: boolean;
|
||||
};
|
||||
|
||||
export type GoalRunCheckpoint = {
|
||||
id: string;
|
||||
goalRunId: string;
|
||||
sequence: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
status: string;
|
||||
agentRunId: string | null;
|
||||
outputSummary: string | null;
|
||||
userFeedback: string | null;
|
||||
approvedAt: number | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
startedAt: number | null;
|
||||
completedAt: number | null;
|
||||
};
|
||||
|
||||
export type GoalRun = {
|
||||
id: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
intentSummary: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
sourceChannel: string;
|
||||
sourceSessionId: string | null;
|
||||
sourceMessageId: string | null;
|
||||
currentCheckpointId: string | null;
|
||||
checkpoints: GoalRunCheckpoint[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
completedAt: number | null;
|
||||
};
|
||||
|
||||
export type ChatState = 'idle' | 'loading' | 'connecting' | 'streaming' | 'waiting' | 'error';
|
||||
|
||||
export type ToolConfirmation = {
|
||||
@@ -913,6 +947,10 @@ export type AgentCodeRunClientPolicy = {
|
||||
};
|
||||
};
|
||||
|
||||
export type GoalRunClientPolicy = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type AuthStatus = {
|
||||
authenticated: boolean;
|
||||
mode?: 'user' | 'legacy' | 'none' | 'unavailable';
|
||||
@@ -922,6 +960,7 @@ export type AuthStatus = {
|
||||
grantedSkills?: string[];
|
||||
unrestricted?: boolean;
|
||||
agentCodeRun?: AgentCodeRunClientPolicy | null;
|
||||
goalRun?: GoalRunClientPolicy | null;
|
||||
};
|
||||
|
||||
export type PathGrant = {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type AgentRunCreateOptions = {
|
||||
validation?: AgentRunValidation | null;
|
||||
validationInstruction?: string | null;
|
||||
selectedAssetIds?: string[];
|
||||
goalRunId?: string | null;
|
||||
};
|
||||
|
||||
export type AgentRunValidationFile = {
|
||||
|
||||
Reference in New Issue
Block a user