Files
kandev/apps/backend/internal/agent/runtime/lifecycle/events.go
T
2026-05-17 20:16:15 +01:00

597 lines
20 KiB
Go

// Package lifecycle manages agent instance lifecycles including tracking,
// state transitions, and cleanup.
package lifecycle
import (
"context"
"time"
"go.uber.org/zap"
agentctl "github.com/kandev/kandev/internal/agent/runtime/agentctl"
"github.com/kandev/kandev/internal/agentctl/types/streams"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/events"
"github.com/kandev/kandev/internal/events/bus"
)
// EventPublisher handles publishing agent lifecycle and session events to the event bus.
type EventPublisher struct {
eventBus bus.EventBus
logger *logger.Logger
}
// NewEventPublisher creates a new EventPublisher with the given event bus and logger.
func NewEventPublisher(eventBus bus.EventBus, log *logger.Logger) *EventPublisher {
return &EventPublisher{
eventBus: eventBus,
logger: log.WithFields(zap.String("component", "event-publisher")),
}
}
// PublishAgentEvent publishes an agent lifecycle event (started, stopped, ready, completed, failed).
func (p *EventPublisher) PublishAgentEvent(ctx context.Context, eventType string, execution *AgentExecution) {
if p.eventBus == nil {
return
}
payload := AgentEventPayload{
AgentExecutionID: execution.ID,
TaskID: execution.TaskID,
SessionID: execution.SessionID,
AgentProfileID: execution.AgentProfileID,
ContainerID: execution.ContainerID,
Status: string(execution.Status),
StartedAt: execution.StartedAt,
FinishedAt: execution.FinishedAt,
ErrorMessage: execution.ErrorMessage,
ExitCode: execution.ExitCode,
}
event := bus.NewEvent(eventType, "agent-manager", payload)
if err := p.eventBus.Publish(ctx, eventType, event); err != nil {
p.logger.Error("failed to publish event",
zap.String("event_type", eventType),
zap.String("instance_id", execution.ID),
zap.Error(err))
} else {
p.logger.Debug("published agent event",
zap.String("event_type", eventType),
zap.String("instance_id", execution.ID))
}
}
// PublishAgentctlEvent publishes an agentctl lifecycle event (starting, ready, error).
func (p *EventPublisher) PublishAgentctlEvent(ctx context.Context, eventType string, execution *AgentExecution, errMsg string) {
if p.eventBus == nil {
return
}
var worktreeID string
var worktreeBranch string
if execution.Metadata != nil {
if id, ok := execution.Metadata[MetadataKeyWorktreeID].(string); ok {
worktreeID = id
}
if branch, ok := execution.Metadata[MetadataKeyWorktreeBranch].(string); ok {
worktreeBranch = branch
}
}
payload := AgentctlEventPayload{
TaskID: execution.TaskID,
SessionID: execution.SessionID,
TaskEnvironmentID: execution.TaskEnvironmentID,
AgentExecutionID: execution.ID,
ErrorMessage: errMsg,
WorktreeID: worktreeID,
WorktreePath: execution.WorkspacePath,
WorktreeBranch: worktreeBranch,
}
event := bus.NewEvent(eventType, "agent-manager", payload)
if err := p.eventBus.Publish(ctx, eventType, event); err != nil {
p.logger.Error("failed to publish agentctl event",
zap.String("event_type", eventType),
zap.String("instance_id", execution.ID),
zap.Error(err))
}
}
// PublishACPSessionCreated publishes an event when an ACP session is created.
func (p *EventPublisher) PublishACPSessionCreated(execution *AgentExecution, sessionID string) {
if p.eventBus == nil || sessionID == "" {
return
}
payload := ACPSessionCreatedPayload{
TaskID: execution.TaskID,
SessionID: execution.SessionID,
AgentProfileID: execution.ID,
AgentExecutionID: execution.ID,
ACPSessionID: sessionID,
}
event := bus.NewEvent(events.AgentACPSessionCreated, "agent-manager", payload)
if err := p.eventBus.Publish(context.Background(), events.AgentACPSessionCreated, event); err != nil {
p.logger.Error("failed to publish ACP session event",
zap.String("event_type", events.AgentACPSessionCreated),
zap.String("instance_id", execution.ID),
zap.Error(err))
}
}
// PublishAgentStreamEvent publishes an agent stream event to the event bus for WebSocket streaming.
// This is different from PublishAgentEvent which publishes lifecycle events (started, stopped, etc.).
func (p *EventPublisher) PublishAgentStreamEvent(execution *AgentExecution, event agentctl.AgentEvent) {
if p.eventBus == nil {
return
}
// Build the nested event data
// event.SessionID is the ACP session ID (internal agent protocol session)
eventData := &AgentStreamEventData{
Type: event.Type,
ACPSessionID: event.SessionID,
Text: event.Text,
ToolCallID: event.ToolCallID,
ParentToolCallID: event.ParentToolCallID,
PendingID: event.PendingID,
ToolName: event.ToolName,
ToolTitle: event.ToolTitle,
ToolStatus: event.ToolStatus,
Error: event.Error,
SessionStatus: event.SessionStatus,
Data: event.Data,
Normalized: event.NormalizedPayload,
AvailableCommands: event.AvailableCommands,
ToolCallContents: event.ToolCallContents,
ContentBlocks: event.ContentBlocks,
Role: event.Role,
CurrentModeID: event.CurrentModeID,
AvailableModes: event.AvailableModes,
SupportsImage: event.SupportsImage,
SupportsAudio: event.SupportsAudio,
SupportsEmbeddedContext: event.SupportsEmbeddedContext,
AuthMethods: event.AuthMethods,
CurrentModelID: event.CurrentModelID,
SessionModels: event.SessionModels,
ConfigOptions: event.ConfigOptions,
Usage: event.Usage,
PlanEntries: event.PlanEntries,
PlanContent: event.PlanContent,
}
// Build agent event message payload
// session_id is the task session ID (execution.SessionID)
// acp_session_id in eventData is the internal agent protocol session
payload := AgentStreamEventPayload{
Type: "agent/event",
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
AgentID: execution.ID,
ExecutionID: execution.ID,
TaskID: execution.TaskID,
SessionID: execution.SessionID,
Data: eventData,
}
busEvent := bus.NewEvent(events.AgentStream, "agent-manager", payload)
subject := events.BuildAgentStreamSubject(execution.SessionID)
if err := p.eventBus.Publish(context.Background(), subject, busEvent); err != nil {
p.logger.Error("failed to publish agent stream event",
zap.String("instance_id", execution.ID),
zap.String("task_id", execution.TaskID),
zap.String("session_id", execution.SessionID),
zap.Error(err))
}
}
// PublishAgentStreamEventPayload publishes a pre-built agent stream event payload.
// This is used for streaming message events where the payload is constructed by the caller.
func (p *EventPublisher) PublishAgentStreamEventPayload(payload *AgentStreamEventPayload) {
if p.eventBus == nil {
return
}
busEvent := bus.NewEvent(events.AgentStream, "agent-manager", *payload)
subject := events.BuildAgentStreamSubject(payload.SessionID)
if err := p.eventBus.Publish(context.Background(), subject, busEvent); err != nil {
p.logger.Error("failed to publish agent stream event payload",
zap.String("task_id", payload.TaskID),
zap.String("session_id", payload.SessionID),
zap.Error(err))
}
}
// PublishGitEvent publishes a unified git event to the event bus.
func (p *EventPublisher) PublishGitEvent(payload *GitEventPayload) {
if p.eventBus == nil {
return
}
if payload.Timestamp == "" {
payload.Timestamp = time.Now().Format(time.RFC3339Nano)
}
event := bus.NewEvent(events.GitEvent, "lifecycle", payload)
subject := events.BuildGitEventSubject(payload.SessionID)
if err := p.eventBus.Publish(context.Background(), subject, event); err != nil {
p.logger.Error("failed to publish git event",
zap.String("session_id", payload.SessionID),
zap.String("type", string(payload.Type)),
zap.Error(err))
}
}
// PublishGitStatus publishes a git status update event.
func (p *EventPublisher) PublishGitStatus(execution *AgentExecution, update *agentctl.GitStatusUpdate) {
p.PublishGitEvent(&GitEventPayload{
Type: GitEventTypeStatusUpdate,
TaskID: execution.TaskID,
SessionID: execution.SessionID,
AgentID: execution.ID,
Timestamp: update.Timestamp.Format(time.RFC3339Nano),
Status: &GitStatusData{
Branch: update.Branch,
RemoteBranch: update.RemoteBranch,
HeadCommit: update.HeadCommit,
BaseCommit: update.BaseCommit,
Modified: update.Modified,
Added: update.Added,
Deleted: update.Deleted,
Untracked: update.Untracked,
Renamed: update.Renamed,
Ahead: update.Ahead,
Behind: update.Behind,
Files: update.Files,
BranchAdditions: update.BranchAdditions,
BranchDeletions: update.BranchDeletions,
RepositoryName: update.RepositoryName,
},
})
}
// PublishGitCommit publishes a git commit created event.
func (p *EventPublisher) PublishGitCommit(execution *AgentExecution, commit *agentctl.GitCommitNotification) {
p.PublishGitEvent(&GitEventPayload{
Type: GitEventTypeCommitCreated,
TaskID: execution.TaskID,
SessionID: execution.SessionID,
AgentID: execution.ID,
Timestamp: time.Now().Format(time.RFC3339Nano),
Commit: &GitCommitData{
CommitSHA: commit.CommitSHA,
ParentSHA: commit.ParentSHA,
Message: commit.Message,
AuthorName: commit.AuthorName,
AuthorEmail: commit.AuthorEmail,
FilesChanged: commit.FilesChanged,
Insertions: commit.Insertions,
Deletions: commit.Deletions,
CommittedAt: commit.CommittedAt.Format(time.RFC3339),
RepositoryName: commit.RepositoryName,
},
})
}
// PublishGitReset publishes a git reset event (HEAD moved backward).
func (p *EventPublisher) PublishGitReset(execution *AgentExecution, reset *agentctl.GitResetNotification) {
p.PublishGitEvent(&GitEventPayload{
Type: GitEventTypeCommitsReset,
TaskID: execution.TaskID,
SessionID: execution.SessionID,
AgentID: execution.ID,
Timestamp: reset.Timestamp.Format(time.RFC3339Nano),
Reset: &GitResetData{
PreviousHead: reset.PreviousHead,
CurrentHead: reset.CurrentHead,
RepositoryName: reset.RepositoryName,
},
})
}
// PublishBranchSwitch publishes a branch switch event.
func (p *EventPublisher) PublishBranchSwitch(execution *AgentExecution, branchSwitch *agentctl.GitBranchSwitchNotification) {
p.PublishGitEvent(&GitEventPayload{
Type: GitEventTypeBranchSwitched,
TaskID: execution.TaskID,
SessionID: execution.SessionID,
AgentID: execution.ID,
Timestamp: branchSwitch.Timestamp.Format(time.RFC3339Nano),
BranchSwitch: &GitBranchSwitchData{
PreviousBranch: branchSwitch.PreviousBranch,
CurrentBranch: branchSwitch.CurrentBranch,
CurrentHead: branchSwitch.CurrentHead,
BaseCommit: branchSwitch.BaseCommit,
RepositoryName: branchSwitch.RepositoryName,
},
})
}
// PublishFileChange publishes a file change notification to the event bus.
func (p *EventPublisher) PublishFileChange(execution *AgentExecution, notification *agentctl.FileChangeNotification) {
if p.eventBus == nil {
return
}
sessionID := execution.SessionID
payload := FileChangeEventPayload{
TaskID: execution.TaskID,
SessionID: sessionID,
AgentID: execution.ID,
Path: notification.Path,
Operation: notification.Operation,
Timestamp: notification.Timestamp.Format(time.RFC3339Nano),
RepositoryName: notification.RepositoryName,
}
event := bus.NewEvent(events.FileChangeNotified, "agent-manager", payload)
subject := events.BuildFileChangeSubject(sessionID)
if err := p.eventBus.Publish(context.Background(), subject, event); err != nil {
p.logger.Error("failed to publish file change event",
zap.String("instance_id", execution.ID),
zap.String("task_id", execution.TaskID),
zap.String("session_id", sessionID),
zap.Error(err))
}
}
// PublishPermissionRequest publishes a permission request event to the event bus.
func (p *EventPublisher) PublishPermissionRequest(execution *AgentExecution, event agentctl.AgentEvent) {
if p.eventBus == nil {
return
}
// Convert options to typed format
options := make([]PermissionOption, len(event.PermissionOptions))
for i, opt := range event.PermissionOptions {
options[i] = PermissionOption{
OptionID: opt.OptionID,
Name: opt.Name,
Kind: string(opt.Kind),
}
}
payload := PermissionRequestEventPayload{
Type: "permission_request",
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
AgentID: execution.ID,
TaskID: execution.TaskID,
SessionID: execution.SessionID,
PendingID: event.PendingID,
ToolCallID: event.ToolCallID,
Title: event.PermissionTitle,
Options: options,
ActionType: event.ActionType,
ActionDetails: event.ActionDetails,
}
busEvent := bus.NewEvent(events.PermissionRequestReceived, "agent-manager", payload)
subject := events.BuildPermissionRequestSubject(execution.SessionID)
if err := p.eventBus.Publish(context.Background(), subject, busEvent); err != nil {
p.logger.Error("failed to publish permission_request event",
zap.String("instance_id", execution.ID),
zap.String("task_id", execution.TaskID),
zap.String("session_id", execution.SessionID),
zap.Error(err))
} else {
p.logger.Debug("published permission_request event",
zap.String("task_id", execution.TaskID),
zap.String("session_id", execution.SessionID),
zap.String("pending_id", event.PendingID),
zap.String("title", event.PermissionTitle))
}
}
// PublishShellOutput publishes a shell output event to the event bus.
func (p *EventPublisher) PublishShellOutput(execution *AgentExecution, data string) {
if p.eventBus == nil {
return
}
sessionID := execution.SessionID
payload := ShellOutputEventPayload{
TaskID: execution.TaskID,
SessionID: sessionID,
AgentID: execution.ID,
Type: "output",
Data: data,
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
}
event := bus.NewEvent(events.ShellOutput, "agent-manager", payload)
subject := events.BuildShellOutputSubject(sessionID)
if err := p.eventBus.Publish(context.Background(), subject, event); err != nil {
p.logger.Error("failed to publish shell output event",
zap.String("instance_id", execution.ID),
zap.String("task_id", execution.TaskID),
zap.String("session_id", sessionID),
zap.Error(err))
}
}
// PublishShellExit publishes a shell exit event to the event bus.
func (p *EventPublisher) PublishShellExit(execution *AgentExecution, exitCode int) {
if p.eventBus == nil {
return
}
sessionID := execution.SessionID
payload := ShellExitEventPayload{
TaskID: execution.TaskID,
SessionID: sessionID,
AgentID: execution.ID,
Type: "exit",
Code: exitCode,
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
}
event := bus.NewEvent(events.ShellExit, "agent-manager", payload)
subject := events.BuildShellExitSubject(sessionID)
if err := p.eventBus.Publish(context.Background(), subject, event); err != nil {
p.logger.Error("failed to publish shell exit event",
zap.String("instance_id", execution.ID),
zap.String("task_id", execution.TaskID),
zap.String("session_id", sessionID),
zap.Error(err))
}
}
// PublishProcessOutput publishes a process output event to the event bus.
func (p *EventPublisher) PublishProcessOutput(execution *AgentExecution, output *agentctl.ProcessOutput) {
if p.eventBus == nil || output == nil {
return
}
payload := ProcessOutputEventPayload{
TaskID: execution.TaskID,
SessionID: output.SessionID,
ProcessID: output.ProcessID,
Kind: string(output.Kind),
Stream: output.Stream,
Data: output.Data,
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
}
event := bus.NewEvent(events.ProcessOutput, "agent-manager", payload)
subject := events.BuildProcessOutputSubject(output.SessionID)
if err := p.eventBus.Publish(context.Background(), subject, event); err != nil {
p.logger.Error("failed to publish process output event",
zap.String("instance_id", execution.ID),
zap.String("task_id", execution.TaskID),
zap.String("session_id", output.SessionID),
zap.Error(err))
}
}
// PublishProcessStatus publishes a process status event to the event bus.
func (p *EventPublisher) PublishProcessStatus(execution *AgentExecution, status *agentctl.ProcessStatusUpdate) {
if p.eventBus == nil || status == nil {
return
}
payload := ProcessStatusEventPayload{
SessionID: status.SessionID,
ProcessID: status.ProcessID,
Kind: string(status.Kind),
ScriptName: status.ScriptName,
Status: string(status.Status),
Command: status.Command,
WorkingDir: status.WorkingDir,
ExitCode: status.ExitCode,
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
}
event := bus.NewEvent(events.ProcessStatus, "agent-manager", payload)
subject := events.BuildProcessStatusSubject(status.SessionID)
if err := p.eventBus.Publish(context.Background(), subject, event); err != nil {
p.logger.Error("failed to publish process status event",
zap.String("session_id", status.SessionID),
zap.Error(err))
}
}
// PublishContextWindow publishes a context window update event to the event bus.
func (p *EventPublisher) PublishContextWindow(execution *AgentExecution, size, used, remaining int64, efficiency float64) {
if p.eventBus == nil {
return
}
sessionID := execution.SessionID
payload := ContextWindowEventPayload{
TaskID: execution.TaskID,
SessionID: sessionID,
AgentID: execution.ID,
ContextWindowSize: size,
ContextWindowUsed: used,
ContextWindowRemaining: remaining,
ContextEfficiency: efficiency,
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
}
event := bus.NewEvent(events.ContextWindowUpdated, "agent-manager", payload)
subject := events.BuildContextWindowSubject(sessionID)
if err := p.eventBus.Publish(context.Background(), subject, event); err != nil {
p.logger.Error("failed to publish context window event",
zap.String("instance_id", execution.ID),
zap.String("task_id", execution.TaskID),
zap.String("session_id", sessionID),
zap.Error(err))
}
}
// PublishAvailableCommands publishes an available commands update event to the event bus.
func (p *EventPublisher) PublishAvailableCommands(execution *AgentExecution, commands []streams.AvailableCommand) {
if p.eventBus == nil {
return
}
sessionID := execution.SessionID
payload := AvailableCommandsEventPayload{
TaskID: execution.TaskID,
SessionID: sessionID,
AgentID: execution.ID,
AvailableCommands: commands,
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
}
event := bus.NewEvent(events.AvailableCommandsUpdated, "agent-manager", payload)
subject := events.BuildAvailableCommandsSubject(sessionID)
if err := p.eventBus.Publish(context.Background(), subject, event); err != nil {
p.logger.Error("failed to publish available commands event",
zap.String("instance_id", execution.ID),
zap.String("task_id", execution.TaskID),
zap.String("session_id", sessionID),
zap.Error(err))
}
}
// PublishPrepareProgress publishes an environment preparation progress event.
func (p *EventPublisher) PublishPrepareProgress(sessionID string, payload *PrepareProgressEventPayload) {
if p.eventBus == nil {
return
}
if payload.Timestamp == "" {
payload.Timestamp = time.Now().UTC().Format(time.RFC3339Nano)
}
event := bus.NewEvent(events.ExecutorPrepareProgress, "lifecycle", payload)
if err := p.eventBus.Publish(context.Background(), events.ExecutorPrepareProgress, event); err != nil {
p.logger.Error("failed to publish prepare progress event",
zap.String("session_id", sessionID),
zap.Error(err))
}
}
// PublishPrepareCompleted publishes an environment preparation completed event.
func (p *EventPublisher) PublishPrepareCompleted(sessionID string, payload *PrepareCompletedEventPayload) {
if p.eventBus == nil {
return
}
if payload.Timestamp == "" {
payload.Timestamp = time.Now().UTC().Format(time.RFC3339Nano)
}
event := bus.NewEvent(events.ExecutorPrepareCompleted, "lifecycle", payload)
if err := p.eventBus.Publish(context.Background(), events.ExecutorPrepareCompleted, event); err != nil {
p.logger.Error("failed to publish prepare completed event",
zap.String("session_id", sessionID),
zap.Error(err))
}
}