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

- 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:
iven
2026-04-06 22:30:16 +08:00
parent bbbcd7725b
commit 02c69bb3cf
13 changed files with 37 additions and 66 deletions

View File

@@ -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>,

View File

@@ -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()),

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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 */}

View File

@@ -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');

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,