fix: subagent unique ID matching + AgentState serialization + pre-existing TS errors
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- S-3: Thread task_id (UUID) through all 6 layers (LoopEvent → StreamChatEvent → kernel-types → gateway-client → streamStore) so subtasks are matched by ID, not description string
- AgentState: Add #[serde(rename_all = "lowercase")] to fix PascalCase serialization ("Running" → "running"), update frontend matcher
- S-1: Remove unused onClose prop from ArtifactPanel + ChatArea call site
- Fix hooks/index.ts: remove orphaned useAutomationEvents re-exports (module deleted)
- Fix types/index.ts: remove orphaned automation type/value re-exports (module deleted)
- Fix ChatArea.tsx: framer-motion 12 + React 19 type compat — use createElement + explicit any return type to avoid unknown-in-JSX-child error
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1074,6 +1074,7 @@ pub enum LoopEvent {
|
||||
ToolEnd { name: String, output: serde_json::Value },
|
||||
/// Sub-agent task status update (started/running/completed/failed)
|
||||
SubtaskStatus {
|
||||
task_id: String,
|
||||
description: String,
|
||||
status: String,
|
||||
detail: Option<String>,
|
||||
|
||||
@@ -107,17 +107,19 @@ impl Tool for TaskTool {
|
||||
);
|
||||
|
||||
// Emit subtask_started event
|
||||
// Create a sub-agent with its own ID
|
||||
let sub_agent_id = AgentId::new();
|
||||
let task_id = sub_agent_id.to_string();
|
||||
|
||||
if let Some(ref tx) = context.event_sender {
|
||||
let _ = tx.send(LoopEvent::SubtaskStatus {
|
||||
task_id: task_id.clone(),
|
||||
description: description.to_string(),
|
||||
status: "started".to_string(),
|
||||
detail: None,
|
||||
}).await;
|
||||
}
|
||||
|
||||
// Create a sub-agent with its own ID
|
||||
let sub_agent_id = AgentId::new();
|
||||
|
||||
// Create a fresh session for the sub-agent
|
||||
let session_id = self.memory.create_session(&sub_agent_id).await?;
|
||||
|
||||
@@ -160,6 +162,7 @@ impl Tool for TaskTool {
|
||||
// Emit subtask_running event
|
||||
if let Some(ref tx) = context.event_sender {
|
||||
let _ = tx.send(LoopEvent::SubtaskStatus {
|
||||
task_id: task_id.clone(),
|
||||
description: description.to_string(),
|
||||
status: "running".to_string(),
|
||||
detail: Some("子Agent正在执行中...".to_string()),
|
||||
@@ -177,6 +180,7 @@ impl Tool for TaskTool {
|
||||
// Emit subtask_completed event
|
||||
if let Some(ref tx) = context.event_sender {
|
||||
let _ = tx.send(LoopEvent::SubtaskStatus {
|
||||
task_id: task_id.clone(),
|
||||
description: description.to_string(),
|
||||
status: "completed".to_string(),
|
||||
detail: Some(format!(
|
||||
@@ -201,6 +205,7 @@ impl Tool for TaskTool {
|
||||
// Emit subtask_failed event
|
||||
if let Some(ref tx) = context.event_sender {
|
||||
let _ = tx.send(LoopEvent::SubtaskStatus {
|
||||
task_id: task_id.clone(),
|
||||
description: description.to_string(),
|
||||
status: "failed".to_string(),
|
||||
detail: Some(e.to_string()),
|
||||
|
||||
@@ -128,6 +128,7 @@ impl AgentConfig {
|
||||
|
||||
/// Agent runtime state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AgentState {
|
||||
/// Agent is running and can receive messages
|
||||
Running,
|
||||
|
||||
@@ -49,7 +49,7 @@ pub enum StreamChatEvent {
|
||||
ThinkingDelta { delta: String },
|
||||
ToolStart { name: String, input: serde_json::Value },
|
||||
ToolEnd { name: String, output: serde_json::Value },
|
||||
SubtaskStatus { description: String, status: String, detail: Option<String> },
|
||||
SubtaskStatus { task_id: String, description: String, status: String, detail: Option<String> },
|
||||
IterationStart { iteration: usize, max_iterations: usize },
|
||||
HandStart { name: String, params: serde_json::Value },
|
||||
HandEnd { name: String, result: serde_json::Value },
|
||||
@@ -341,9 +341,10 @@ pub async fn agent_chat_stream(
|
||||
StreamChatEvent::ToolEnd { name: name.clone(), output: output.clone() }
|
||||
}
|
||||
}
|
||||
LoopEvent::SubtaskStatus { description, status, detail } => {
|
||||
tracing::debug!("[agent_chat_stream] SubtaskStatus: {} - {}", description, status);
|
||||
LoopEvent::SubtaskStatus { task_id, description, status, detail } => {
|
||||
tracing::debug!("[agent_chat_stream] SubtaskStatus: {} - {} (id={})", description, status, task_id);
|
||||
StreamChatEvent::SubtaskStatus {
|
||||
task_id: task_id.clone(),
|
||||
description: description.clone(),
|
||||
status: status.clone(),
|
||||
detail: detail.clone(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useCallback, useMemo, type MutableRefObject, type RefObject, type CSSProperties } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback, useMemo, type MutableRefObject, type RefObject, type CSSProperties, createElement } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { List, type ListImperativeAPI } from 'react-window';
|
||||
import { useChatStore, type Message } from '../store/chatStore';
|
||||
@@ -262,7 +262,6 @@ export function ChatArea() {
|
||||
artifacts={artifacts}
|
||||
selectedId={selectedArtifactId}
|
||||
onSelect={selectArtifact}
|
||||
onClose={() => setArtifactPanelOpen(false)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -609,18 +608,21 @@ function MessageBubble({ message, setInput }: { message: Message; setInput: (tex
|
||||
const isUser = message.role === 'user';
|
||||
const isThinking = message.streaming && !message.content;
|
||||
|
||||
// Extract typed arrays for JSX rendering (avoids TS2322 from && chain producing unknown)
|
||||
// Extract typed arrays for JSX rendering
|
||||
const toolCallSteps: ToolCallStep[] | undefined = message.toolSteps;
|
||||
const subtaskList: Subtask[] | undefined = message.subtasks;
|
||||
|
||||
const renderToolSteps = (): React.ReactNode => {
|
||||
// framer-motion 12 + React 19 type compat: motion.span/div return types resolve to `unknown`.
|
||||
// Use createElement to bypass JSX child-context type inference.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const renderToolChain = (): any => {
|
||||
if (isUser || !toolCallSteps || toolCallSteps.length === 0) return null;
|
||||
return <ToolCallChain steps={toolCallSteps} isStreaming={!!message.streaming} />;
|
||||
return createElement(ToolCallChain, { steps: toolCallSteps, isStreaming: !!message.streaming });
|
||||
};
|
||||
|
||||
const renderSubtasks = (): React.ReactNode => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const renderSubtasks = (): any => {
|
||||
if (isUser || !subtaskList || subtaskList.length === 0) return null;
|
||||
return <TaskProgress tasks={subtaskList} className="mb-3" />;
|
||||
return createElement(TaskProgress, { tasks: subtaskList, className: 'mb-3' });
|
||||
};
|
||||
|
||||
// Download message as Markdown file
|
||||
@@ -669,7 +671,7 @@ function MessageBubble({ message, setInput }: { message: Message; setInput: (tex
|
||||
/>
|
||||
)}
|
||||
{/* Tool call steps chain (DeerFlow-inspired) */}
|
||||
{renderToolSteps()}
|
||||
{renderToolChain()}
|
||||
{/* Subtask tracking (DeerFlow-inspired) */}
|
||||
{renderSubtasks()}
|
||||
{/* Message content with streaming support */}
|
||||
|
||||
@@ -28,7 +28,6 @@ interface ArtifactPanelProps {
|
||||
artifacts: ArtifactFile[];
|
||||
selectedId?: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -74,7 +73,6 @@ export function ArtifactPanel({
|
||||
artifacts,
|
||||
selectedId,
|
||||
onSelect,
|
||||
onClose: _onClose,
|
||||
className = '',
|
||||
}: ArtifactPanelProps) {
|
||||
const [viewMode, setViewMode] = useState<'preview' | 'code'>('preview');
|
||||
|
||||
@@ -4,16 +4,4 @@
|
||||
* @module hooks
|
||||
*/
|
||||
|
||||
export {
|
||||
useAutomationEvents,
|
||||
useHandEvents,
|
||||
useWorkflowEvents,
|
||||
} from './useAutomationEvents';
|
||||
|
||||
// Re-export types from useAutomationEvents
|
||||
export type {
|
||||
UseAutomationEventsOptions,
|
||||
} from './useAutomationEvents';
|
||||
|
||||
export { useOptimisticMessages } from './useOptimisticMessages';
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ export class GatewayClient {
|
||||
onThinkingDelta?: (delta: string) => void;
|
||||
onTool?: (tool: string, input: string, output: string) => void;
|
||||
onHand?: (name: string, status: string, result?: unknown) => void;
|
||||
onSubtaskStatus?: (description: string, status: string, detail?: string) => void;
|
||||
onSubtaskStatus?: (taskId: string, description: string, status: string, detail?: string) => void;
|
||||
onComplete: (inputTokens?: number, outputTokens?: number) => void;
|
||||
onError: (error: string) => void;
|
||||
}>();
|
||||
@@ -404,7 +404,7 @@ export class GatewayClient {
|
||||
const agents = await this.restGet<Array<{ id: string; name?: string; state?: string }>>('/api/agents');
|
||||
if (agents && agents.length > 0) {
|
||||
// Prefer agent with state "Running", otherwise use first agent
|
||||
const runningAgent = agents.find((a: { id: string; name?: string; state?: string }) => a.state === 'Running');
|
||||
const runningAgent = agents.find((a: { id: string; name?: string; state?: string }) => a.state === 'running');
|
||||
const defaultAgent = runningAgent || agents[0];
|
||||
this.defaultAgentId = defaultAgent.id;
|
||||
this.log('info', `Fetched default agent from /api/agents: ${this.defaultAgentId} (${defaultAgent.name || 'unnamed'})`);
|
||||
@@ -470,7 +470,7 @@ export class GatewayClient {
|
||||
onThinkingDelta?: (delta: string) => void;
|
||||
onTool?: (tool: string, input: string, output: string) => void;
|
||||
onHand?: (name: string, status: string, result?: unknown) => void;
|
||||
onSubtaskStatus?: (description: string, status: string, detail?: string) => void;
|
||||
onSubtaskStatus?: (taskId: string, description: string, status: string, detail?: string) => void;
|
||||
onComplete: (inputTokens?: number, outputTokens?: number) => void;
|
||||
onError: (error: string) => void;
|
||||
},
|
||||
@@ -652,7 +652,7 @@ export class GatewayClient {
|
||||
case 'subtask_status':
|
||||
// Sub-agent task status update
|
||||
if (callbacks.onSubtaskStatus && data.description) {
|
||||
callbacks.onSubtaskStatus(data.description, data.status || '', data.detail);
|
||||
callbacks.onSubtaskStatus(data.task_id || data.description, data.description, data.status || '', data.detail);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ export interface ZclawStreamEvent {
|
||||
agent_id?: string;
|
||||
agents?: Array<{ id: string; name: string; status: string }>;
|
||||
// Subtask status fields
|
||||
task_id?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
detail?: string;
|
||||
|
||||
@@ -147,9 +147,10 @@ export function installChatMethods(ClientClass: { prototype: KernelClient }): vo
|
||||
break;
|
||||
|
||||
case 'subtaskStatus':
|
||||
log.debug('Subtask status:', streamEvent.description, streamEvent.status, streamEvent.detail);
|
||||
log.debug('Subtask status:', streamEvent.taskId, streamEvent.description, streamEvent.status, streamEvent.detail);
|
||||
if (callbacks.onSubtaskStatus) {
|
||||
callbacks.onSubtaskStatus(
|
||||
streamEvent.taskId,
|
||||
streamEvent.description,
|
||||
streamEvent.status,
|
||||
streamEvent.detail ?? undefined
|
||||
|
||||
@@ -69,7 +69,7 @@ export interface StreamCallbacks {
|
||||
onThinkingDelta?: (delta: string) => void;
|
||||
onTool?: (tool: string, input: string, output: string) => void;
|
||||
onHand?: (name: string, status: string, result?: unknown) => void;
|
||||
onSubtaskStatus?: (description: string, status: string, detail?: string) => void;
|
||||
onSubtaskStatus?: (taskId: string, description: string, status: string, detail?: string) => void;
|
||||
onComplete: (inputTokens?: number, outputTokens?: number) => void;
|
||||
onError: (error: string) => void;
|
||||
}
|
||||
@@ -129,6 +129,7 @@ export interface StreamEventHandEnd {
|
||||
|
||||
export interface StreamEventSubtaskStatus {
|
||||
type: 'subtaskStatus';
|
||||
taskId: string;
|
||||
description: string;
|
||||
status: string;
|
||||
detail?: string;
|
||||
|
||||
@@ -382,7 +382,7 @@ export const useStreamStore = create<StreamState>()(
|
||||
}
|
||||
}
|
||||
},
|
||||
onSubtaskStatus: (description: string, status: string, detail?: string) => {
|
||||
onSubtaskStatus: (taskId: string, description: string, status: string, detail?: string) => {
|
||||
// Map backend status to frontend Subtask status
|
||||
const statusMap: Record<string, Subtask['status']> = {
|
||||
started: 'pending',
|
||||
@@ -396,12 +396,12 @@ export const useStreamStore = create<StreamState>()(
|
||||
msgs.map(m => {
|
||||
if (m.id !== assistantId) return m;
|
||||
const subtasks = [...(m.subtasks || [])];
|
||||
const existingIdx = subtasks.findIndex(st => st.description === description);
|
||||
const existingIdx = subtasks.findIndex(st => st.id === taskId);
|
||||
if (existingIdx >= 0) {
|
||||
subtasks[existingIdx] = { ...subtasks[existingIdx], status: mappedStatus, result: detail };
|
||||
} else {
|
||||
subtasks.push({
|
||||
id: `subtask_${Date.now()}_${generateRandomString(4)}`,
|
||||
id: taskId,
|
||||
description,
|
||||
status: mappedStatus,
|
||||
result: detail,
|
||||
|
||||
@@ -129,34 +129,6 @@ export {
|
||||
createPaginatedResponse,
|
||||
} from './api-responses';
|
||||
|
||||
// Automation Types
|
||||
export type {
|
||||
CategoryType,
|
||||
CategoryConfig,
|
||||
CategoryStats,
|
||||
AutomationStatus,
|
||||
AutomationType,
|
||||
RunInfo,
|
||||
ScheduleInfo,
|
||||
AutomationItem,
|
||||
} from './automation';
|
||||
|
||||
// Automation Constants and Functions
|
||||
export {
|
||||
HAND_CATEGORY_MAP,
|
||||
CATEGORY_CONFIGS,
|
||||
handStatusToAutomationStatus,
|
||||
workflowStatusToAutomationStatus,
|
||||
handToAutomationItem,
|
||||
workflowToAutomationItem,
|
||||
adaptToAutomationItems,
|
||||
calculateCategoryStats,
|
||||
filterByCategory,
|
||||
filterByType,
|
||||
filterByStatus,
|
||||
searchAutomationItems,
|
||||
} from './automation';
|
||||
|
||||
// Classroom Types
|
||||
export type {
|
||||
AgentProfile,
|
||||
|
||||
Reference in New Issue
Block a user