feat(agent,trigger): wire agent_lifecycle as a real event source

This commit is contained in:
matevip 2026-05-08 15:07:18 +08:00
parent dae74cf26c
commit 231724d6a9
3 changed files with 121 additions and 0 deletions

View File

@ -3,12 +3,15 @@ package vip.mate.agent;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import vip.mate.agent.context.ChatOrigin;
import vip.mate.agent.context.ChatOriginHolder;
import vip.mate.agent.event.AgentLifecycleEvent;
import vip.mate.agent.model.AgentEntity;
import vip.mate.agent.repository.AgentMapper;
import vip.mate.exception.MateClawException;
@ -43,6 +46,11 @@ public class AgentService {
private final MemoryLifecycleMediator lifecycleMediator;
private final MemoryProperties memoryProperties;
/** Field-injected publisher for agent_lifecycle trigger events; the
* trigger module's bridge listens and forwards into ingest. */
@Autowired(required = false)
private ApplicationEventPublisher events;
/** 运行时 Agent 实例缓存agentId -> BaseAgent */
private final Map<Long, BaseAgent> agentInstances = new ConcurrentHashMap<>();
@ -76,18 +84,51 @@ public class AgentService {
agent.setAgentType("react");
}
agentMapper.insert(agent);
publishLifecycle(agent, "spawned");
return agent;
}
public AgentEntity updateAgent(AgentEntity agent) {
// Detect enabled-flag flip so the lifecycle event reflects the
// intent rather than every metadata edit. Reading the prior row
// is cheap and gives us a clean diff source.
AgentEntity prior = agentMapper.selectById(agent.getId());
agentMapper.updateById(agent);
agentInstances.remove(agent.getId());
if (prior != null && prior.getEnabled() != null
&& !prior.getEnabled().equals(agent.getEnabled())) {
publishLifecycle(agent,
Boolean.TRUE.equals(agent.getEnabled()) ? "enabled" : "disabled");
}
return agent;
}
public void deleteAgent(Long id) {
AgentEntity prior = agentMapper.selectById(id);
agentMapper.deleteById(id);
agentInstances.remove(id);
if (prior != null) publishLifecycle(prior, "terminated");
}
/**
* Best-effort publish of an {@link AgentLifecycleEvent}. A publish
* failure must never roll back the agent CRUD that just succeeded
* the agent_lifecycle trigger surface is observability, not the
* canonical record.
*/
private void publishLifecycle(AgentEntity agent, String phase) {
if (events == null || agent == null) return;
try {
events.publishEvent(new AgentLifecycleEvent(
agent.getWorkspaceId() == null ? 0L : agent.getWorkspaceId(),
agent.getId() == null ? 0L : agent.getId(),
agent.getName(),
phase,
System.currentTimeMillis()));
} catch (Exception e) {
log.warn("[AgentService] lifecycle publish failed for agent {} ({}): {}",
agent.getId(), phase, e.getMessage());
}
}
/**

View File

@ -0,0 +1,25 @@
package vip.mate.agent.event;
/**
* Spring application event fired when an agent's lifecycle state changes.
* The trigger module subscribes via {@code @EventListener} and forwards
* the payload through {@code TriggerEventIngestService} so triggers of
* pattern type {@code agent_lifecycle} can fan out to workflows.
*
* <p>{@code phase} matches the matcher's vocabulary: {@code spawned} for
* a fresh create, {@code enabled} / {@code disabled} for a flag flip,
* {@code terminated} for a delete. {@code crashed} is reserved for v1
* once the agent runtime grows a structured error hook.
*
* <p>The dedup key downstream is {@code phase + ":" + agentId + ":" +
* timestamp}; that's stable across retries of the same operation but
* lets the same agent flip enabled/disabled repeatedly without the
* trigger pipeline collapsing the events.
*/
public record AgentLifecycleEvent(
long workspaceId,
long agentId,
String agentName,
String phase,
long timestamp
) {}

View File

@ -0,0 +1,55 @@
package vip.mate.trigger.dispatch;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import vip.mate.agent.event.AgentLifecycleEvent;
import vip.mate.trigger.ingest.TriggerEventEnvelope;
import vip.mate.trigger.ingest.TriggerEventIngestService;
import java.util.HashMap;
import java.util.Map;
/**
* Forwards {@link AgentLifecycleEvent} into the trigger pipeline as
* {@code agent_lifecycle} envelopes. Lives in the trigger module so the
* agent runtime stays free of trigger / ingest dependencies, matching
* the workflow_completion + channel_message bridge pattern.
*
* <p>The dedup key composes phase + agentId + timestamp so the same
* agent flipping enabled / disabled repeatedly stays observable, but
* an at-least-once retry of the same exact lifecycle event collapses.
* Failures inside ingest are logged and swallowed.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AgentLifecycleEventBridge {
private final TriggerEventIngestService ingestService;
@EventListener
public void onLifecycle(AgentLifecycleEvent event) {
if (event == null) return;
try {
Map<String, Object> data = new HashMap<>();
// The matcher reads `agentId` and `phase` out of the envelope
// data; the field names mirror the matcher's vocabulary so
// pattern_json can narrow precisely.
data.put("agentId", event.agentId());
if (event.agentName() != null) data.put("agentName", event.agentName());
data.put("phase", event.phase());
data.put("timestamp", event.timestamp());
ingestService.ingest(new TriggerEventEnvelope(
event.workspaceId(),
"agent_lifecycle",
event.phase() + ":" + event.agentId() + ":" + event.timestamp(),
"system",
data));
} catch (Exception e) {
log.warn("[AgentLifecycleBridge] forwarding agent {} phase={} failed: {}",
event.agentId(), event.phase(), e.getMessage());
}
}
}