refactor: remove auggie-specific code and use standard ACP protocol (#47)
- Remove ProtocolAuggie constant and AuggieAdapter - Remove hooks system (HooksRegistry, AgentHooks, AuggieHooks) - Remove PreGeneratedSessionID and SessionDir fields - Change auggie-agent to use standard 'acp' protocol in agents.json - Add model select dropdown to profile settings page - Fix unused variable lint warnings Auggie now uses the standard ACP protocol flow like other agents. The --resume flag was found to not work in ACP mode, so the workaround was unnecessary since session/new returns valid session IDs.
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"agents": [
|
||||
{
|
||||
"name": "codex",
|
||||
"display_name": "Codex",
|
||||
"supports_mcp": false,
|
||||
"mcp_config_path": {
|
||||
"linux": [],
|
||||
"windows": [],
|
||||
"macos": []
|
||||
},
|
||||
"installation_path": {
|
||||
"linux": ["~/.codex/auth.json"],
|
||||
"windows": ["~/.codex/auth.json"],
|
||||
"macos": ["~/.codex/auth.json"]
|
||||
},
|
||||
"capabilities": {
|
||||
"supports_session_resume": false,
|
||||
"supports_shell": false,
|
||||
"supports_workspace_only": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "auggie",
|
||||
"display_name": "Auggie",
|
||||
"supports_mcp": false,
|
||||
"mcp_config_path": {
|
||||
"linux": [],
|
||||
"windows": [],
|
||||
"macos": []
|
||||
},
|
||||
"installation_path": {
|
||||
"linux": ["~/.augment/.auggie.json"],
|
||||
"windows": ["~/.augment/.auggie.json"],
|
||||
"macos": ["~/.augment/.auggie.json"]
|
||||
},
|
||||
"capabilities": {
|
||||
"supports_session_resume": false,
|
||||
"supports_shell": false,
|
||||
"supports_workspace_only": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,32 +1,32 @@
|
||||
// Package discovery provides agent installation detection and discovery functionality.
|
||||
// It reads agent definitions from the unified agents.json configuration in the registry package.
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/kandev/kandev/internal/agent/registry"
|
||||
)
|
||||
|
||||
//go:embed agents.json
|
||||
var agentsFS embed.FS
|
||||
// OSPaths defines OS-specific paths for discovery (re-exported from registry for API compatibility)
|
||||
type OSPaths = registry.OSPaths
|
||||
|
||||
type OSPaths struct {
|
||||
Linux []string `json:"linux"`
|
||||
Windows []string `json:"windows"`
|
||||
MacOS []string `json:"macos"`
|
||||
}
|
||||
// Capabilities defines discovery-specific capabilities (re-exported from registry)
|
||||
type Capabilities = registry.DiscoveryCapabilities
|
||||
|
||||
type Capabilities struct {
|
||||
SupportsSessionResume bool `json:"supports_session_resume"`
|
||||
SupportsShell bool `json:"supports_shell"`
|
||||
SupportsWorkspaceOnly bool `json:"supports_workspace_only"`
|
||||
}
|
||||
// ModelConfig defines the model configuration for an agent (re-exported from registry)
|
||||
type ModelConfig = registry.ModelConfig
|
||||
|
||||
func (p OSPaths) ForOS(goos string) []string {
|
||||
// ModelEntry defines a single model available for an agent (re-exported from registry)
|
||||
type ModelEntry = registry.ModelEntry
|
||||
|
||||
// ForOS returns paths for the specified operating system
|
||||
func ForOS(p OSPaths, goos string) []string {
|
||||
switch goos {
|
||||
case "windows":
|
||||
return p.Windows
|
||||
@@ -37,6 +37,7 @@ func (p OSPaths) ForOS(goos string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
// KnownAgent represents an agent definition with discovery metadata
|
||||
type KnownAgent struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
@@ -44,12 +45,15 @@ type KnownAgent struct {
|
||||
MCPConfigPath OSPaths `json:"mcp_config_path"`
|
||||
InstallationPath OSPaths `json:"installation_path"`
|
||||
Capabilities Capabilities `json:"capabilities"`
|
||||
ModelConfig ModelConfig `json:"model_config"`
|
||||
}
|
||||
|
||||
// Config is the legacy structure for agent discovery configuration
|
||||
type Config struct {
|
||||
Agents []KnownAgent `json:"agents"`
|
||||
}
|
||||
|
||||
// Availability represents the result of detecting an agent's installation
|
||||
type Availability struct {
|
||||
Name string `json:"name"`
|
||||
SupportsMCP bool `json:"supports_mcp"`
|
||||
@@ -59,29 +63,53 @@ type Availability struct {
|
||||
MatchedPath string `json:"matched_path,omitempty"`
|
||||
}
|
||||
|
||||
// Adapter defines the interface for agent detection strategies
|
||||
type Adapter interface {
|
||||
Detect(ctx context.Context) (Availability, error)
|
||||
}
|
||||
|
||||
// Registry manages agent discovery adapters and definitions
|
||||
type Registry struct {
|
||||
adapters []Adapter
|
||||
definitions []KnownAgent
|
||||
}
|
||||
|
||||
// LoadRegistry creates a new discovery registry by loading agent definitions
|
||||
// from the unified agents.json configuration in the registry package.
|
||||
func LoadRegistry() (*Registry, error) {
|
||||
data, err := agentsFS.ReadFile("agents.json")
|
||||
agentConfigs, err := registry.GetAgentDefinitions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read agents config: %w", err)
|
||||
return nil, fmt.Errorf("load agent definitions: %w", err)
|
||||
}
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse agents config: %w", err)
|
||||
|
||||
definitions := make([]KnownAgent, 0, len(agentConfigs))
|
||||
adapters := make([]Adapter, 0, len(agentConfigs))
|
||||
|
||||
for _, agentCfg := range agentConfigs {
|
||||
// Convert registry.AgentTypeConfig to discovery.KnownAgent
|
||||
knownAgent := KnownAgent{
|
||||
Name: extractAgentName(agentCfg.ID),
|
||||
DisplayName: agentCfg.DisplayName,
|
||||
SupportsMCP: agentCfg.Discovery.SupportsMCP,
|
||||
MCPConfigPath: agentCfg.Discovery.MCPConfigPath,
|
||||
InstallationPath: agentCfg.Discovery.InstallationPath,
|
||||
Capabilities: agentCfg.Discovery.DiscoveryCapabilities,
|
||||
ModelConfig: agentCfg.ModelConfig,
|
||||
}
|
||||
if knownAgent.DisplayName == "" {
|
||||
knownAgent.DisplayName = agentCfg.Name
|
||||
}
|
||||
definitions = append(definitions, knownAgent)
|
||||
adapters = append(adapters, NewFilePresenceAdapter(knownAgent))
|
||||
}
|
||||
adapters := make([]Adapter, 0, len(cfg.Agents))
|
||||
for _, agent := range cfg.Agents {
|
||||
adapters = append(adapters, NewFilePresenceAdapter(agent))
|
||||
}
|
||||
return &Registry{adapters: adapters, definitions: cfg.Agents}, nil
|
||||
|
||||
return &Registry{adapters: adapters, definitions: definitions}, nil
|
||||
}
|
||||
|
||||
// extractAgentName extracts the short name from an agent ID (e.g., "auggie-agent" -> "auggie")
|
||||
func extractAgentName(id string) string {
|
||||
name := strings.TrimSuffix(id, "-agent")
|
||||
return name
|
||||
}
|
||||
|
||||
func (r *Registry) Definitions() []KnownAgent {
|
||||
@@ -113,8 +141,8 @@ func NewFilePresenceAdapter(def KnownAgent) *FilePresenceAdapter {
|
||||
|
||||
func (a *FilePresenceAdapter) Detect(ctx context.Context) (Availability, error) {
|
||||
_ = ctx
|
||||
paths := resolvePaths(a.definition.InstallationPath.ForOS(runtime.GOOS))
|
||||
mcpPaths := resolvePaths(a.definition.MCPConfigPath.ForOS(runtime.GOOS))
|
||||
paths := resolvePaths(ForOS(a.definition.InstallationPath, runtime.GOOS))
|
||||
mcpPaths := resolvePaths(ForOS(a.definition.MCPConfigPath, runtime.GOOS))
|
||||
available, matched := anyPathExists(paths)
|
||||
mcpPath := ""
|
||||
if len(mcpPaths) > 0 {
|
||||
|
||||
@@ -1220,5 +1220,3 @@ func (m *Manager) RespondToPermissionBySessionID(sessionID, pendingID, optionID
|
||||
|
||||
return m.RespondToPermission(execution.ID, pendingID, optionID, cancelled)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
agentctl "github.com/kandev/kandev/internal/agentctl/client"
|
||||
"github.com/kandev/kandev/internal/agent/registry"
|
||||
agentctl "github.com/kandev/kandev/internal/agentctl/client"
|
||||
v1 "github.com/kandev/kandev/pkg/api/v1"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"$schema": "./agents.schema.json",
|
||||
"version": "1.0",
|
||||
"agents": [
|
||||
{
|
||||
"id": "auggie-agent",
|
||||
"name": "Augment Coding Agent",
|
||||
"display_name": "Auggie",
|
||||
"description": "Auggie CLI-powered autonomous coding agent. Requires AUGMENT_SESSION_AUTH for authentication.",
|
||||
"enabled": true,
|
||||
"image": "kandev/augment-agent",
|
||||
"tag": "latest",
|
||||
"cmd": ["auggie", "--acp"],
|
||||
"working_dir": "/workspace",
|
||||
"required_env": ["AUGMENT_SESSION_AUTH"],
|
||||
"env": {},
|
||||
"mounts": [
|
||||
{"source": "{workspace}", "target": "/workspace", "read_only": false}
|
||||
],
|
||||
"resource_limits": {
|
||||
"memory_mb": 4096,
|
||||
"cpu_cores": 2.0,
|
||||
"timeout_seconds": 3600
|
||||
},
|
||||
"capabilities": ["code_generation", "code_review", "refactoring", "testing", "shell_execution"],
|
||||
"protocol": "acp",
|
||||
"protocol_config": {},
|
||||
"model_flag": "--model",
|
||||
"workspace_flag": "--workspace-root",
|
||||
"session_config": {
|
||||
"resume_via_acp": false,
|
||||
"resume_flag": "",
|
||||
"can_recover": false,
|
||||
"session_dir_template": "{home}/.augment/sessions",
|
||||
"session_dir_target": "/root/.augment/sessions"
|
||||
},
|
||||
"permission_config": {
|
||||
"permission_flag": "--permission",
|
||||
"tools_requiring_permission": [
|
||||
"launch-process",
|
||||
"save-file",
|
||||
"str-replace-editor",
|
||||
"remove-files"
|
||||
]
|
||||
},
|
||||
"discovery": {
|
||||
"supports_mcp": false,
|
||||
"mcp_config_path": {
|
||||
"linux": [],
|
||||
"windows": [],
|
||||
"macos": []
|
||||
},
|
||||
"installation_path": {
|
||||
"linux": ["~/.augment/.auggie.json"],
|
||||
"windows": ["~/.augment/.auggie.json"],
|
||||
"macos": ["~/.augment/.auggie.json"]
|
||||
},
|
||||
"discovery_capabilities": {
|
||||
"supports_session_resume": false,
|
||||
"supports_shell": false,
|
||||
"supports_workspace_only": false
|
||||
}
|
||||
},
|
||||
"model_config": {
|
||||
"default_model": "sonnet4.5",
|
||||
"available_models": [
|
||||
{
|
||||
"id": "sonnet4.5",
|
||||
"name": "Sonnet 4.5",
|
||||
"provider": "anthropic",
|
||||
"context_window": 200000,
|
||||
"is_default": true
|
||||
},
|
||||
{
|
||||
"id": "opus4.5",
|
||||
"name": "Claude Opus 4.5",
|
||||
"provider": "anthropic",
|
||||
"context_window": 200000,
|
||||
"is_default": false
|
||||
},
|
||||
{
|
||||
"id": "haiku4.5",
|
||||
"name": "Haiku 4.5",
|
||||
"provider": "anthropic",
|
||||
"context_window": 200000,
|
||||
"is_default": false
|
||||
},
|
||||
{
|
||||
"id": "sonnet4",
|
||||
"name": "Sonnet 4",
|
||||
"provider": "anthropic",
|
||||
"context_window": 200000,
|
||||
"is_default": false
|
||||
},
|
||||
{
|
||||
"id": "gpt5.1",
|
||||
"name": "GPT-5.1",
|
||||
"provider": "openai",
|
||||
"context_window": 128000,
|
||||
"is_default": false
|
||||
},
|
||||
{
|
||||
"id": "gpt5",
|
||||
"name": "GPT-5",
|
||||
"provider": "openai",
|
||||
"context_window": 128000,
|
||||
"is_default": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "codex-agent",
|
||||
"name": "OpenAI Codex Agent",
|
||||
"display_name": "Codex",
|
||||
"description": "OpenAI Codex CLI-powered autonomous coding agent using the Codex app-server protocol.",
|
||||
"enabled": true,
|
||||
"image": "kandev/codex-agent",
|
||||
"tag": "latest",
|
||||
"cmd": ["codex", "app-server"],
|
||||
"working_dir": "/workspace",
|
||||
"required_env": [],
|
||||
"env": {},
|
||||
"mounts": [
|
||||
{"source": "{workspace}", "target": "/workspace", "read_only": false}
|
||||
],
|
||||
"resource_limits": {
|
||||
"memory_mb": 4096,
|
||||
"cpu_cores": 2.0,
|
||||
"timeout_seconds": 3600
|
||||
},
|
||||
"capabilities": ["code_generation", "code_review", "refactoring", "testing", "shell_execution"],
|
||||
"protocol": "codex",
|
||||
"protocol_config": {},
|
||||
"model_flag": "--model",
|
||||
"workspace_flag": "",
|
||||
"session_config": {
|
||||
"resume_via_acp": true,
|
||||
"resume_flag": "",
|
||||
"can_recover": true,
|
||||
"session_dir_template": "{home}/.codex/sessions",
|
||||
"session_dir_target": "/root/.codex/sessions"
|
||||
},
|
||||
"permission_config": {
|
||||
"permission_flag": "",
|
||||
"tools_requiring_permission": []
|
||||
},
|
||||
"discovery": {
|
||||
"supports_mcp": false,
|
||||
"mcp_config_path": {
|
||||
"linux": [],
|
||||
"windows": [],
|
||||
"macos": []
|
||||
},
|
||||
"installation_path": {
|
||||
"linux": ["~/.codex/auth.json"],
|
||||
"windows": ["~/.codex/auth.json"],
|
||||
"macos": ["~/.codex/auth.json"]
|
||||
},
|
||||
"discovery_capabilities": {
|
||||
"supports_session_resume": false,
|
||||
"supports_shell": false,
|
||||
"supports_workspace_only": false
|
||||
}
|
||||
},
|
||||
"model_config": {
|
||||
"default_model": "gpt-5.2-codex",
|
||||
"available_models": [
|
||||
{
|
||||
"id": "gpt-5.2-codex",
|
||||
"name": "GPT-5.2 Codex",
|
||||
"provider": "openai",
|
||||
"context_window": 200000,
|
||||
"is_default": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.1-codex-max",
|
||||
"name": "GPT-5.1 Codex Max",
|
||||
"provider": "openai",
|
||||
"context_window": 200000,
|
||||
"is_default": false
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.1-codex-mini",
|
||||
"name": "GPT-5.1 Codex Mini",
|
||||
"provider": "openai",
|
||||
"context_window": 200000,
|
||||
"is_default": false
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.2",
|
||||
"name": "GPT-5.2",
|
||||
"provider": "openai",
|
||||
"context_window": 200000,
|
||||
"is_default": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
package registry
|
||||
|
||||
import "github.com/kandev/kandev/pkg/agent"
|
||||
|
||||
// boolPtr returns a pointer to a bool value
|
||||
func boolPtr(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
// DefaultAgents returns the default agent configurations
|
||||
func DefaultAgents() []*AgentTypeConfig {
|
||||
return []*AgentTypeConfig{
|
||||
{
|
||||
ID: "auggie-agent",
|
||||
Name: "Augment Coding Agent",
|
||||
Description: "Auggie CLI-powered autonomous coding agent. Requires AUGMENT_SESSION_AUTH for authentication.",
|
||||
Image: "kandev/augment-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"auggie", "--acp"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{"AUGMENT_SESSION_AUTH"},
|
||||
Env: map[string]string{},
|
||||
Mounts: []MountTemplate{
|
||||
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
|
||||
},
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
WorkspaceFlag: "--workspace-root",
|
||||
Protocol: agent.ProtocolACP,
|
||||
SessionConfig: SessionConfig{
|
||||
ResumeViaACP: false, // Auggie handles resume via CLI flag
|
||||
ResumeFlag: "--resume",
|
||||
CanRecover: boolPtr(false), // Auggie cannot recover sessions after backend restart
|
||||
SessionDirTemplate: "{home}/.augment/sessions",
|
||||
SessionDirTarget: "/root/.augment/sessions",
|
||||
},
|
||||
PermissionConfig: PermissionConfig{
|
||||
PermissionFlag: "--permission",
|
||||
ToolsRequiringPermission: []string{
|
||||
"launch-process",
|
||||
"save-file",
|
||||
"str-replace-editor",
|
||||
"remove-files",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "claude-agent",
|
||||
Name: "Claude Code Agent",
|
||||
Description: "Claude Code CLI-powered autonomous coding agent.",
|
||||
Image: "kandev/claude-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"claude", "--acp"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{},
|
||||
Env: map[string]string{},
|
||||
Mounts: []MountTemplate{
|
||||
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
|
||||
},
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
Protocol: agent.ProtocolACP,
|
||||
SessionConfig: SessionConfig{
|
||||
ResumeViaACP: true, // Claude uses ACP session/load for resume
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "codex-agent",
|
||||
Name: "OpenAI Codex Agent",
|
||||
Description: "OpenAI Codex CLI-powered autonomous coding agent using the Codex app-server protocol.",
|
||||
Image: "kandev/codex-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"codex", "app-server"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{},
|
||||
Env: map[string]string{},
|
||||
Mounts: []MountTemplate{
|
||||
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
|
||||
},
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
Protocol: agent.ProtocolCodex,
|
||||
SessionConfig: SessionConfig{
|
||||
ResumeViaACP: true, // Codex uses thread/resume via ACP
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "gemini-agent",
|
||||
Name: "Gemini CLI Agent",
|
||||
Description: "Google Gemini CLI-powered autonomous coding agent.",
|
||||
Image: "kandev/gemini-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"gemini", "--acp"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{},
|
||||
Env: map[string]string{},
|
||||
Mounts: []MountTemplate{
|
||||
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
|
||||
},
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
Protocol: agent.ProtocolACP,
|
||||
SessionConfig: SessionConfig{
|
||||
ResumeViaACP: true, // Gemini uses ACP session/load for resume
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "opencode-agent",
|
||||
Name: "OpenCode Agent",
|
||||
Description: "OpenCode CLI-powered autonomous coding agent.",
|
||||
Image: "kandev/opencode-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"opencode", "--acp"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{},
|
||||
Env: map[string]string{},
|
||||
Mounts: []MountTemplate{
|
||||
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
|
||||
},
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
Protocol: agent.ProtocolACP,
|
||||
SessionConfig: SessionConfig{
|
||||
ResumeViaACP: true, // OpenCode uses ACP session/load for resume
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "copilot-agent",
|
||||
Name: "GitHub Copilot Agent",
|
||||
Description: "GitHub Copilot CLI-powered autonomous coding agent.",
|
||||
Image: "kandev/copilot-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"copilot", "--acp"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{},
|
||||
Env: map[string]string{},
|
||||
Mounts: []MountTemplate{
|
||||
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
|
||||
},
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
Protocol: agent.ProtocolACP,
|
||||
SessionConfig: SessionConfig{
|
||||
ResumeViaACP: true, // Copilot uses ACP session/load for resume
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -15,6 +16,15 @@ import (
|
||||
v1 "github.com/kandev/kandev/pkg/api/v1"
|
||||
)
|
||||
|
||||
//go:embed agents.json
|
||||
var agentsFS embed.FS
|
||||
|
||||
// agentsConfig is the structure of the agents.json file
|
||||
type agentsConfig struct {
|
||||
Version string `json:"version"`
|
||||
Agents []*AgentTypeConfig `json:"agents"`
|
||||
}
|
||||
|
||||
// AgentTypeConfig holds configuration for an agent type
|
||||
type AgentTypeConfig struct {
|
||||
ID string `json:"id"`
|
||||
@@ -43,6 +53,15 @@ type AgentTypeConfig struct {
|
||||
|
||||
// Permission configuration
|
||||
PermissionConfig PermissionConfig `json:"permission_config,omitempty"` // How to handle tool permissions
|
||||
|
||||
// Discovery configuration (for detecting agent installation)
|
||||
Discovery DiscoveryConfig `json:"discovery,omitempty"` // Discovery metadata for agent detection
|
||||
|
||||
// Model configuration
|
||||
ModelConfig ModelConfig `json:"model_config,omitempty"` // Available models and default model
|
||||
|
||||
// Display name for UI (shorter than Name)
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
}
|
||||
|
||||
// SessionConfig defines how session resumption is handled for an agent type
|
||||
@@ -107,6 +126,43 @@ type ResourceLimits struct {
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
}
|
||||
|
||||
// ModelConfig defines the model configuration for an agent
|
||||
type ModelConfig struct {
|
||||
DefaultModel string `json:"default_model"`
|
||||
AvailableModels []ModelEntry `json:"available_models"`
|
||||
}
|
||||
|
||||
// ModelEntry defines a single model available for an agent
|
||||
type ModelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
ContextWindow int `json:"context_window"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// OSPaths defines OS-specific paths for discovery
|
||||
type OSPaths struct {
|
||||
Linux []string `json:"linux"`
|
||||
Windows []string `json:"windows"`
|
||||
MacOS []string `json:"macos"`
|
||||
}
|
||||
|
||||
// DiscoveryCapabilities defines discovery-specific capabilities
|
||||
type DiscoveryCapabilities struct {
|
||||
SupportsSessionResume bool `json:"supports_session_resume"`
|
||||
SupportsShell bool `json:"supports_shell"`
|
||||
SupportsWorkspaceOnly bool `json:"supports_workspace_only"`
|
||||
}
|
||||
|
||||
// DiscoveryConfig holds discovery-related configuration for an agent
|
||||
type DiscoveryConfig struct {
|
||||
SupportsMCP bool `json:"supports_mcp"`
|
||||
MCPConfigPath OSPaths `json:"mcp_config_path"`
|
||||
InstallationPath OSPaths `json:"installation_path"`
|
||||
DiscoveryCapabilities DiscoveryCapabilities `json:"discovery_capabilities"`
|
||||
}
|
||||
|
||||
// Registry manages agent type configurations
|
||||
type Registry struct {
|
||||
agents map[string]*AgentTypeConfig
|
||||
@@ -311,3 +367,54 @@ func ValidateConfig(config *AgentTypeConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultAgents returns the default agent configurations loaded from agents.json
|
||||
func DefaultAgents() []*AgentTypeConfig {
|
||||
agents, err := loadAgentsFromJSON()
|
||||
if err != nil {
|
||||
// Fall back to empty list if loading fails (should not happen with embedded file)
|
||||
return []*AgentTypeConfig{}
|
||||
}
|
||||
return agents
|
||||
}
|
||||
|
||||
// loadAgentsFromJSON loads agent configurations from the embedded agents.json file
|
||||
func loadAgentsFromJSON() ([]*AgentTypeConfig, error) {
|
||||
data, err := agentsFS.ReadFile("agents.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read agents config: %w", err)
|
||||
}
|
||||
|
||||
var cfg agentsConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse agents config: %w", err)
|
||||
}
|
||||
|
||||
// Post-process to convert protocol strings to agent.Protocol type
|
||||
for _, agentCfg := range cfg.Agents {
|
||||
agentCfg.Protocol = parseProtocol(string(agentCfg.Protocol))
|
||||
}
|
||||
|
||||
return cfg.Agents, nil
|
||||
}
|
||||
|
||||
// parseProtocol converts a protocol string to agent.Protocol
|
||||
func parseProtocol(p string) agent.Protocol {
|
||||
switch p {
|
||||
case "acp":
|
||||
return agent.ProtocolACP
|
||||
case "rest":
|
||||
return agent.ProtocolREST
|
||||
case "mcp":
|
||||
return agent.ProtocolMCP
|
||||
case "codex":
|
||||
return agent.ProtocolCodex
|
||||
default:
|
||||
return agent.ProtocolACP // Default to ACP
|
||||
}
|
||||
}
|
||||
|
||||
// GetAgentDefinitions returns the raw agent definitions from agents.json for discovery.
|
||||
// This provides access to discovery metadata that may be needed by the discovery package.
|
||||
func GetAgentDefinitions() ([]*AgentTypeConfig, error) {
|
||||
return loadAgentsFromJSON()
|
||||
}
|
||||
|
||||
@@ -89,6 +89,18 @@ func (c *Controller) ListAvailableAgents(ctx context.Context) (*dto.ListAvailabl
|
||||
if displayName == "" {
|
||||
displayName = def.Name
|
||||
}
|
||||
// Convert model entries
|
||||
modelEntries := make([]dto.ModelEntryDTO, 0, len(def.ModelConfig.AvailableModels))
|
||||
for _, model := range def.ModelConfig.AvailableModels {
|
||||
modelEntries = append(modelEntries, dto.ModelEntryDTO{
|
||||
ID: model.ID,
|
||||
Name: model.Name,
|
||||
Provider: model.Provider,
|
||||
ContextWindow: model.ContextWindow,
|
||||
IsDefault: model.IsDefault,
|
||||
})
|
||||
}
|
||||
|
||||
payload = append(payload, dto.AvailableAgentDTO{
|
||||
Name: availability.Name,
|
||||
DisplayName: displayName,
|
||||
@@ -102,6 +114,10 @@ func (c *Controller) ListAvailableAgents(ctx context.Context) (*dto.ListAvailabl
|
||||
SupportsShell: def.Capabilities.SupportsShell,
|
||||
SupportsWorkspaceOnly: def.Capabilities.SupportsWorkspaceOnly,
|
||||
},
|
||||
ModelConfig: dto.ModelConfigDTO{
|
||||
DefaultModel: def.ModelConfig.DefaultModel,
|
||||
AvailableModels: modelEntries,
|
||||
},
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,6 +51,19 @@ type AgentCapabilitiesDTO struct {
|
||||
SupportsWorkspaceOnly bool `json:"supports_workspace_only"`
|
||||
}
|
||||
|
||||
type ModelEntryDTO struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
ContextWindow int `json:"context_window"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
type ModelConfigDTO struct {
|
||||
DefaultModel string `json:"default_model"`
|
||||
AvailableModels []ModelEntryDTO `json:"available_models"`
|
||||
}
|
||||
|
||||
type AvailableAgentDTO struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
@@ -60,6 +73,7 @@ type AvailableAgentDTO struct {
|
||||
Available bool `json:"available"`
|
||||
MatchedPath string `json:"matched_path,omitempty"`
|
||||
Capabilities AgentCapabilitiesDTO `json:"capabilities"`
|
||||
ModelConfig ModelConfigDTO `json:"model_config"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ type CreateInstanceRequest struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
WorkspacePath string `json:"workspace_path"`
|
||||
AgentCommand string `json:"agent_command,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"` // Protocol adapter to use (acp, rest, mcp)
|
||||
Protocol string `json:"protocol,omitempty"` // Protocol adapter to use (acp, rest, mcp, codex)
|
||||
WorkspaceFlag string `json:"workspace_flag,omitempty"` // CLI flag for workspace path (e.g., "--workspace-root")
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
AutoStart bool `json:"auto_start,omitempty"`
|
||||
|
||||
@@ -109,6 +109,10 @@ type InstanceConfig struct {
|
||||
|
||||
// LogFormat for this instance
|
||||
LogFormat string
|
||||
|
||||
// AgentType identifies the agent (e.g., "auggie", "codex", "claude")
|
||||
// Used for agent-specific adapter selection
|
||||
AgentType string
|
||||
}
|
||||
|
||||
// Load loads the configuration from environment variables.
|
||||
@@ -168,6 +172,9 @@ func (c *Config) NewInstanceConfig(port int, overrides *InstanceOverrides) *Inst
|
||||
if overrides.ApprovalPolicy != "" {
|
||||
cfg.ApprovalPolicy = overrides.ApprovalPolicy
|
||||
}
|
||||
if overrides.AgentType != "" {
|
||||
cfg.AgentType = overrides.AgentType
|
||||
}
|
||||
}
|
||||
|
||||
// Parse agent command into args
|
||||
@@ -189,6 +196,7 @@ type InstanceOverrides struct {
|
||||
AutoStart *bool
|
||||
Env []string
|
||||
ApprovalPolicy string
|
||||
AgentType string
|
||||
}
|
||||
|
||||
// ParseCommand splits a command string into arguments
|
||||
|
||||
@@ -52,7 +52,7 @@ type CreateRequest struct {
|
||||
// AgentCommand is an optional command to start the agent. If empty, a default is used.
|
||||
AgentCommand string `json:"agent_command,omitempty"`
|
||||
|
||||
// Protocol is the protocol adapter to use (acp, codex). If empty, default is used.
|
||||
// Protocol is the protocol adapter to use (acp, codex, auggie). If empty, default is used.
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
|
||||
// WorkspaceFlag is the CLI flag for workspace path (e.g., "--workspace-root").
|
||||
|
||||
@@ -24,10 +24,9 @@ func NewController(svc *orchestrator.Service) *Controller {
|
||||
func (c *Controller) GetStatus(ctx context.Context, req dto.GetStatusRequest) (dto.StatusResponse, error) {
|
||||
status := c.service.GetStatus()
|
||||
return dto.StatusResponse{
|
||||
Running: status.Running,
|
||||
ActiveAgents: status.ActiveAgents,
|
||||
QueuedTasks: status.QueuedTasks,
|
||||
MaxConcurrent: 0, // Not available from service Status; would need scheduler access
|
||||
Running: status.Running,
|
||||
ActiveAgents: status.ActiveAgents,
|
||||
QueuedTasks: status.QueuedTasks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,9 @@ type GetStatusRequest struct{}
|
||||
|
||||
// StatusResponse is the response for orchestrator.status
|
||||
type StatusResponse struct {
|
||||
Running bool `json:"running"`
|
||||
ActiveAgents int `json:"active_agents"`
|
||||
QueuedTasks int `json:"queued_tasks"`
|
||||
MaxConcurrent int `json:"max_concurrent"`
|
||||
Running bool `json:"running"`
|
||||
ActiveAgents int `json:"active_agents"`
|
||||
QueuedTasks int `json:"queued_tasks"`
|
||||
}
|
||||
|
||||
// GetQueueRequest is the request for orchestrator.queue
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrMaxConcurrentReached = errors.New("maximum concurrent executions reached")
|
||||
ErrNoAgentProfileID = errors.New("task has no agent_profile_id configured")
|
||||
ErrExecutionNotFound = errors.New("execution not found")
|
||||
ErrExecutionAlreadyRunning = errors.New("execution already running")
|
||||
@@ -146,7 +145,6 @@ type Executor struct {
|
||||
logger *logger.Logger
|
||||
|
||||
// Configuration
|
||||
maxConcurrent int
|
||||
retryLimit int
|
||||
retryDelay time.Duration
|
||||
worktreeEnabled bool // Whether to use Git worktrees for agent isolation
|
||||
@@ -154,7 +152,6 @@ type Executor struct {
|
||||
|
||||
// ExecutorConfig holds configuration for the Executor
|
||||
type ExecutorConfig struct {
|
||||
MaxConcurrent int // Maximum concurrent agent executions
|
||||
WorktreeEnabled bool // Whether to use Git worktrees for agent isolation
|
||||
ShellPrefs ShellPreferenceProvider
|
||||
}
|
||||
@@ -165,16 +162,11 @@ type ShellPreferenceProvider interface {
|
||||
|
||||
// NewExecutor creates a new executor
|
||||
func NewExecutor(agentManager AgentManagerClient, repo repository.Repository, log *logger.Logger, cfg ExecutorConfig) *Executor {
|
||||
maxConcurrent := cfg.MaxConcurrent
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 5 // default
|
||||
}
|
||||
return &Executor{
|
||||
agentManager: agentManager,
|
||||
repo: repo,
|
||||
shellPrefs: cfg.ShellPrefs,
|
||||
logger: log.WithFields(zap.String("component", "executor")),
|
||||
maxConcurrent: maxConcurrent,
|
||||
retryLimit: 3,
|
||||
retryDelay: 5 * time.Second,
|
||||
worktreeEnabled: cfg.WorktreeEnabled,
|
||||
@@ -223,14 +215,6 @@ func (e *Executor) Execute(ctx context.Context, task *v1.Task) (*TaskExecution,
|
||||
// ExecuteWithProfile starts agent execution for a task using an explicit agent profile.
|
||||
// The prompt parameter is the initial prompt to send to the agent.
|
||||
func (e *Executor) ExecuteWithProfile(ctx context.Context, task *v1.Task, agentProfileID string, prompt string) (*TaskExecution, error) {
|
||||
// Check if max concurrent limit is reached
|
||||
if !e.CanExecute() {
|
||||
e.logger.Warn("max concurrent executions reached",
|
||||
zap.Int("max", e.maxConcurrent),
|
||||
zap.Int("current", e.ActiveCount()))
|
||||
return nil, ErrMaxConcurrentReached
|
||||
}
|
||||
|
||||
if agentProfileID == "" {
|
||||
e.logger.Error("task has no agent_profile_id configured", zap.String("task_id", task.ID))
|
||||
return nil, ErrNoAgentProfileID
|
||||
@@ -465,13 +449,6 @@ func (e *Executor) ResumeSession(ctx context.Context, session *models.TaskSessio
|
||||
return nil, ErrExecutionNotFound
|
||||
}
|
||||
|
||||
if !e.CanExecute() {
|
||||
e.logger.Warn("max concurrent executions reached",
|
||||
zap.Int("max", e.maxConcurrent),
|
||||
zap.Int("current", e.ActiveCount()))
|
||||
return nil, ErrMaxConcurrentReached
|
||||
}
|
||||
|
||||
if session.AgentProfileID == "" {
|
||||
e.logger.Error("task session has no agent_profile_id configured",
|
||||
zap.String("task_id", session.TaskID),
|
||||
@@ -896,9 +873,10 @@ func (e *Executor) ActiveCount() int {
|
||||
return len(sessions)
|
||||
}
|
||||
|
||||
// CanExecute returns true if there's capacity for another execution
|
||||
// CanExecute returns true if there's capacity for another execution.
|
||||
// Currently always returns true as there is no concurrent execution limit.
|
||||
func (e *Executor) CanExecute() bool {
|
||||
return e.ActiveCount() < e.maxConcurrent
|
||||
return true
|
||||
}
|
||||
|
||||
// UpdateProgressBySession updates the progress of an execution by session ID
|
||||
|
||||
@@ -25,7 +25,6 @@ var (
|
||||
// SchedulerConfig holds scheduler configuration
|
||||
type SchedulerConfig struct {
|
||||
ProcessInterval time.Duration // How often to process the queue
|
||||
MaxConcurrent int // Max concurrent agent executions
|
||||
RetryLimit int // Max retries for failed tasks
|
||||
RetryDelay time.Duration // Delay between retries
|
||||
}
|
||||
@@ -34,7 +33,6 @@ type SchedulerConfig struct {
|
||||
func DefaultSchedulerConfig() SchedulerConfig {
|
||||
return SchedulerConfig{
|
||||
ProcessInterval: 5 * time.Second,
|
||||
MaxConcurrent: 5,
|
||||
RetryLimit: 3,
|
||||
RetryDelay: 30 * time.Second,
|
||||
}
|
||||
@@ -50,7 +48,6 @@ type TaskRepository interface {
|
||||
type QueueStatus struct {
|
||||
QueuedTasks int
|
||||
ActiveExecutions int
|
||||
MaxConcurrent int
|
||||
TotalProcessed int64
|
||||
TotalFailed int64
|
||||
}
|
||||
@@ -107,8 +104,7 @@ func (s *Scheduler) Start(ctx context.Context) error {
|
||||
s.mu.Unlock()
|
||||
|
||||
s.logger.Info("scheduler starting",
|
||||
zap.Duration("process_interval", s.config.ProcessInterval),
|
||||
zap.Int("max_concurrent", s.config.MaxConcurrent))
|
||||
zap.Duration("process_interval", s.config.ProcessInterval))
|
||||
|
||||
s.wg.Add(1)
|
||||
go s.processLoop(ctx)
|
||||
@@ -162,7 +158,6 @@ func (s *Scheduler) GetQueueStatus() *QueueStatus {
|
||||
return &QueueStatus{
|
||||
QueuedTasks: s.queue.Len(),
|
||||
ActiveExecutions: s.executor.ActiveCount(),
|
||||
MaxConcurrent: s.config.MaxConcurrent,
|
||||
TotalProcessed: atomic.LoadInt64(&s.totalProcessed),
|
||||
TotalFailed: atomic.LoadInt64(&s.totalFailed),
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func createTestLogger() *logger.Logger {
|
||||
return log
|
||||
}
|
||||
|
||||
func createTestExecutor(t *testing.T, agentMgr *mockAgentManager, log *logger.Logger, maxConcurrent int) *executor.Executor {
|
||||
func createTestExecutor(t *testing.T, agentMgr *mockAgentManager, log *logger.Logger) *executor.Executor {
|
||||
tmpDir := t.TempDir()
|
||||
dbConn, err := db.OpenSQLite(filepath.Join(tmpDir, "test.db"))
|
||||
if err != nil {
|
||||
@@ -150,7 +150,6 @@ func createTestExecutor(t *testing.T, agentMgr *mockAgentManager, log *logger.Lo
|
||||
}
|
||||
})
|
||||
cfg := executor.ExecutorConfig{
|
||||
MaxConcurrent: maxConcurrent,
|
||||
WorktreeEnabled: true,
|
||||
}
|
||||
return executor.NewExecutor(agentMgr, repo, log, cfg)
|
||||
@@ -172,7 +171,7 @@ func TestNewScheduler(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
|
||||
@@ -190,9 +189,6 @@ func TestDefaultSchedulerConfig(t *testing.T) {
|
||||
if cfg.ProcessInterval != 5*time.Second {
|
||||
t.Errorf("expected ProcessInterval = 5s, got %v", cfg.ProcessInterval)
|
||||
}
|
||||
if cfg.MaxConcurrent != 5 {
|
||||
t.Errorf("expected MaxConcurrent = 5, got %d", cfg.MaxConcurrent)
|
||||
}
|
||||
if cfg.RetryLimit != 3 {
|
||||
t.Errorf("expected RetryLimit = 3, got %d", cfg.RetryLimit)
|
||||
}
|
||||
@@ -205,7 +201,7 @@ func TestStartStop(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
|
||||
@@ -231,7 +227,7 @@ func TestStartAlreadyRunning(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
|
||||
@@ -251,7 +247,7 @@ func TestStopNotRunning(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
|
||||
@@ -266,7 +262,7 @@ func TestEnqueueTask(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
|
||||
@@ -286,7 +282,7 @@ func TestRemoveTask(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
|
||||
@@ -308,7 +304,7 @@ func TestRemoveNonExistentTask(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
|
||||
@@ -323,11 +319,10 @@ func TestGetQueueStatus(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
cfg := DefaultSchedulerConfig()
|
||||
cfg.MaxConcurrent = 10
|
||||
s := NewScheduler(q, exec, taskRepo, log, cfg)
|
||||
|
||||
_ = s.EnqueueTask(createTestTask("task-1", 5))
|
||||
@@ -337,9 +332,6 @@ func TestGetQueueStatus(t *testing.T) {
|
||||
if status.QueuedTasks != 2 {
|
||||
t.Errorf("expected QueuedTasks = 2, got %d", status.QueuedTasks)
|
||||
}
|
||||
if status.MaxConcurrent != 10 {
|
||||
t.Errorf("expected MaxConcurrent = 10, got %d", status.MaxConcurrent)
|
||||
}
|
||||
if status.ActiveExecutions != 0 {
|
||||
t.Errorf("expected ActiveExecutions = 0, got %d", status.ActiveExecutions)
|
||||
}
|
||||
@@ -349,7 +341,7 @@ func TestHandleTaskCompleted(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
|
||||
@@ -378,7 +370,7 @@ func TestPriorityOrdering(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
|
||||
@@ -399,7 +391,7 @@ func TestIsRunning(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
|
||||
@@ -423,7 +415,7 @@ func TestSchedulerWithContextCancellation(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
cfg := DefaultSchedulerConfig()
|
||||
@@ -452,7 +444,7 @@ func TestEnqueueDuplicateTask(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
|
||||
@@ -470,7 +462,7 @@ func TestRetryTaskExceedsLimit(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
cfg := DefaultSchedulerConfig()
|
||||
@@ -504,7 +496,7 @@ func TestRetryTaskNotFound(t *testing.T) {
|
||||
q := queue.NewTaskQueue(100)
|
||||
agentMgr := newMockAgentManager()
|
||||
log := createTestLogger()
|
||||
exec := createTestExecutor(t, agentMgr, log, 5)
|
||||
exec := createTestExecutor(t, agentMgr, log)
|
||||
taskRepo := newTestTaskRepository()
|
||||
|
||||
cfg := DefaultSchedulerConfig()
|
||||
|
||||
@@ -114,7 +114,6 @@ func NewService(
|
||||
|
||||
// Create the executor with the agent manager client and repository for persistent sessions
|
||||
execCfg := executor.ExecutorConfig{
|
||||
MaxConcurrent: cfg.Scheduler.MaxConcurrent,
|
||||
WorktreeEnabled: cfg.WorktreeEnabled,
|
||||
ShellPrefs: shellPrefs,
|
||||
}
|
||||
|
||||
@@ -8,6 +8,13 @@ import { Button } from '@kandev/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
|
||||
import { Input } from '@kandev/ui/input';
|
||||
import { Label } from '@kandev/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@kandev/ui/select';
|
||||
import { Separator } from '@kandev/ui/separator';
|
||||
import { Switch } from '@kandev/ui/switch';
|
||||
import { Textarea } from '@kandev/ui/textarea';
|
||||
@@ -82,6 +89,12 @@ function AgentSetupForm({
|
||||
const resolveDisplayName = (name: string) =>
|
||||
availableAgents.find((item) => item.name === name)?.display_name ?? '';
|
||||
|
||||
// Get model config for the current agent
|
||||
const currentAgentModelConfig = useMemo(() => {
|
||||
const agent = availableAgents.find((item) => item.name === draftAgent.name);
|
||||
return agent?.model_config ?? { default_model: '', available_models: [] };
|
||||
}, [availableAgents, draftAgent.name]);
|
||||
|
||||
const isProfileDirty = (draft: DraftProfile, saved?: AgentProfile) => {
|
||||
if (!saved) return true;
|
||||
return (
|
||||
@@ -311,13 +324,34 @@ function AgentSetupForm({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Model</Label>
|
||||
<Input
|
||||
value={profile.model}
|
||||
onChange={(event) =>
|
||||
handleProfileChange(profile.id, { model: event.target.value })
|
||||
}
|
||||
placeholder="model-id"
|
||||
/>
|
||||
{currentAgentModelConfig.available_models.length > 0 ? (
|
||||
<Select
|
||||
value={profile.model || currentAgentModelConfig.default_model}
|
||||
onValueChange={(value) => handleProfileChange(profile.id, { model: value })}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{currentAgentModelConfig.available_models.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
{model.is_default && (
|
||||
<span className="ml-2 text-muted-foreground">(default)</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={profile.model}
|
||||
onChange={(event) =>
|
||||
handleProfileChange(profile.id, { model: event.target.value })
|
||||
}
|
||||
placeholder="model-id"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -9,21 +9,24 @@ import { Button } from '@kandev/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
|
||||
import { Input } from '@kandev/ui/input';
|
||||
import { Label } from '@kandev/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
|
||||
import { Separator } from '@kandev/ui/separator';
|
||||
import { Switch } from '@kandev/ui/switch';
|
||||
import { Textarea } from '@kandev/ui/textarea';
|
||||
import { useToast } from '@/components/toast-provider';
|
||||
import { UnsavedChangesBadge, UnsavedSaveButton } from '@/components/settings/unsaved-indicator';
|
||||
import { deleteAgentProfileAction, updateAgentProfileAction } from '@/app/actions/agents';
|
||||
import type { Agent, AgentProfile } from '@/lib/types/http';
|
||||
import type { Agent, AgentProfile, ModelConfig } from '@/lib/types/http';
|
||||
import { useAppStore } from '@/components/state-provider';
|
||||
import { useAvailableAgents } from '@/hooks/use-available-agents';
|
||||
|
||||
type ProfileEditorProps = {
|
||||
agent: Agent;
|
||||
profile: AgentProfile;
|
||||
modelConfig: ModelConfig;
|
||||
};
|
||||
|
||||
function ProfileEditor({ agent, profile }: ProfileEditorProps) {
|
||||
function ProfileEditor({ agent, profile, modelConfig }: ProfileEditorProps) {
|
||||
const { toast } = useToast();
|
||||
const settingsAgents = useAppStore((state) => state.settingsAgents.items);
|
||||
const setSettingsAgents = useAppStore((state) => state.setSettingsAgents);
|
||||
@@ -151,11 +154,32 @@ function ProfileEditor({ agent, profile }: ProfileEditorProps) {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Model</Label>
|
||||
<Input
|
||||
value={draft.model}
|
||||
onChange={(event) => setDraft({ ...draft, model: event.target.value })}
|
||||
placeholder="model-id"
|
||||
/>
|
||||
{modelConfig.available_models.length > 0 ? (
|
||||
<Select
|
||||
value={draft.model || modelConfig.default_model}
|
||||
onValueChange={(value) => setDraft({ ...draft, model: value })}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{modelConfig.available_models.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
{model.is_default && (
|
||||
<span className="ml-2 text-muted-foreground">(default)</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={draft.model}
|
||||
onChange={(event) => setDraft({ ...draft, model: event.target.value })}
|
||||
placeholder="model-id"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -225,6 +249,7 @@ export default function AgentProfilePage() {
|
||||
const agentKey = decodeURIComponent(agentParam ?? '');
|
||||
const profileId = profileParam ?? '';
|
||||
const settingsAgents = useAppStore((state) => state.settingsAgents.items);
|
||||
const availableAgents = useAvailableAgents().items;
|
||||
|
||||
const agent = useMemo(() => {
|
||||
return settingsAgents.find((item) => item.name === agentKey) ?? null;
|
||||
@@ -234,6 +259,11 @@ export default function AgentProfilePage() {
|
||||
return agent?.profiles.find((item) => item.id === profileId) ?? null;
|
||||
}, [agent?.profiles, profileId]);
|
||||
|
||||
const modelConfig = useMemo(() => {
|
||||
const availableAgent = availableAgents.find((item) => item.name === agent?.name);
|
||||
return availableAgent?.model_config ?? { default_model: '', available_models: [] };
|
||||
}, [availableAgents, agent?.name]);
|
||||
|
||||
if (!agent || !profile) {
|
||||
return (
|
||||
<Card>
|
||||
@@ -252,6 +282,7 @@ export default function AgentProfilePage() {
|
||||
key={profile.id}
|
||||
agent={agent}
|
||||
profile={profile}
|
||||
modelConfig={modelConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
} from '@kandev/ui/sidebar';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@kandev/ui/collapsible';
|
||||
import { useAppStore } from '@/components/state-provider';
|
||||
import { useAvailableAgents } from '@/hooks/use-available-agents';
|
||||
|
||||
export function SettingsAppSidebar() {
|
||||
const pathname = usePathname();
|
||||
@@ -39,7 +38,6 @@ export function SettingsAppSidebar() {
|
||||
const environments = useAppStore((state) => state.environments.items);
|
||||
const executors = useAppStore((state) => state.executors.items);
|
||||
const agents = useAppStore((state) => state.settingsAgents.items);
|
||||
const availableAgents = useAvailableAgents().items;
|
||||
|
||||
return (
|
||||
<Sidebar variant="inset">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import type { AppState } from '@/lib/state/store';
|
||||
import { useAppStoreApi } from '@/components/state-provider';
|
||||
|
||||
@@ -11,7 +11,10 @@ type StateHydratorProps = {
|
||||
export function StateHydrator({ initialState }: StateHydratorProps) {
|
||||
const store = useAppStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
// Use useLayoutEffect to hydrate state synchronously before child effects run.
|
||||
// This ensures SSR-hydrated data is available before hooks like useSettingsData
|
||||
// decide whether to fetch data.
|
||||
useLayoutEffect(() => {
|
||||
if (Object.keys(initialState).length) {
|
||||
store.getState().hydrate(initialState);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { IconBrain, IconListCheck, IconLoader2, IconPaperclip } from '@tabler/icons-react';
|
||||
import { IconBrain, IconCpu, IconListCheck, IconLoader2, IconPaperclip } from '@tabler/icons-react';
|
||||
import { Button } from '@kandev/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -24,6 +24,7 @@ import { useLazyLoadMessages } from '@/hooks/use-lazy-load-messages';
|
||||
import { useSession } from '@/hooks/use-session';
|
||||
import { useTask } from '@/hooks/use-task';
|
||||
import { useSessionMessages } from '@/hooks/use-session-messages';
|
||||
import { useSettingsData } from '@/hooks/use-settings-data';
|
||||
import { MessageRenderer } from '@/components/task/chat/message-renderer';
|
||||
import { RunningIndicator } from '@/components/task/chat/messages/running-indicator';
|
||||
import { TodoSummary } from '@/components/task/chat/todo-summary';
|
||||
@@ -65,6 +66,22 @@ export function TaskChatPanel({
|
||||
const isWorking = isStarting || session?.state === 'RUNNING';
|
||||
const isAgentBusy = session?.state === 'CREATED' || session?.state === 'RUNNING';
|
||||
|
||||
// Ensure agent profile data is loaded (may not be hydrated from SSR in all navigation paths)
|
||||
useSettingsData(true);
|
||||
|
||||
// Get model from agent profile using agent_profile_id
|
||||
const settingsAgents = useAppStore((state) => state.settingsAgents.items);
|
||||
const sessionProfile = useMemo(() => {
|
||||
if (!session?.agent_profile_id) return null;
|
||||
for (const agent of settingsAgents) {
|
||||
const profile = agent.profiles.find((p) => p.id === session.agent_profile_id);
|
||||
if (profile) return profile;
|
||||
}
|
||||
return null;
|
||||
}, [session?.agent_profile_id, settingsAgents]);
|
||||
|
||||
const sessionModel = sessionProfile?.model ?? null;
|
||||
|
||||
// Fetch messages for this session
|
||||
const { messages, isLoading: messagesLoading } = useSessionMessages(resolvedSessionId);
|
||||
const isInitialLoading = messagesLoading && messages.length === 0;
|
||||
@@ -305,10 +322,17 @@ export function TaskChatPanel({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Running indicator - shows agent state */}
|
||||
{/* Session info - shows agent state and model */}
|
||||
{session?.state && (
|
||||
<div className="mt-2">
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<RunningIndicator state={session.state} />
|
||||
{/* Model indicator */}
|
||||
{sessionModel && (
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-muted/50 text-xs text-muted-foreground border border-border/50">
|
||||
<IconCpu className="h-3 w-3" />
|
||||
<span className="font-medium">{sessionModel}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -376,6 +376,19 @@ export type AgentCapabilities = {
|
||||
supports_workspace_only: boolean;
|
||||
};
|
||||
|
||||
export type ModelEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
context_window: number;
|
||||
is_default: boolean;
|
||||
};
|
||||
|
||||
export type ModelConfig = {
|
||||
default_model: string;
|
||||
available_models: ModelEntry[];
|
||||
};
|
||||
|
||||
export type AvailableAgent = {
|
||||
name: string;
|
||||
display_name: string;
|
||||
@@ -385,6 +398,7 @@ export type AvailableAgent = {
|
||||
available: boolean;
|
||||
matched_path?: string | null;
|
||||
capabilities: AgentCapabilities;
|
||||
model_config: ModelConfig;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user