package vip.mate.team.service; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import vip.mate.team.model.AgentTeamEntity; import vip.mate.team.model.TeamRunCreateCommand; import vip.mate.team.model.TeamRunEntity; import vip.mate.team.model.TeamRunStatus; import vip.mate.team.model.TeamRunView; import vip.mate.team.model.TeamTaskEntity; import vip.mate.team.repository.TeamRunMapper; import vip.mate.team.repository.TeamTaskMapper; import java.time.LocalDateTime; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** Owns team run creation, lifecycle transitions, authorization, and reads. */ @Service public class TeamRunService { public record SealResult(TeamRunEntity run, boolean transitioned) { } public record CancelResult(TeamRunEntity run, boolean transitioned) { } private static final int MAX_TITLE_LENGTH = 255; private static final Set FINAL_OUTCOMES = Set.of( TeamRunStatus.COMPLETED, TeamRunStatus.PARTIAL, TeamRunStatus.FAILED); private final TeamRunMapper runMapper; private final TeamTaskMapper taskMapper; private final TeamService teamService; private final TeamRunStateMachine stateMachine; public TeamRunService(TeamRunMapper runMapper, TeamTaskMapper taskMapper, TeamService teamService) { this.runMapper = runMapper; this.taskMapper = taskMapper; this.teamService = teamService; this.stateMachine = new TeamRunStateMachine(); } public TeamRunEntity startRun(TeamRunCreateCommand command) { validateCreate(command); TeamRunEntity existing = findByOrigin(command.getWorkspaceId(), command.getLeadConversationId(), command.getOriginMessageId()); if (existing != null) { return existing; } TeamRunEntity run = new TeamRunEntity(); run.setTeamId(command.getTeamId()); run.setWorkspaceId(command.getWorkspaceId()); run.setLeadAgentId(command.getLeadAgentId()); run.setLeadConversationId(command.getLeadConversationId()); run.setOriginMessageId(command.getOriginMessageId()); run.setTitle(deriveTitle(command)); run.setObjective(command.getObjective().trim()); run.setStatus(TeamRunStatus.PLANNING); run.setMetadata(command.getMetadata()); try { runMapper.insert(run); return run; } catch (DuplicateKeyException duplicate) { TeamRunEntity winner = findByOrigin(command.getWorkspaceId(), command.getLeadConversationId(), command.getOriginMessageId()); if (winner != null) { return winner; } throw duplicate; } } public TeamRunEntity requireRun(Long runId, Long workspaceId) { TeamRunEntity run = runMapper.selectById(runId); if (run == null || workspaceId == null || !workspaceId.equals(run.getWorkspaceId())) { throw new IllegalArgumentException("team run not found in workspace: " + runId); } return run; } public Set findPlanningRunIds(Collection runIds) { if (runIds == null || runIds.isEmpty()) { return Set.of(); } return runMapper.selectBatchIds(runIds).stream() .filter(run -> TeamRunStatus.PLANNING.equals(run.getStatus())) .map(TeamRunEntity::getId) .collect(Collectors.toSet()); } public TeamRunView getRun(Long runId, Long workspaceId) { return buildView(requireRun(runId, workspaceId)); } public List listTeamRuns(Long teamId, Long workspaceId) { return runMapper.selectList(Wrappers.lambdaQuery() .eq(TeamRunEntity::getTeamId, teamId) .eq(TeamRunEntity::getWorkspaceId, workspaceId) .orderByDesc(TeamRunEntity::getCreateTime)) .stream().map(this::buildView).toList(); } public List listConversationRuns(String conversationId, Long workspaceId) { return runMapper.selectList(Wrappers.lambdaQuery() .eq(TeamRunEntity::getLeadConversationId, conversationId) .eq(TeamRunEntity::getWorkspaceId, workspaceId) .orderByDesc(TeamRunEntity::getCreateTime)) .stream().map(this::buildView).toList(); } @Transactional public TeamRunEntity sealRun(Long runId, Long workspaceId) { return sealRunWithResult(runId, workspaceId).run(); } @Transactional public SealResult sealRunWithResult(Long runId, Long workspaceId) { TeamRunEntity run = requireRun(runId, workspaceId); if (!TeamRunStatus.PLANNING.equals(run.getStatus())) { return new SealResult(run, false); } long taskCount = taskMapper.selectCount(Wrappers.lambdaQuery() .eq(TeamTaskEntity::getRunId, runId)); if (taskCount == 0) { throw new IllegalStateException("cannot seal a team run without tasks"); } LocalDateTime startedAt = LocalDateTime.now(); int changed = runMapper.update(null, Wrappers.lambdaUpdate() .eq(TeamRunEntity::getId, runId) .eq(TeamRunEntity::getStatus, TeamRunStatus.PLANNING) .set(TeamRunEntity::getStatus, TeamRunStatus.RUNNING) .set(TeamRunEntity::getStartedAt, startedAt)); if (changed == 1) { run.setStatus(TeamRunStatus.RUNNING); run.setStartedAt(startedAt); return new SealResult(run, true); } TeamRunEntity current = requireRun(runId, workspaceId); if (!TeamRunStatus.PLANNING.equals(current.getStatus())) { return new SealResult(current, false); } throw new IllegalStateException("failed to seal team run: " + runId); } @Transactional public TeamRunEntity markFinalized(Long runId, Long workspaceId, String finalSummary) { TeamRunEntity run = requireRun(runId, workspaceId); if (TeamRunStatus.isTerminal(run.getStatus())) { return run; } if (!TeamRunStatus.FINALIZING.equals(run.getStatus())) { throw new IllegalStateException("team run is not finalizing: " + runId); } String outcome = metadata(run.getMetadata()).getStr("projectedOutcome"); if (!FINAL_OUTCOMES.contains(outcome)) { throw new IllegalStateException("team run has no valid projected outcome: " + runId); } LocalDateTime completedAt = LocalDateTime.now(); int changed = runMapper.update(null, Wrappers.lambdaUpdate() .eq(TeamRunEntity::getId, runId) .eq(TeamRunEntity::getStatus, TeamRunStatus.FINALIZING) .set(TeamRunEntity::getStatus, outcome) .set(TeamRunEntity::getFinalSummary, finalSummary) .set(TeamRunEntity::getCompletedAt, completedAt)); if (changed == 1) { run.setStatus(outcome); run.setFinalSummary(finalSummary); run.setCompletedAt(completedAt); return run; } TeamRunEntity current = requireRun(runId, workspaceId); if (TeamRunStatus.isTerminal(current.getStatus())) { return current; } throw new IllegalStateException("failed to finalize team run: " + runId); } @Transactional public TeamRunEntity cancelRun(Long runId, Long workspaceId, String reason) { return cancelRunWithResult(runId, workspaceId, reason).run(); } @Transactional public CancelResult cancelRunWithResult(Long runId, Long workspaceId, String reason) { TeamRunEntity run = requireRun(runId, workspaceId); if (TeamRunStatus.isTerminal(run.getStatus())) { return new CancelResult(run, false); } LocalDateTime completedAt = LocalDateTime.now(); int changed = runMapper.update(null, Wrappers.lambdaUpdate() .eq(TeamRunEntity::getId, runId) .notIn(TeamRunEntity::getStatus, TeamRunStatus.TERMINAL) .set(TeamRunEntity::getStatus, TeamRunStatus.CANCELLED) .set(TeamRunEntity::getStopReason, reason) .set(TeamRunEntity::getCompletedAt, completedAt)); if (changed == 1) { run.setStatus(TeamRunStatus.CANCELLED); run.setStopReason(reason); run.setCompletedAt(completedAt); return new CancelResult(run, true); } return new CancelResult(requireRun(runId, workspaceId), false); } public TeamRunView buildView(TeamRunEntity run) { List tasks = tasksForRun(run.getId()); TeamRunStateMachine.Projection projection = stateMachine.project(run, tasks); return new TeamRunView(run.getId(), run.getTeamId(), run.getWorkspaceId(), run.getLeadAgentId(), run.getLeadConversationId(), run.getOriginMessageId(), run.getTitle(), run.getObjective(), projection.status(), run.getFinalSummary(), run.getStopReason(), run.getMetadata(), run.getStartedAt(), run.getCompletedAt(), run.getCreateTime(), run.getUpdateTime(), projection.progress(), tasks.stream().map(TeamRunView.Task::from).toList()); } private List tasksForRun(Long runId) { return taskMapper.selectList(Wrappers.lambdaQuery() .eq(TeamTaskEntity::getRunId, runId) .orderByAsc(TeamTaskEntity::getTaskNumber)); } private void validateCreate(TeamRunCreateCommand command) { if (command == null || command.getTeamId() == null || command.getWorkspaceId() == null || command.getLeadAgentId() == null) { throw new IllegalArgumentException("team, workspace, and lead are required"); } AgentTeamEntity team = teamService.getTeam(command.getTeamId()); if (team == null || !TeamService.STATUS_ACTIVE.equals(team.getStatus())) { throw new IllegalArgumentException("team not found or not active: " + command.getTeamId()); } if (!command.getWorkspaceId().equals(team.getWorkspaceId())) { throw new IllegalArgumentException("team is not in workspace: " + command.getWorkspaceId()); } if (!command.getLeadAgentId().equals(team.getLeadAgentId())) { throw new IllegalArgumentException("agent is not the team lead: " + command.getLeadAgentId()); } if (command.getLeadConversationId() == null || command.getLeadConversationId().isBlank()) { throw new IllegalArgumentException("lead conversation is required"); } if (command.getObjective() == null || command.getObjective().isBlank()) { throw new IllegalArgumentException("objective is required"); } } private TeamRunEntity findByOrigin(Long workspaceId, String conversationId, Long originMessageId) { if (originMessageId == null) { return null; } return runMapper.selectOne(Wrappers.lambdaQuery() .eq(TeamRunEntity::getWorkspaceId, workspaceId) .eq(TeamRunEntity::getLeadConversationId, conversationId) .eq(TeamRunEntity::getOriginMessageId, originMessageId)); } private String deriveTitle(TeamRunCreateCommand command) { String title = command.getTitle() == null || command.getTitle().isBlank() ? command.getObjective().trim() : command.getTitle().trim(); return title.length() <= MAX_TITLE_LENGTH ? title : title.substring(0, MAX_TITLE_LENGTH); } private JSONObject metadata(String value) { if (value == null || value.isBlank()) { return new JSONObject(); } try { return JSONUtil.parseObj(value); } catch (RuntimeException invalidJson) { return new JSONObject(); } } }