1111 lines
38 KiB
Go
1111 lines
38 KiB
Go
// Package main is the unified entry point for Kandev.
|
|
// This single binary runs all services together with shared infrastructure.
|
|
// The server exposes WebSocket and HTTP endpoints.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
|
|
// Common packages
|
|
"github.com/kandev/kandev/internal/common/config"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
|
|
// Event bus
|
|
"github.com/kandev/kandev/internal/events"
|
|
"github.com/kandev/kandev/internal/events/bus"
|
|
|
|
// WebSocket gateway
|
|
gateways "github.com/kandev/kandev/internal/gateway/websocket"
|
|
|
|
// Agent Manager packages
|
|
"github.com/kandev/kandev/internal/agent/agentctl/launcher"
|
|
agentcontroller "github.com/kandev/kandev/internal/agent/controller"
|
|
"github.com/kandev/kandev/internal/agent/credentials"
|
|
"github.com/kandev/kandev/internal/agent/discovery"
|
|
"github.com/kandev/kandev/internal/agent/docker"
|
|
agenthandlers "github.com/kandev/kandev/internal/agent/handlers"
|
|
"github.com/kandev/kandev/internal/agent/lifecycle"
|
|
"github.com/kandev/kandev/internal/agent/registry"
|
|
agentsettingscontroller "github.com/kandev/kandev/internal/agent/settings/controller"
|
|
agentsettingshandlers "github.com/kandev/kandev/internal/agent/settings/handlers"
|
|
settingsstore "github.com/kandev/kandev/internal/agent/settings/store"
|
|
"github.com/kandev/kandev/internal/agent/worktree"
|
|
|
|
// Orchestrator packages
|
|
"github.com/kandev/kandev/internal/orchestrator"
|
|
orchestratorcontroller "github.com/kandev/kandev/internal/orchestrator/controller"
|
|
"github.com/kandev/kandev/internal/orchestrator/executor"
|
|
orchestratorhandlers "github.com/kandev/kandev/internal/orchestrator/handlers"
|
|
|
|
// Task Service packages
|
|
taskcontroller "github.com/kandev/kandev/internal/task/controller"
|
|
taskhandlers "github.com/kandev/kandev/internal/task/handlers"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
"github.com/kandev/kandev/internal/task/repository"
|
|
taskservice "github.com/kandev/kandev/internal/task/service"
|
|
usercontroller "github.com/kandev/kandev/internal/user/controller"
|
|
userhandlers "github.com/kandev/kandev/internal/user/handlers"
|
|
userservice "github.com/kandev/kandev/internal/user/service"
|
|
userstore "github.com/kandev/kandev/internal/user/store"
|
|
|
|
// ACP protocol
|
|
"github.com/kandev/kandev/pkg/acp/protocol"
|
|
v1 "github.com/kandev/kandev/pkg/api/v1"
|
|
ws "github.com/kandev/kandev/pkg/websocket"
|
|
)
|
|
|
|
// Command-line flags
|
|
var (
|
|
flagAgentRuntime = flag.String("agent-runtime", "", "Agent runtime mode: 'docker' or 'standalone'")
|
|
flagPort = flag.Int("port", 0, "HTTP server port (default: 8080)")
|
|
flagLogLevel = flag.String("log-level", "", "Log level: debug, info, warn, error")
|
|
flagHelp = flag.Bool("help", false, "Show help message")
|
|
flagVersion = flag.Bool("version", false, "Show version information")
|
|
)
|
|
|
|
func init() {
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, "Usage: kandev [options]\n\n")
|
|
fmt.Fprintf(os.Stderr, "Kandev is an AI-powered development task orchestrator.\n\n")
|
|
fmt.Fprintf(os.Stderr, "Options:\n")
|
|
flag.PrintDefaults()
|
|
fmt.Fprintf(os.Stderr, "\nExamples:\n")
|
|
fmt.Fprintf(os.Stderr, " kandev # Start with Docker runtime (default)\n")
|
|
fmt.Fprintf(os.Stderr, " kandev -agent-runtime=standalone # Start with standalone agentctl\n")
|
|
fmt.Fprintf(os.Stderr, " kandev -port=9000 -log-level=debug # Custom port and log level\n")
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
if *flagHelp {
|
|
flag.Usage()
|
|
os.Exit(0)
|
|
}
|
|
|
|
if *flagVersion {
|
|
fmt.Println("kandev version 0.1.0")
|
|
os.Exit(0)
|
|
}
|
|
|
|
// 1. Load configuration
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Apply command-line flag overrides (flags take precedence over config/env)
|
|
if *flagAgentRuntime != "" {
|
|
cfg.Agent.Runtime = *flagAgentRuntime
|
|
}
|
|
if *flagPort > 0 {
|
|
cfg.Server.Port = *flagPort
|
|
}
|
|
if *flagLogLevel != "" {
|
|
cfg.Logging.Level = *flagLogLevel
|
|
}
|
|
|
|
// 2. Initialize logger
|
|
log, err := logger.NewLogger(logger.LoggingConfig{
|
|
Level: cfg.Logging.Level,
|
|
Format: cfg.Logging.Format,
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() {
|
|
_ = log.Sync()
|
|
}()
|
|
logger.SetDefault(log)
|
|
|
|
log.Info("Starting Kandev (unified mode)...")
|
|
|
|
// 3. Create context with cancellation
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// 4. Initialize event bus (in-memory for unified mode, or NATS if configured)
|
|
var eventBus bus.EventBus
|
|
if cfg.NATS.URL != "" {
|
|
log.Info("Connecting to NATS...", zap.String("url", cfg.NATS.URL))
|
|
natsEventBus, err := bus.NewNATSEventBus(cfg.NATS, log)
|
|
if err != nil {
|
|
log.Fatal("Failed to connect to NATS", zap.Error(err))
|
|
}
|
|
eventBus = natsEventBus
|
|
defer natsEventBus.Close()
|
|
log.Info("Connected to NATS event bus")
|
|
} else {
|
|
log.Info("Using in-memory event bus")
|
|
eventBus = bus.NewMemoryEventBus(log)
|
|
}
|
|
|
|
// 5. Initialize Docker client
|
|
dockerClient, err := docker.NewClient(cfg.Docker, log)
|
|
if err != nil {
|
|
log.Warn("Failed to initialize Docker client - agent features will be disabled", zap.Error(err))
|
|
dockerClient = nil
|
|
} else {
|
|
if err := dockerClient.Ping(ctx); err != nil {
|
|
log.Warn("Docker daemon not available - agent features will be disabled", zap.Error(err))
|
|
_ = dockerClient.Close()
|
|
dockerClient = nil
|
|
} else {
|
|
defer func() {
|
|
_ = dockerClient.Close()
|
|
}()
|
|
log.Info("Connected to Docker daemon")
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// TASK SERVICE
|
|
// ============================================
|
|
log.Info("Initializing Task Service...")
|
|
|
|
// Get database path from environment or use default
|
|
dbPath := os.Getenv("KANDEV_DB_PATH")
|
|
if dbPath == "" {
|
|
dbPath = "./kandev.db"
|
|
}
|
|
|
|
// Initialize SQLite repository
|
|
var taskRepo repository.Repository
|
|
taskRepo, err = repository.NewSQLiteRepository(dbPath)
|
|
if err != nil {
|
|
log.Fatal("Failed to initialize SQLite database", zap.Error(err), zap.String("db_path", dbPath))
|
|
}
|
|
defer func() {
|
|
_ = taskRepo.Close()
|
|
}()
|
|
log.Info("SQLite database initialized", zap.String("db_path", dbPath))
|
|
|
|
agentSettingsRepo, err := settingsstore.NewSQLiteRepository(dbPath)
|
|
if err != nil {
|
|
log.Fatal("Failed to initialize agent settings store", zap.Error(err), zap.String("db_path", dbPath))
|
|
}
|
|
defer func() {
|
|
_ = agentSettingsRepo.Close()
|
|
}()
|
|
|
|
userRepo, err := userstore.NewSQLiteRepository(dbPath)
|
|
if err != nil {
|
|
log.Fatal("Failed to initialize user store", zap.Error(err), zap.String("db_path", dbPath))
|
|
}
|
|
defer func() {
|
|
_ = userRepo.Close()
|
|
}()
|
|
|
|
discoveryRegistry, err := discovery.LoadRegistry()
|
|
if err != nil {
|
|
log.Fatal("Failed to load agent discovery config", zap.Error(err))
|
|
}
|
|
agentSettingsController := agentsettingscontroller.NewController(agentSettingsRepo, discoveryRegistry, taskRepo, log)
|
|
|
|
userSvc := userservice.NewService(userRepo, eventBus, log)
|
|
userCtrl := usercontroller.NewController(userSvc)
|
|
|
|
taskSvc := taskservice.NewService(
|
|
taskRepo,
|
|
eventBus,
|
|
log,
|
|
taskservice.RepositoryDiscoveryConfig{
|
|
Roots: cfg.RepositoryDiscovery.Roots,
|
|
MaxDepth: cfg.RepositoryDiscovery.MaxDepth,
|
|
},
|
|
)
|
|
log.Info("Task Service initialized")
|
|
|
|
// ACP messages are now stored as comments in the task_comments table
|
|
// The comment system provides unified storage for all task-related communication
|
|
log.Info("ACP messages will be stored as comments")
|
|
|
|
// ============================================
|
|
// AGENTCTL LAUNCHER (for standalone mode)
|
|
// ============================================
|
|
var agentctlLauncher *launcher.Launcher
|
|
|
|
if cfg.Agent.Runtime == "standalone" {
|
|
log.Info("Starting agentctl for standalone mode...")
|
|
|
|
agentctlLauncher = launcher.New(launcher.Config{
|
|
Host: cfg.Agent.StandaloneHost,
|
|
Port: cfg.Agent.StandalonePort,
|
|
AutoApprovePermissions: true, // Always auto-approve in standalone mode (reindex, etc.)
|
|
}, log)
|
|
|
|
if err := agentctlLauncher.Start(ctx); err != nil {
|
|
log.Fatal("Failed to start agentctl subprocess", zap.Error(err))
|
|
}
|
|
|
|
log.Info("agentctl started successfully",
|
|
zap.Int("port", cfg.Agent.StandalonePort))
|
|
|
|
// Ensure agentctl is stopped on panic (in addition to Pdeathsig for crashes)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Error("panic recovered, stopping agentctl", zap.Any("panic", r))
|
|
stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer stopCancel()
|
|
if err := agentctlLauncher.Stop(stopCtx); err != nil {
|
|
log.Error("failed to stop agentctl on panic", zap.Error(err))
|
|
}
|
|
panic(r) // Re-panic after cleanup
|
|
}
|
|
}()
|
|
}
|
|
|
|
// ============================================
|
|
// AGENT MANAGER
|
|
// ============================================
|
|
var lifecycleMgr *lifecycle.Manager
|
|
var agentRegistry *registry.Registry
|
|
|
|
// Agent manager is enabled if:
|
|
// - Docker mode and Docker is available, OR
|
|
// - Standalone mode (agentctl was started above)
|
|
agentManagerEnabled := (cfg.Agent.Runtime == "docker" && dockerClient != nil) ||
|
|
cfg.Agent.Runtime == "standalone"
|
|
|
|
if agentManagerEnabled {
|
|
log.Info("Initializing Agent Manager...",
|
|
zap.String("runtime", cfg.Agent.Runtime))
|
|
|
|
// Agent Registry
|
|
agentRegistry = registry.NewRegistry(log)
|
|
agentRegistry.LoadDefaults()
|
|
|
|
// Credentials Manager
|
|
credsMgr := credentials.NewManager(log)
|
|
credsMgr.AddProvider(credentials.NewEnvProvider("KANDEV_"))
|
|
credsMgr.AddProvider(credentials.NewAugmentSessionProvider()) // Read ~/.augment/session.json
|
|
if credsFile := os.Getenv("KANDEV_CREDENTIALS_FILE"); credsFile != "" {
|
|
credsMgr.AddProvider(credentials.NewFileProvider(credsFile))
|
|
}
|
|
|
|
// Lifecycle Manager (uses agentctl for agent communication)
|
|
lifecycleMgr = lifecycle.NewManager(dockerClient, agentRegistry, eventBus, cfg.Agent, log)
|
|
lifecycleMgr.SetCredentialsManager(credsMgr)
|
|
lifecycleMgr.SetProfileResolver(lifecycle.NewStoreProfileResolver(agentSettingsRepo))
|
|
|
|
if err := lifecycleMgr.Start(ctx); err != nil {
|
|
log.Fatal("Failed to start lifecycle manager", zap.Error(err))
|
|
}
|
|
|
|
log.Info("Agent Manager initialized",
|
|
zap.String("runtime", cfg.Agent.Runtime),
|
|
zap.Int("agent_types", len(agentRegistry.List())))
|
|
} else {
|
|
log.Info("Agent Manager disabled (no Docker and not in standalone mode)")
|
|
}
|
|
|
|
// ============================================
|
|
// WORKTREE MANAGER
|
|
// ============================================
|
|
log.Info("Initializing Worktree Manager...")
|
|
|
|
// Create worktree store (uses same database as tasks)
|
|
// Type assert to get the *sql.DB from the SQLite repository
|
|
var worktreeMgr *worktree.Manager
|
|
if sqliteRepo, ok := taskRepo.(*repository.SQLiteRepository); ok {
|
|
worktreeStore, err := worktree.NewSQLiteStore(sqliteRepo.DB())
|
|
if err != nil {
|
|
log.Fatal("Failed to initialize worktree store", zap.Error(err))
|
|
}
|
|
|
|
// Create worktree manager with config from common config
|
|
worktreeMgr, err = worktree.NewManager(worktree.Config{
|
|
Enabled: cfg.Worktree.Enabled,
|
|
BasePath: cfg.Worktree.BasePath,
|
|
MaxPerRepo: cfg.Worktree.MaxPerRepo,
|
|
BranchPrefix: "kandev/", // Default branch prefix
|
|
}, worktreeStore, log)
|
|
if err != nil {
|
|
log.Fatal("Failed to initialize worktree manager", zap.Error(err))
|
|
}
|
|
log.Info("Worktree Manager initialized",
|
|
zap.Bool("enabled", cfg.Worktree.Enabled),
|
|
zap.String("base_path", cfg.Worktree.BasePath))
|
|
|
|
// Wire worktree manager to lifecycle manager for agent isolation
|
|
if lifecycleMgr != nil {
|
|
lifecycleMgr.SetWorktreeManager(worktreeMgr)
|
|
log.Info("Worktree Manager connected to Lifecycle Manager")
|
|
}
|
|
|
|
// Wire worktree manager to task service for cleanup on task deletion
|
|
taskSvc.SetWorktreeCleanup(worktreeMgr)
|
|
log.Info("Worktree Manager connected to Task Service for cleanup")
|
|
} else {
|
|
log.Warn("Worktree Manager disabled (requires SQLite repository)")
|
|
}
|
|
|
|
// ============================================
|
|
// ORCHESTRATOR
|
|
// ============================================
|
|
log.Info("Initializing Orchestrator...")
|
|
|
|
// Create an adapter for the task repository to work with orchestrator
|
|
taskRepoAdapter := &taskRepositoryAdapter{repo: taskRepo, svc: taskSvc}
|
|
|
|
// Create agent manager client (adapter or mock)
|
|
var agentManagerClient executor.AgentManagerClient
|
|
if lifecycleMgr != nil {
|
|
agentManagerClient = newLifecycleAdapter(lifecycleMgr, agentRegistry, log)
|
|
} else {
|
|
agentManagerClient = executor.NewMockAgentManagerClient(log)
|
|
}
|
|
|
|
serviceCfg := orchestrator.DefaultServiceConfig()
|
|
orchestratorSvc := orchestrator.NewService(serviceCfg, eventBus, agentManagerClient, taskRepoAdapter, taskRepo, log)
|
|
taskSvc.SetExecutionStopper(orchestratorSvc)
|
|
|
|
// Set up comment creator for saving agent responses as comments
|
|
orchestratorSvc.SetMessageCreator(&messageCreatorAdapter{svc: taskSvc})
|
|
|
|
// ============================================
|
|
// WEBSOCKET GATEWAY (All communication via WebSocket)
|
|
// ============================================
|
|
log.Info("Initializing WebSocket Gateway...")
|
|
|
|
// Create the unified WebSocket gateway
|
|
gateway := gateways.NewGateway(log)
|
|
|
|
// Prepare Task Service controllers for HTTP + WebSocket handlers
|
|
workspaceController := taskcontroller.NewWorkspaceController(taskSvc)
|
|
boardController := taskcontroller.NewBoardController(taskSvc)
|
|
taskController := taskcontroller.NewTaskController(taskSvc)
|
|
if worktreeMgr != nil {
|
|
taskController.SetWorktreeLookup(worktreeMgr) // Enable worktree info in task responses
|
|
}
|
|
messageController := taskcontroller.NewMessageController(taskSvc)
|
|
repositoryController := taskcontroller.NewRepositoryController(taskSvc)
|
|
executorController := taskcontroller.NewExecutorController(taskSvc)
|
|
environmentController := taskcontroller.NewEnvironmentController(taskSvc)
|
|
|
|
// Create orchestrator controller and handlers (Pattern A)
|
|
orchestratorCtrl := orchestratorcontroller.NewController(orchestratorSvc)
|
|
orchestratorHandlers := orchestratorhandlers.NewHandlers(orchestratorCtrl, log)
|
|
orchestratorHandlers.RegisterHandlers(gateway.Dispatcher)
|
|
log.Info("Registered Orchestrator WebSocket handlers")
|
|
|
|
// Create agent controller and handlers (Pattern A)
|
|
if lifecycleMgr != nil && agentRegistry != nil {
|
|
agentCtrl := agentcontroller.NewController(lifecycleMgr, agentRegistry)
|
|
agentHandlers := agenthandlers.NewHandlers(agentCtrl, log)
|
|
agentHandlers.RegisterHandlers(gateway.Dispatcher)
|
|
log.Info("Registered Agent Manager WebSocket handlers")
|
|
|
|
// Register workspace file handlers
|
|
workspaceFileHandlers := agenthandlers.NewWorkspaceFileHandlers(lifecycleMgr, log)
|
|
workspaceFileHandlers.RegisterHandlers(gateway.Dispatcher)
|
|
log.Info("Registered Workspace File WebSocket handlers")
|
|
|
|
// Register shell handlers
|
|
shellHandlers := agenthandlers.NewShellHandlers(lifecycleMgr, log)
|
|
shellHandlers.SetHub(gateway.Hub)
|
|
shellHandlers.RegisterHandlers(gateway.Dispatcher)
|
|
lifecycleMgr.SetShellStreamStarter(shellHandlers)
|
|
log.Info("Registered Shell WebSocket handlers")
|
|
}
|
|
|
|
// Start the WebSocket hub
|
|
go gateway.Hub.Run(ctx)
|
|
gateways.RegisterTaskNotifications(ctx, eventBus, gateway.Hub, log)
|
|
gateways.RegisterUserNotifications(ctx, eventBus, gateway.Hub, log)
|
|
|
|
// Set up historical logs provider for task subscriptions
|
|
// Uses messages instead of execution logs - all agent messages are now messages
|
|
gateway.Hub.SetHistoricalLogsProvider(func(ctx context.Context, taskID string) ([]*ws.Message, error) {
|
|
var session *models.TaskSession
|
|
session, err = taskRepo.GetActiveTaskSessionByTaskID(ctx, taskID)
|
|
if err != nil {
|
|
session, err = taskRepo.GetTaskSessionByTaskID(ctx, taskID)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
messages, err := taskSvc.ListMessages(ctx, session.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]*ws.Message, 0, len(messages))
|
|
for _, message := range messages {
|
|
action := ws.ActionMessageAdded
|
|
payload := map[string]interface{}{
|
|
"message_id": message.ID,
|
|
"task_session_id": message.TaskSessionID,
|
|
"task_id": message.TaskID,
|
|
"author_type": string(message.AuthorType),
|
|
"author_id": message.AuthorID,
|
|
"content": message.Content,
|
|
"type": string(message.Type),
|
|
"requests_input": message.RequestsInput,
|
|
"created_at": message.CreatedAt.Format(time.RFC3339),
|
|
}
|
|
if message.Metadata != nil {
|
|
payload["metadata"] = message.Metadata
|
|
}
|
|
notification, err := ws.NewNotification(action, payload)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
result = append(result, notification)
|
|
}
|
|
|
|
// Also send current git status if available
|
|
session, err = taskRepo.GetActiveTaskSessionByTaskID(ctx, taskID)
|
|
if err == nil && session != nil && session.Metadata != nil {
|
|
if gitStatus, ok := session.Metadata["git_status"]; ok {
|
|
// Add task_id to the git status data
|
|
gitStatusData, ok := gitStatus.(map[string]interface{})
|
|
if !ok {
|
|
gitStatusData = map[string]interface{}{"data": gitStatus}
|
|
}
|
|
gitStatusData["task_id"] = taskID
|
|
|
|
gitStatusNotification, err := ws.NewNotification("git.status", gitStatusData)
|
|
if err == nil {
|
|
result = append(result, gitStatusNotification)
|
|
}
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
})
|
|
log.Info("Historical logs provider configured for task subscriptions (using messages and git status)")
|
|
|
|
// NOTE: We no longer create comments for each ACP message chunk.
|
|
// Instead, the lifecycle manager accumulates message chunks and:
|
|
// 1. Flushes the buffer as a comment when a tool use starts (step boundary)
|
|
// 2. Flushes the buffer as a comment when the prompt completes (final response)
|
|
// This is handled via the prompt.complete and step.complete events.
|
|
|
|
// Wire ACP handler to broadcast to WebSocket clients as notifications
|
|
orchestratorSvc.RegisterACPHandler(func(taskID string, msg *protocol.Message) {
|
|
// Only broadcast message types that the frontend handles
|
|
// Skip internal message types like session/update
|
|
switch msg.Type {
|
|
case protocol.MessageTypeProgress,
|
|
protocol.MessageTypeLog,
|
|
protocol.MessageTypeResult,
|
|
protocol.MessageTypeError,
|
|
protocol.MessageTypeStatus,
|
|
protocol.MessageTypeHeartbeat,
|
|
protocol.MessageTypeInputRequired:
|
|
// Convert ACP message to WebSocket notification
|
|
action := "acp." + string(msg.Type)
|
|
notification, _ := ws.NewNotification(action, map[string]interface{}{
|
|
"task_id": taskID,
|
|
"type": msg.Type,
|
|
"data": msg.Data,
|
|
"timestamp": msg.Timestamp,
|
|
})
|
|
gateway.Hub.BroadcastToTask(taskID, notification)
|
|
default:
|
|
// Skip other message types (session/update, etc.)
|
|
}
|
|
})
|
|
|
|
// Wire input request handler to create agent messages when input is requested
|
|
orchestratorSvc.SetInputRequestHandler(func(ctx context.Context, taskID, agentID, message string) error {
|
|
log.Info("agent requesting user input, creating message",
|
|
zap.String("task_id", taskID),
|
|
zap.String("agent_id", agentID))
|
|
|
|
session, err := taskRepo.GetActiveTaskSessionByTaskID(ctx, taskID)
|
|
if err != nil {
|
|
log.Error("failed to resolve active agent session for input request",
|
|
zap.String("task_id", taskID),
|
|
zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
// Create a message from the agent
|
|
messageRecord, err := taskSvc.CreateMessage(ctx, &taskservice.CreateMessageRequest{
|
|
TaskSessionID: session.ID,
|
|
TaskID: taskID,
|
|
Content: message,
|
|
AuthorType: "agent",
|
|
AuthorID: agentID,
|
|
RequestsInput: true,
|
|
})
|
|
if err != nil {
|
|
log.Error("failed to create agent message",
|
|
zap.String("task_id", taskID),
|
|
zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
// Broadcast message.added notification to task subscribers
|
|
notification, _ := ws.NewNotification(ws.ActionMessageAdded, map[string]interface{}{
|
|
"task_id": taskID,
|
|
"task_session_id": session.ID,
|
|
"message": messageRecord.ToAPI(),
|
|
"requests_input": true,
|
|
})
|
|
gateway.Hub.BroadcastToTask(taskID, notification)
|
|
|
|
// Also broadcast input.requested notification
|
|
inputNotification, _ := ws.NewNotification(ws.ActionInputRequested, map[string]interface{}{
|
|
"task_id": taskID,
|
|
"message_id": messageRecord.ID,
|
|
"message": message,
|
|
})
|
|
gateway.Hub.BroadcastToTask(taskID, inputNotification)
|
|
|
|
return nil
|
|
})
|
|
|
|
if err := orchestratorSvc.Start(ctx); err != nil {
|
|
log.Fatal("Failed to start orchestrator", zap.Error(err))
|
|
}
|
|
log.Info("Orchestrator initialized")
|
|
|
|
// Subscribe to message.added events and broadcast to WebSocket subscribers
|
|
_, err = eventBus.Subscribe(events.MessageAdded, func(ctx context.Context, event *bus.Event) error {
|
|
data := event.Data
|
|
taskSessionID, _ := data["task_session_id"].(string)
|
|
taskID, _ := data["task_id"].(string)
|
|
if taskSessionID == "" {
|
|
return nil
|
|
}
|
|
|
|
payload := map[string]interface{}{
|
|
"task_id": taskID,
|
|
"task_session_id": taskSessionID,
|
|
"message_id": data["message_id"],
|
|
"author_type": data["author_type"],
|
|
"author_id": data["author_id"],
|
|
"content": data["content"],
|
|
"type": data["type"],
|
|
"requests_input": data["requests_input"],
|
|
"created_at": data["created_at"],
|
|
}
|
|
if metadata, ok := data["metadata"]; ok && metadata != nil {
|
|
payload["metadata"] = metadata
|
|
}
|
|
notification, err := ws.NewNotification(ws.ActionMessageAdded, payload)
|
|
if err != nil {
|
|
log.Error("Failed to create message.added notification", zap.Error(err))
|
|
return nil
|
|
}
|
|
gateway.Hub.BroadcastToTask(taskID, notification)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Error("Failed to subscribe to message.added events", zap.Error(err))
|
|
} else {
|
|
log.Info("Subscribed to message.added events for WebSocket broadcasting")
|
|
}
|
|
|
|
// Subscribe to message.updated events and broadcast to WebSocket subscribers
|
|
_, err = eventBus.Subscribe(events.MessageUpdated, func(ctx context.Context, event *bus.Event) error {
|
|
data := event.Data
|
|
taskSessionID, _ := data["task_session_id"].(string)
|
|
taskID, _ := data["task_id"].(string)
|
|
if taskSessionID == "" {
|
|
log.Warn("message.updated event has no task_session_id, skipping")
|
|
return nil
|
|
}
|
|
payload := map[string]interface{}{
|
|
"message_id": data["message_id"],
|
|
"task_session_id": taskSessionID,
|
|
"task_id": taskID,
|
|
"author_type": data["author_type"],
|
|
"author_id": data["author_id"],
|
|
"content": data["content"],
|
|
"type": data["type"],
|
|
"requests_input": data["requests_input"],
|
|
"created_at": data["created_at"],
|
|
}
|
|
if metadata, ok := data["metadata"]; ok && metadata != nil {
|
|
payload["metadata"] = metadata
|
|
}
|
|
notification, err := ws.NewNotification(ws.ActionMessageUpdated, payload)
|
|
if err != nil {
|
|
log.Error("Failed to create message.updated notification", zap.Error(err))
|
|
return nil
|
|
}
|
|
gateway.Hub.BroadcastToTask(taskID, notification)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Error("Failed to subscribe to message.updated events", zap.Error(err))
|
|
} else {
|
|
log.Info("Subscribed to message.updated events for WebSocket broadcasting")
|
|
}
|
|
|
|
// Subscribe to task_session.state_changed events and broadcast to task subscribers
|
|
_, err = eventBus.Subscribe(events.TaskSessionStateChanged, func(ctx context.Context, event *bus.Event) error {
|
|
data := event.Data
|
|
taskID, _ := data["task_id"].(string)
|
|
if taskID == "" {
|
|
return nil
|
|
}
|
|
notification, err := ws.NewNotification(ws.ActionTaskSessionStateChanged, data)
|
|
if err != nil {
|
|
log.Error("Failed to create task_session.state_changed notification", zap.Error(err))
|
|
return nil
|
|
}
|
|
gateway.Hub.BroadcastToTask(taskID, notification)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Error("Failed to subscribe to task_session.state_changed events", zap.Error(err))
|
|
} else {
|
|
log.Info("Subscribed to task_session.state_changed events for WebSocket broadcasting")
|
|
}
|
|
|
|
// Subscribe to git status events and broadcast to WebSocket subscribers
|
|
_, err = eventBus.Subscribe(events.BuildGitStatusWildcardSubject(), func(ctx context.Context, event *bus.Event) error {
|
|
data := event.Data
|
|
taskID, _ := data["task_id"].(string)
|
|
if taskID == "" {
|
|
return nil
|
|
}
|
|
|
|
// Broadcast git status update to task subscribers
|
|
notification, _ := ws.NewNotification("git.status", data)
|
|
gateway.Hub.BroadcastToTask(taskID, notification)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Error("Failed to subscribe to git status events", zap.Error(err))
|
|
} else {
|
|
log.Info("Subscribed to git status events for WebSocket broadcasting")
|
|
}
|
|
|
|
// Subscribe to file change events and broadcast to WebSocket subscribers
|
|
_, err = eventBus.Subscribe(events.BuildFileChangeWildcardSubject(), func(ctx context.Context, event *bus.Event) error {
|
|
data := event.Data
|
|
taskID, _ := data["task_id"].(string)
|
|
if taskID == "" {
|
|
return nil
|
|
}
|
|
|
|
// Broadcast file change notification to task subscribers
|
|
notification, _ := ws.NewNotification("workspace.file.changes", data)
|
|
gateway.Hub.BroadcastToTask(taskID, notification)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Error("Failed to subscribe to file change events", zap.Error(err))
|
|
} else {
|
|
log.Info("Subscribed to file change events for WebSocket broadcasting")
|
|
}
|
|
|
|
// ============================================
|
|
// HTTP SERVER (WebSocket + HTTP endpoints)
|
|
// ============================================
|
|
if cfg.Logging.Level != "debug" {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
router := gin.New()
|
|
router.Use(gin.Recovery())
|
|
router.Use(corsMiddleware())
|
|
|
|
// WebSocket endpoint - primary realtime transport
|
|
gateway.SetupRoutes(router)
|
|
|
|
// Task Service handlers (HTTP + WebSocket)
|
|
taskhandlers.RegisterWorkspaceRoutes(router, gateway.Dispatcher, workspaceController, log)
|
|
taskhandlers.RegisterBoardRoutes(router, gateway.Dispatcher, boardController, log)
|
|
taskhandlers.RegisterTaskRoutes(router, gateway.Dispatcher, taskController, log)
|
|
taskhandlers.RegisterRepositoryRoutes(router, gateway.Dispatcher, repositoryController, log)
|
|
taskhandlers.RegisterExecutorRoutes(router, gateway.Dispatcher, executorController, log)
|
|
taskhandlers.RegisterEnvironmentRoutes(router, gateway.Dispatcher, environmentController, log)
|
|
taskhandlers.RegisterMessageRoutes(
|
|
router,
|
|
gateway.Dispatcher,
|
|
messageController,
|
|
taskController,
|
|
&orchestratorAdapter{svc: orchestratorSvc},
|
|
log,
|
|
)
|
|
log.Info("Registered Task Service handlers (HTTP + WebSocket)")
|
|
|
|
agentsettingshandlers.RegisterRoutes(router, agentSettingsController, gateway.Hub, log)
|
|
log.Info("Registered Agent Settings handlers (HTTP)")
|
|
|
|
userhandlers.RegisterRoutes(router, gateway.Dispatcher, userCtrl, log)
|
|
log.Info("Registered User handlers (HTTP + WebSocket)")
|
|
|
|
// Health check (simple HTTP for load balancers/monitoring)
|
|
router.GET("/health", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "ok",
|
|
"service": "kandev",
|
|
"mode": "websocket+http",
|
|
})
|
|
})
|
|
|
|
// Create HTTP server
|
|
port := cfg.Server.Port
|
|
if port == 0 {
|
|
port = 8080
|
|
}
|
|
server := &http.Server{
|
|
Addr: fmt.Sprintf(":%d", port),
|
|
Handler: router,
|
|
ReadTimeout: cfg.Server.ReadTimeoutDuration(),
|
|
WriteTimeout: cfg.Server.WriteTimeoutDuration(),
|
|
}
|
|
|
|
// Start server
|
|
go func() {
|
|
log.Info("WebSocket server listening", zap.Int("port", port))
|
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatal("Failed to start server", zap.Error(err))
|
|
}
|
|
}()
|
|
|
|
// Print routes summary
|
|
log.Info("API configured",
|
|
zap.String("websocket", "/ws"),
|
|
zap.String("health", "/health"),
|
|
zap.String("http", "/api/v1"),
|
|
)
|
|
|
|
// ============================================
|
|
// GRACEFUL SHUTDOWN
|
|
// ============================================
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
|
sig := <-quit
|
|
|
|
log.Info("Received shutdown signal", zap.String("signal", sig.String()))
|
|
|
|
log.Info("Shutting down Kandev...")
|
|
cancel()
|
|
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer shutdownCancel()
|
|
|
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
|
log.Error("HTTP server shutdown error", zap.Error(err))
|
|
}
|
|
|
|
if err := orchestratorSvc.Stop(); err != nil {
|
|
log.Error("Orchestrator stop error", zap.Error(err))
|
|
}
|
|
|
|
if lifecycleMgr != nil {
|
|
if err := lifecycleMgr.Stop(); err != nil {
|
|
log.Error("Lifecycle manager stop error", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
// Stop agentctl subprocess (must be after lifecycle manager to allow cleanup)
|
|
if agentctlLauncher != nil {
|
|
log.Info("Stopping agentctl subprocess...")
|
|
if err := agentctlLauncher.Stop(shutdownCtx); err != nil {
|
|
log.Error("agentctl stop error", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
log.Info("Kandev stopped")
|
|
}
|
|
|
|
// taskRepositoryAdapter adapts the task repository for the orchestrator's scheduler
|
|
type taskRepositoryAdapter struct {
|
|
repo repository.Repository
|
|
svc *taskservice.Service
|
|
}
|
|
|
|
// GetTask retrieves a task by ID and converts it to API type
|
|
func (a *taskRepositoryAdapter) GetTask(ctx context.Context, taskID string) (*v1.Task, error) {
|
|
task, err := a.repo.GetTask(ctx, taskID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return task.ToAPI(), nil
|
|
}
|
|
|
|
// UpdateTaskState updates task state via the service
|
|
func (a *taskRepositoryAdapter) UpdateTaskState(ctx context.Context, taskID string, state v1.TaskState) error {
|
|
_, err := a.svc.UpdateTaskState(ctx, taskID, state)
|
|
return err
|
|
}
|
|
|
|
// lifecycleAdapter adapts the lifecycle manager as an AgentManagerClient
|
|
type lifecycleAdapter struct {
|
|
mgr *lifecycle.Manager
|
|
registry *registry.Registry
|
|
logger *logger.Logger
|
|
}
|
|
|
|
// newLifecycleAdapter creates a new lifecycle adapter
|
|
func newLifecycleAdapter(mgr *lifecycle.Manager, reg *registry.Registry, log *logger.Logger) *lifecycleAdapter {
|
|
return &lifecycleAdapter{
|
|
mgr: mgr,
|
|
registry: reg,
|
|
logger: log.WithFields(zap.String("component", "lifecycle_adapter")),
|
|
}
|
|
}
|
|
|
|
// LaunchAgent starts a new agent container for a task
|
|
func (a *lifecycleAdapter) LaunchAgent(ctx context.Context, req *executor.LaunchAgentRequest) (*executor.LaunchAgentResponse, error) {
|
|
// The RepositoryURL field contains a local filesystem path for the workspace
|
|
// If empty, the agent will run without a mounted workspace
|
|
launchReq := &lifecycle.LaunchRequest{
|
|
TaskID: req.TaskID,
|
|
SessionID: req.SessionID,
|
|
TaskTitle: req.TaskTitle,
|
|
AgentProfileID: req.AgentProfileID,
|
|
WorkspacePath: req.RepositoryURL, // May be empty - lifecycle manager handles this
|
|
TaskDescription: req.TaskDescription,
|
|
Env: req.Env,
|
|
ACPSessionID: req.ACPSessionID,
|
|
Metadata: req.Metadata,
|
|
// Worktree configuration for concurrent agent execution
|
|
UseWorktree: req.UseWorktree,
|
|
RepositoryID: req.RepositoryID,
|
|
RepositoryPath: req.RepositoryPath,
|
|
BaseBranch: req.BaseBranch,
|
|
}
|
|
|
|
instance, err := a.mgr.Launch(ctx, launchReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Streaming is now handled by the lifecycle manager
|
|
|
|
// Extract worktree info from metadata if available
|
|
var worktreeID, worktreePath, worktreeBranch string
|
|
if instance.Metadata != nil {
|
|
if id, ok := instance.Metadata["worktree_id"].(string); ok {
|
|
worktreeID = id
|
|
}
|
|
if path, ok := instance.Metadata["worktree_path"].(string); ok {
|
|
worktreePath = path
|
|
}
|
|
if branch, ok := instance.Metadata["worktree_branch"].(string); ok {
|
|
worktreeBranch = branch
|
|
}
|
|
}
|
|
|
|
return &executor.LaunchAgentResponse{
|
|
AgentInstanceID: instance.ID,
|
|
ContainerID: instance.ContainerID,
|
|
Status: instance.Status,
|
|
WorktreeID: worktreeID,
|
|
WorktreePath: worktreePath,
|
|
WorktreeBranch: worktreeBranch,
|
|
}, nil
|
|
}
|
|
|
|
// StopAgent stops a running agent
|
|
func (a *lifecycleAdapter) StopAgent(ctx context.Context, agentInstanceID string, force bool) error {
|
|
return a.mgr.StopAgent(ctx, agentInstanceID, force)
|
|
}
|
|
|
|
// GetAgentStatus returns the status of an agent instance
|
|
func (a *lifecycleAdapter) GetAgentStatus(ctx context.Context, agentInstanceID string) (*v1.AgentInstance, error) {
|
|
instance, found := a.mgr.GetInstance(agentInstanceID)
|
|
if !found {
|
|
return nil, fmt.Errorf("agent instance %q not found", agentInstanceID)
|
|
}
|
|
|
|
containerID := instance.ContainerID
|
|
now := time.Now()
|
|
result := &v1.AgentInstance{
|
|
ID: instance.ID,
|
|
TaskID: instance.TaskID,
|
|
AgentProfileID: instance.AgentProfileID,
|
|
ContainerID: &containerID,
|
|
Status: instance.Status,
|
|
StartedAt: &instance.StartedAt,
|
|
StoppedAt: instance.FinishedAt,
|
|
CreatedAt: instance.StartedAt,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
if instance.ExitCode != nil {
|
|
result.ExitCode = instance.ExitCode
|
|
}
|
|
if instance.ErrorMessage != "" {
|
|
result.ErrorMessage = &instance.ErrorMessage
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ListAgentTypes returns available agent types
|
|
func (a *lifecycleAdapter) ListAgentTypes(ctx context.Context) ([]*v1.AgentType, error) {
|
|
configs := a.registry.List()
|
|
result := make([]*v1.AgentType, 0, len(configs))
|
|
for _, cfg := range configs {
|
|
result = append(result, cfg.ToAPIType())
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// PromptAgent sends a follow-up prompt to a running agent
|
|
func (a *lifecycleAdapter) PromptAgent(ctx context.Context, agentInstanceID string, prompt string) (*executor.PromptResult, error) {
|
|
result, err := a.mgr.PromptAgent(ctx, agentInstanceID, prompt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &executor.PromptResult{
|
|
StopReason: result.StopReason,
|
|
AgentMessage: result.AgentMessage,
|
|
}, nil
|
|
}
|
|
|
|
// RespondToPermissionByTaskID sends a response to a permission request for a task
|
|
func (a *lifecycleAdapter) RespondToPermissionByTaskID(ctx context.Context, taskID, pendingID, optionID string, cancelled bool) error {
|
|
return a.mgr.RespondToPermissionByTaskID(taskID, pendingID, optionID, cancelled)
|
|
}
|
|
|
|
// GetRecoveredInstances returns instances recovered from Docker during startup
|
|
func (a *lifecycleAdapter) GetRecoveredInstances() []executor.RecoveredInstanceInfo {
|
|
recovered := a.mgr.GetRecoveredInstances()
|
|
result := make([]executor.RecoveredInstanceInfo, len(recovered))
|
|
for i, r := range recovered {
|
|
result[i] = executor.RecoveredInstanceInfo{
|
|
InstanceID: r.InstanceID,
|
|
TaskID: r.TaskID,
|
|
ContainerID: r.ContainerID,
|
|
AgentProfileID: r.AgentProfileID,
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// IsAgentRunningForTask checks if an agent is actually running for a task
|
|
// This probes the actual agent (Docker container or standalone process)
|
|
func (a *lifecycleAdapter) IsAgentRunningForTask(ctx context.Context, taskID string) bool {
|
|
return a.mgr.IsAgentRunningForTask(ctx, taskID)
|
|
}
|
|
|
|
// corsMiddleware returns a CORS middleware for HTTP and WebSocket connections
|
|
func corsMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Header("Access-Control-Allow-Origin", "*")
|
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS")
|
|
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization, Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Protocol")
|
|
c.Header("Access-Control-Allow-Credentials", "true")
|
|
|
|
if c.Request.Method == "OPTIONS" {
|
|
c.AbortWithStatus(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// orchestratorAdapter adapts the orchestrator.Service to the taskhandlers.OrchestratorService interface
|
|
type orchestratorAdapter struct {
|
|
svc *orchestrator.Service
|
|
}
|
|
|
|
// PromptTask forwards to the orchestrator service and converts the result type
|
|
func (a *orchestratorAdapter) PromptTask(ctx context.Context, taskID string, prompt string) (*taskhandlers.PromptResult, error) {
|
|
result, err := a.svc.PromptTask(ctx, taskID, prompt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &taskhandlers.PromptResult{
|
|
StopReason: result.StopReason,
|
|
AgentMessage: result.AgentMessage,
|
|
}, nil
|
|
}
|
|
|
|
func (a *orchestratorAdapter) ResumeTaskSession(ctx context.Context, taskID, taskSessionID string) error {
|
|
_, err := a.svc.ResumeTaskSession(ctx, taskID, taskSessionID)
|
|
return err
|
|
}
|
|
|
|
// messageCreatorAdapter adapts the task service to the orchestrator.MessageCreator interface
|
|
type messageCreatorAdapter struct {
|
|
svc *taskservice.Service
|
|
}
|
|
|
|
// CreateAgentMessage creates a message with author_type="agent"
|
|
func (a *messageCreatorAdapter) CreateAgentMessage(ctx context.Context, taskID, content, agentSessionID string) error {
|
|
_, err := a.svc.CreateMessage(ctx, &taskservice.CreateMessageRequest{
|
|
TaskSessionID: agentSessionID,
|
|
TaskID: taskID,
|
|
Content: content,
|
|
AuthorType: "agent",
|
|
})
|
|
return err
|
|
}
|
|
|
|
// CreateUserMessage creates a message with author_type="user"
|
|
func (a *messageCreatorAdapter) CreateUserMessage(ctx context.Context, taskID, content, agentSessionID string) error {
|
|
_, err := a.svc.CreateMessage(ctx, &taskservice.CreateMessageRequest{
|
|
TaskSessionID: agentSessionID,
|
|
TaskID: taskID,
|
|
Content: content,
|
|
AuthorType: "user",
|
|
})
|
|
return err
|
|
}
|
|
|
|
// CreateToolCallMessage creates a message for a tool call with type="tool_call"
|
|
func (a *messageCreatorAdapter) CreateToolCallMessage(ctx context.Context, taskID, toolCallID, title, status, agentSessionID string, args map[string]interface{}) error {
|
|
metadata := map[string]interface{}{
|
|
"tool_call_id": toolCallID,
|
|
"title": title,
|
|
"status": status,
|
|
}
|
|
|
|
// Add args if provided (contains kind, path, locations, raw_input)
|
|
if len(args) > 0 {
|
|
metadata["args"] = args
|
|
|
|
// Extract kind as tool_name for icon selection in the frontend
|
|
if kind, ok := args["kind"].(string); ok && kind != "" {
|
|
metadata["tool_name"] = kind
|
|
}
|
|
}
|
|
|
|
_, err := a.svc.CreateMessage(ctx, &taskservice.CreateMessageRequest{
|
|
TaskSessionID: agentSessionID,
|
|
TaskID: taskID,
|
|
Content: title,
|
|
AuthorType: "agent",
|
|
Type: "tool_call",
|
|
Metadata: metadata,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// UpdateToolCallMessage updates a tool call message's status
|
|
func (a *messageCreatorAdapter) UpdateToolCallMessage(ctx context.Context, taskID, toolCallID, status, result, agentSessionID string) error {
|
|
return a.svc.UpdateToolCallMessage(ctx, agentSessionID, toolCallID, status, result)
|
|
}
|
|
|
|
// CreateSessionMessage creates a message for non-chat session updates (status/progress/error/etc).
|
|
func (a *messageCreatorAdapter) CreateSessionMessage(ctx context.Context, taskID, content, agentSessionID, messageType string, metadata map[string]interface{}, requestsInput bool) error {
|
|
_, err := a.svc.CreateMessage(ctx, &taskservice.CreateMessageRequest{
|
|
TaskSessionID: agentSessionID,
|
|
TaskID: taskID,
|
|
Content: content,
|
|
AuthorType: "agent",
|
|
Type: messageType,
|
|
Metadata: metadata,
|
|
RequestsInput: requestsInput,
|
|
})
|
|
return err
|
|
}
|