feat(client): project plan and task agents

This commit is contained in:
baiyanyun
2026-06-08 11:57:47 +08:00
parent 710bf72353
commit ff25c2fef7
7 changed files with 315 additions and 130 deletions

View File

@@ -11,7 +11,9 @@ import type {
DigitalEmployeeDecision,
DigitalEmployeeDuty,
DigitalEmployeeManagedService,
DigitalEmployeePlanAgentState,
DigitalEmployeeRunDetail,
DigitalEmployeeTaskAgentState,
DigitalEmployeeTaskExecutionSummary,
DigitalEmployeeWorkEvent,
DigitalEmployeeWorkdayActionRequest,
@@ -829,6 +831,63 @@ function planRecordStatus(plan: QimingclawPlanRecord, tasks: QimingclawTaskRecor
return plan.status || 'pending';
}
function buildPlanAgents(snapshot: QimingclawSnapshot | null): DigitalEmployeePlanAgentState[] {
return runtimePlans(snapshot).map((plan) => {
const tasks = runtimePlanTasks(snapshot, plan.id);
const status = planRecordStatus(plan, tasks);
const tone = planStatusTone(status);
const failedTaskCount = tasks.filter((task) => taskStatusTone(task.status) === 'danger').length;
const runningTaskCount = tasks.filter((task) => taskStatusTone(task.status) === 'running').length;
return {
agent_id: `plan-agent-${slugifyIdPart(plan.id)}`,
plan_id: plan.id,
title: plan.title,
lifecycle: status,
status_label: planStatusLabel(status),
status_tone: tone,
managed_task_count: tasks.length,
running_task_count: runningTaskCount,
failed_task_count: failedTaskCount,
next_action: failedTaskCount > 0 ? 'review_failures' : runningTaskCount > 0 ? 'monitor_runs' : tone === 'success' ? 'archive_results' : 'activate_plan',
can_replan: failedTaskCount > 0,
can_pause: runningTaskCount > 0,
can_resume: status === 'paused',
updated_at: latestRuntimeTime(plan, tasks),
};
});
}
function buildTaskAgents(snapshot: QimingclawSnapshot | null): DigitalEmployeeTaskAgentState[] {
const runs = runtimeRuns(snapshot);
return runtimeTasks(snapshot).map((task) => {
const latestRun = latestRunForTask(runs, task.id);
const tone = taskStatusTone(task.status);
return {
agent_id: `task-agent-${slugifyIdPart(task.id)}`,
task_id: task.id,
plan_id: task.planId ?? null,
title: task.title,
execution_state: task.status,
status_label: taskStatusLabel(task.status),
status_tone: tone,
assigned_skill_id: task.assignedSkillId ?? null,
latest_run_id: latestRun?.id ?? null,
can_retry: tone === 'danger',
can_skip: tone === 'danger' || tone === 'waiting',
can_cancel: tone === 'running',
next_action: tone === 'danger' ? 'retry_or_skip' : tone === 'running' ? 'watch_progress' : tone === 'success' ? 'collect_artifacts' : 'start_run',
updated_at: latestRun?.updatedAt ?? task.updatedAt,
};
});
}
function latestRunForTask(runs: QimingclawRunRecord[], taskId: string): QimingclawRunRecord | null {
const sorted = runs
.filter((run) => run.taskId === taskId)
.sort((left, right) => (right.updatedAt || right.createdAt).localeCompare(left.updatedAt || left.createdAt));
return sorted[0] ?? null;
}
function runtimePlanToDebugPlan(plan: QimingclawPlanRecord, snapshot: QimingclawSnapshot | null): DebugPlan {
const tasks = runtimePlanTasks(snapshot, plan.id);
const status = planRecordStatus(plan, tasks);
@@ -1800,6 +1859,9 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
const managedServices = buildManagedServices(snapshot);
const managedServiceIssues = managedServices.filter((service) => service.requires_human_action || service.status === 'degraded');
const runningManagedServiceCount = managedServices.filter((service) => service.running).length;
const planAgents = buildPlanAgents(snapshot);
const taskAgents = buildTaskAgents(snapshot);
const actionableTaskAgents = taskAgents.filter((agent) => agent.can_retry || agent.can_skip || agent.can_cancel);
return {
source: 'live',
@@ -1852,9 +1914,14 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
{ label: '同步异常', value: String(sync?.failed ?? 0) },
{ label: '服务运行', value: `${runningManagedServiceCount}/${managedServices.length}` },
{ label: '需处理服务', value: String(managedServiceIssues.length) },
{ label: 'Plan Agent', value: String(planAgents.length) },
{ label: 'Task Agent', value: String(taskAgents.length) },
{ label: '可治理任务', value: String(actionableTaskAgents.length) },
],
},
managed_services: managedServices,
plan_agents: planAgents,
task_agents: taskAgents,
duties,
events,
execution_summaries: executionSummaries,
@@ -1875,6 +1942,10 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
serviceLine(snapshot, 'computerServer', 'Computer Server'),
managedServiceSummary(managedServices),
...managedServiceIssues.slice(0, 3).map(managedServiceLine),
`Plan Agent${planAgents.length} 个计划代理Task Agent${taskAgents.length} 个任务代理。`,
actionableTaskAgents[0]
? `待治理任务:${actionableTaskAgents[0].title},下一步 ${actionableTaskAgents[0].next_action}`
: 'Task Agent暂无需要重试、跳过或取消的任务。',
syncLine(snapshot),
`活跃会话:${snapshot?.sessions.active ?? 0} / ${snapshot?.sessions.total ?? 0}`,
],
@@ -2054,6 +2125,9 @@ async function handleGovernanceCommand(
const planId = stringFromUnknown(body.context_refs?.plan_id)
|| stringFromUnknown(body.payload?.plan_id)
|| stringFromUnknown(body.payload?.original_plan_id);
const taskId = stringFromUnknown(body.context_refs?.task_id)
|| stringFromUnknown(body.payload?.task_id)
|| stringFromUnknown(body.payload?.original_task_id);
const scheduleId = stringFromUnknown(body.context_refs?.schedule_id)
|| stringFromUnknown(body.payload?.schedule_id);
@@ -2108,6 +2182,20 @@ async function handleGovernanceCommand(
return response;
}
if (['retry_task', 'skip_task', 'pause_task', 'resume_task', 'provide_task_input', 'cancel_task', 'start_run', 'cancel_run'].includes(body.command_kind)) {
const targetTask = taskId || runtimeTasks(snapshot)[0]?.id || 'qimingclaw-task-agent';
const response = governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},并记录到 qimingclaw Task Agent 控制平面。`, {
plan_id: planId || runtimeTasks(snapshot).find((task) => task.id === targetTask)?.planId || 'qimingclaw-task-agent-plan',
task_id: targetTask,
task_agent_id: `task-agent-${slugifyIdPart(targetTask)}`,
status: taskGovernanceStatus(body.command_kind),
next_action: taskGovernanceNextAction(body.command_kind),
source: 'qimingclaw-task-agent',
});
await recordGovernanceCommand(body, response);
return response;
}
const response = {
command_id: commandId,
accepted: false,
@@ -2134,6 +2222,46 @@ function governanceAccepted(
};
}
function taskGovernanceStatus(commandKind: string): string {
switch (commandKind) {
case 'retry_task':
case 'start_run':
case 'resume_task':
return 'running';
case 'skip_task':
return 'skipped';
case 'cancel_task':
case 'cancel_run':
return 'cancelled';
case 'pause_task':
return 'paused';
default:
return 'accepted';
}
}
function taskGovernanceNextAction(commandKind: string): string {
switch (commandKind) {
case 'retry_task':
return 'start_retry_run';
case 'skip_task':
return 'continue_next_task';
case 'pause_task':
return 'await_resume';
case 'resume_task':
return 'watch_progress';
case 'provide_task_input':
return 'resume_with_input';
case 'cancel_task':
case 'cancel_run':
return 'close_execution';
case 'start_run':
return 'watch_progress';
default:
return 'record_control_event';
}
}
function stringFromUnknown(value: unknown): string {
return typeof value === 'string' && value.trim() ? value.trim() : '';
}