mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-12 23:27:30 +08:00
Refine agent config schema and prompt file loading
This commit is contained in:
@@ -21,28 +21,30 @@
|
||||
"max_summary_chars": 6000,
|
||||
"max_transcript_chars": 20000
|
||||
},
|
||||
"runtime_control": {
|
||||
"intent_max_input_chars": 1200,
|
||||
"autolearn_max_rounds_without_user": 200,
|
||||
"execution": {
|
||||
"run_state_ttl_seconds": 1800,
|
||||
"run_state_max": 500,
|
||||
"tool_parallel_safe_names": ["read_file", "list_files", "find_files", "grep_files", "memory_search", "web_search", "repo_map", "system_info"],
|
||||
"tool_max_parallel_calls": 2,
|
||||
"system_summary": {
|
||||
"marker": "## System Task Summary",
|
||||
"completed_prefix": "- Completed:",
|
||||
"changes_prefix": "- Changes:",
|
||||
"outcome_prefix": "- Outcome:",
|
||||
"completed_title": "Completed Actions",
|
||||
"changes_title": "Change Summaries",
|
||||
"outcomes_title": "Execution Outcomes"
|
||||
}
|
||||
"tool_max_parallel_calls": 2
|
||||
},
|
||||
"summary_policy": {
|
||||
"marker": "## System Task Summary",
|
||||
"completed_prefix": "- Completed:",
|
||||
"changes_prefix": "- Changes:",
|
||||
"outcome_prefix": "- Outcome:",
|
||||
"completed_title": "Completed Actions",
|
||||
"changes_title": "Change Summaries",
|
||||
"outcomes_title": "Execution Outcomes"
|
||||
}
|
||||
},
|
||||
"router": {
|
||||
"enabled": true,
|
||||
"main_agent_id": "main",
|
||||
"strategy": "rules_first",
|
||||
"policy": {
|
||||
"intent_max_input_chars": 1200,
|
||||
"max_rounds_without_user": 200
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"agent_id": "coder",
|
||||
@@ -73,7 +75,7 @@
|
||||
"type": "router",
|
||||
"display_name": "Main Agent",
|
||||
"role": "orchestrator",
|
||||
"system_prompt": "你负责消息路由、任务拆解、仲裁与结果汇总。",
|
||||
"system_prompt_file": "agents/main/AGENT.md",
|
||||
"memory_namespace": "main",
|
||||
"accept_from": ["user", "coder", "tester"],
|
||||
"can_talk_to": ["coder", "tester"],
|
||||
@@ -94,7 +96,7 @@
|
||||
"type": "worker",
|
||||
"display_name": "Code Agent",
|
||||
"role": "code",
|
||||
"system_prompt": "你负责代码实现与重构,输出具体修改建议和变更结果。",
|
||||
"system_prompt_file": "agents/coder/AGENT.md",
|
||||
"memory_namespace": "coder",
|
||||
"accept_from": ["main", "tester"],
|
||||
"can_talk_to": ["main", "tester"],
|
||||
@@ -117,7 +119,7 @@
|
||||
"type": "worker",
|
||||
"display_name": "Test Agent",
|
||||
"role": "test",
|
||||
"system_prompt": "你负责测试、验证、回归检查与风险反馈。",
|
||||
"system_prompt_file": "agents/tester/AGENT.md",
|
||||
"memory_namespace": "tester",
|
||||
"accept_from": ["main", "coder"],
|
||||
"can_talk_to": ["main", "coder"],
|
||||
|
||||
@@ -148,12 +148,12 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
toolsRegistry.Register(tools.NewCronTool(cs))
|
||||
}
|
||||
|
||||
maxParallelCalls := cfg.Agents.Defaults.RuntimeControl.ToolMaxParallelCalls
|
||||
maxParallelCalls := cfg.Agents.Defaults.Execution.ToolMaxParallelCalls
|
||||
if maxParallelCalls <= 0 {
|
||||
maxParallelCalls = 4
|
||||
}
|
||||
parallelSafe := make(map[string]struct{})
|
||||
for _, name := range cfg.Agents.Defaults.RuntimeControl.ToolParallelSafeNames {
|
||||
for _, name := range cfg.Agents.Defaults.Execution.ToolParallelSafeNames {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed != "" {
|
||||
parallelSafe[trimmed] = struct{}{}
|
||||
@@ -313,16 +313,50 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
if sessionKey == "" {
|
||||
sessionKey = fmt.Sprintf("subagent:%s", strings.TrimSpace(task.ID))
|
||||
}
|
||||
taskInput := task.Task
|
||||
if p := strings.TrimSpace(task.SystemPrompt); p != "" {
|
||||
taskInput = fmt.Sprintf("Role Profile Prompt:\n%s\n\nTask:\n%s", p, task.Task)
|
||||
}
|
||||
taskInput := loop.buildSubagentTaskInput(task)
|
||||
return loop.ProcessDirectWithOptions(ctx, taskInput, sessionKey, task.OriginChannel, task.OriginChatID, task.MemoryNS, task.ToolAllowlist)
|
||||
})
|
||||
|
||||
return loop
|
||||
}
|
||||
|
||||
func (al *AgentLoop) buildSubagentTaskInput(task *tools.SubagentTask) string {
|
||||
if task == nil {
|
||||
return ""
|
||||
}
|
||||
taskText := strings.TrimSpace(task.Task)
|
||||
if promptFile := strings.TrimSpace(task.SystemPromptFile); promptFile != "" {
|
||||
if promptText := al.readSubagentPromptFile(promptFile); promptText != "" {
|
||||
return fmt.Sprintf("Role Profile Policy (%s):\n%s\n\nTask:\n%s", promptFile, promptText, taskText)
|
||||
}
|
||||
}
|
||||
if prompt := strings.TrimSpace(task.SystemPrompt); prompt != "" {
|
||||
return fmt.Sprintf("Role Profile Prompt:\n%s\n\nTask:\n%s", prompt, taskText)
|
||||
}
|
||||
return taskText
|
||||
}
|
||||
|
||||
func (al *AgentLoop) readSubagentPromptFile(relPath string) string {
|
||||
if al == nil {
|
||||
return ""
|
||||
}
|
||||
workspace := strings.TrimSpace(al.workspace)
|
||||
relPath = strings.TrimSpace(relPath)
|
||||
if workspace == "" || relPath == "" || filepath.IsAbs(relPath) {
|
||||
return ""
|
||||
}
|
||||
fullPath := filepath.Clean(filepath.Join(workspace, relPath))
|
||||
relToWorkspace, err := filepath.Rel(workspace, fullPath)
|
||||
if err != nil || strings.HasPrefix(relToWorkspace, "..") {
|
||||
return ""
|
||||
}
|
||||
data, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
func (al *AgentLoop) Run(ctx context.Context) error {
|
||||
al.running = true
|
||||
|
||||
|
||||
@@ -125,16 +125,17 @@ func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, a
|
||||
items := make([]map[string]interface{}, 0, len(cfg.Agents.Subagents))
|
||||
for agentID, subcfg := range cfg.Agents.Subagents {
|
||||
items = append(items, map[string]interface{}{
|
||||
"agent_id": agentID,
|
||||
"enabled": subcfg.Enabled,
|
||||
"type": subcfg.Type,
|
||||
"display_name": subcfg.DisplayName,
|
||||
"role": subcfg.Role,
|
||||
"description": subcfg.Description,
|
||||
"system_prompt": subcfg.SystemPrompt,
|
||||
"memory_namespace": subcfg.MemoryNamespace,
|
||||
"tool_allowlist": append([]string(nil), subcfg.Tools.Allowlist...),
|
||||
"routing_keywords": routeKeywordsForRegistry(cfg.Agents.Router.Rules, agentID),
|
||||
"agent_id": agentID,
|
||||
"enabled": subcfg.Enabled,
|
||||
"type": subcfg.Type,
|
||||
"display_name": subcfg.DisplayName,
|
||||
"role": subcfg.Role,
|
||||
"description": subcfg.Description,
|
||||
"system_prompt": subcfg.SystemPrompt,
|
||||
"system_prompt_file": subcfg.SystemPromptFile,
|
||||
"memory_namespace": subcfg.MemoryNamespace,
|
||||
"tool_allowlist": append([]string(nil), subcfg.Tools.Allowlist...),
|
||||
"routing_keywords": routeKeywordsForRegistry(cfg.Agents.Router.Rules, agentID),
|
||||
})
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
|
||||
@@ -72,12 +72,13 @@ func TestHandleSubagentRuntimeUpsertConfigSubagent(t *testing.T) {
|
||||
subagentRouter: tools.NewSubagentRouter(manager),
|
||||
}
|
||||
out, err := loop.HandleSubagentRuntime(context.Background(), "upsert_config_subagent", map[string]interface{}{
|
||||
"agent_id": "reviewer",
|
||||
"role": "testing",
|
||||
"display_name": "Review Agent",
|
||||
"system_prompt": "review changes",
|
||||
"routing_keywords": []interface{}{"review", "regression"},
|
||||
"tool_allowlist": []interface{}{"shell", "sessions"},
|
||||
"agent_id": "reviewer",
|
||||
"role": "testing",
|
||||
"display_name": "Review Agent",
|
||||
"system_prompt": "review changes",
|
||||
"system_prompt_file": "agents/reviewer/AGENT.md",
|
||||
"routing_keywords": []interface{}{"review", "regression"},
|
||||
"tool_allowlist": []interface{}{"shell", "sessions"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("upsert config subagent failed: %v", err)
|
||||
@@ -94,6 +95,9 @@ func TestHandleSubagentRuntimeUpsertConfigSubagent(t *testing.T) {
|
||||
if !ok || subcfg.DisplayName != "Review Agent" {
|
||||
t.Fatalf("expected reviewer subagent in config, got %+v", reloaded.Agents.Subagents)
|
||||
}
|
||||
if subcfg.SystemPromptFile != "agents/reviewer/AGENT.md" {
|
||||
t.Fatalf("expected system_prompt_file to persist, got %+v", subcfg)
|
||||
}
|
||||
if len(reloaded.Agents.Router.Rules) == 0 {
|
||||
t.Fatalf("expected router rules to be persisted")
|
||||
}
|
||||
|
||||
43
pkg/agent/subagent_prompt_test.go
Normal file
43
pkg/agent/subagent_prompt_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"clawgo/pkg/tools"
|
||||
)
|
||||
|
||||
func TestBuildSubagentTaskInputPrefersPromptFile(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(workspace, "agents", "coder"), 0755); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(workspace, "agents", "coder", "AGENT.md"), []byte("coder-file-policy"), 0644); err != nil {
|
||||
t.Fatalf("write AGENT failed: %v", err)
|
||||
}
|
||||
loop := &AgentLoop{workspace: workspace}
|
||||
input := loop.buildSubagentTaskInput(&tools.SubagentTask{
|
||||
Task: "implement login flow",
|
||||
SystemPrompt: "inline-fallback",
|
||||
SystemPromptFile: "agents/coder/AGENT.md",
|
||||
})
|
||||
if !strings.Contains(input, "coder-file-policy") {
|
||||
t.Fatalf("expected prompt file content, got: %s", input)
|
||||
}
|
||||
if strings.Contains(input, "inline-fallback") {
|
||||
t.Fatalf("expected file prompt to take precedence, got: %s", input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSubagentTaskInputFallsBackToInlinePrompt(t *testing.T) {
|
||||
loop := &AgentLoop{workspace: t.TempDir()}
|
||||
input := loop.buildSubagentTaskInput(&tools.SubagentTask{
|
||||
Task: "run regression",
|
||||
SystemPrompt: "test inline prompt",
|
||||
})
|
||||
if !strings.Contains(input, "test inline prompt") {
|
||||
t.Fatalf("expected inline prompt in task input, got: %s", input)
|
||||
}
|
||||
}
|
||||
@@ -34,15 +34,21 @@ type AgentsConfig struct {
|
||||
}
|
||||
|
||||
type AgentRouterConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
MainAgentID string `json:"main_agent_id,omitempty"`
|
||||
Strategy string `json:"strategy,omitempty"`
|
||||
Rules []AgentRouteRule `json:"rules,omitempty"`
|
||||
AllowDirectAgentChat bool `json:"allow_direct_agent_chat,omitempty"`
|
||||
MaxHops int `json:"max_hops,omitempty"`
|
||||
DefaultTimeoutSec int `json:"default_timeout_sec,omitempty"`
|
||||
DefaultWaitReply bool `json:"default_wait_reply,omitempty"`
|
||||
StickyThreadOwner bool `json:"sticky_thread_owner,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
MainAgentID string `json:"main_agent_id,omitempty"`
|
||||
Strategy string `json:"strategy,omitempty"`
|
||||
Policy AgentRouterPolicyConfig `json:"policy,omitempty"`
|
||||
Rules []AgentRouteRule `json:"rules,omitempty"`
|
||||
AllowDirectAgentChat bool `json:"allow_direct_agent_chat,omitempty"`
|
||||
MaxHops int `json:"max_hops,omitempty"`
|
||||
DefaultTimeoutSec int `json:"default_timeout_sec,omitempty"`
|
||||
DefaultWaitReply bool `json:"default_wait_reply,omitempty"`
|
||||
StickyThreadOwner bool `json:"sticky_thread_owner,omitempty"`
|
||||
}
|
||||
|
||||
type AgentRouterPolicyConfig struct {
|
||||
IntentMaxInputChars int `json:"intent_max_input_chars" env:"CLAWGO_INTENT_MAX_INPUT_CHARS"`
|
||||
MaxRoundsWithoutUser int `json:"max_rounds_without_user" env:"CLAWGO_AUTOLEARN_MAX_ROUNDS_WITHOUT_USER"`
|
||||
}
|
||||
|
||||
type AgentRouteRule struct {
|
||||
@@ -66,6 +72,7 @@ type SubagentConfig struct {
|
||||
Role string `json:"role,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||
SystemPromptFile string `json:"system_prompt_file,omitempty"`
|
||||
MemoryNamespace string `json:"memory_namespace,omitempty"`
|
||||
AcceptFrom []string `json:"accept_from,omitempty"`
|
||||
CanTalkTo []string `json:"can_talk_to,omitempty"`
|
||||
@@ -94,15 +101,16 @@ type SubagentRuntimeConfig struct {
|
||||
}
|
||||
|
||||
type AgentDefaults struct {
|
||||
Workspace string `json:"workspace" env:"CLAWGO_AGENTS_DEFAULTS_WORKSPACE"`
|
||||
Proxy string `json:"proxy" env:"CLAWGO_AGENTS_DEFAULTS_PROXY"`
|
||||
ProxyFallbacks []string `json:"proxy_fallbacks" env:"CLAWGO_AGENTS_DEFAULTS_PROXY_FALLBACKS"`
|
||||
MaxTokens int `json:"max_tokens" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOKENS"`
|
||||
Temperature float64 `json:"temperature" env:"CLAWGO_AGENTS_DEFAULTS_TEMPERATURE"`
|
||||
MaxToolIterations int `json:"max_tool_iterations" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
||||
Heartbeat HeartbeatConfig `json:"heartbeat"`
|
||||
ContextCompaction ContextCompactionConfig `json:"context_compaction"`
|
||||
RuntimeControl RuntimeControlConfig `json:"runtime_control"`
|
||||
Workspace string `json:"workspace" env:"CLAWGO_AGENTS_DEFAULTS_WORKSPACE"`
|
||||
Proxy string `json:"proxy" env:"CLAWGO_AGENTS_DEFAULTS_PROXY"`
|
||||
ProxyFallbacks []string `json:"proxy_fallbacks" env:"CLAWGO_AGENTS_DEFAULTS_PROXY_FALLBACKS"`
|
||||
MaxTokens int `json:"max_tokens" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOKENS"`
|
||||
Temperature float64 `json:"temperature" env:"CLAWGO_AGENTS_DEFAULTS_TEMPERATURE"`
|
||||
MaxToolIterations int `json:"max_tool_iterations" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
||||
Heartbeat HeartbeatConfig `json:"heartbeat"`
|
||||
ContextCompaction ContextCompactionConfig `json:"context_compaction"`
|
||||
Execution AgentExecutionConfig `json:"execution"`
|
||||
SummaryPolicy SystemSummaryPolicyConfig `json:"summary_policy"`
|
||||
}
|
||||
|
||||
type HeartbeatConfig struct {
|
||||
@@ -112,14 +120,11 @@ type HeartbeatConfig struct {
|
||||
PromptTemplate string `json:"prompt_template" env:"CLAWGO_AGENTS_DEFAULTS_HEARTBEAT_PROMPT_TEMPLATE"`
|
||||
}
|
||||
|
||||
type RuntimeControlConfig struct {
|
||||
IntentMaxInputChars int `json:"intent_max_input_chars" env:"CLAWGO_INTENT_MAX_INPUT_CHARS"`
|
||||
AutoLearnMaxRoundsWithoutUser int `json:"autolearn_max_rounds_without_user" env:"CLAWGO_AUTOLEARN_MAX_ROUNDS_WITHOUT_USER"`
|
||||
RunStateTTLSeconds int `json:"run_state_ttl_seconds" env:"CLAWGO_RUN_STATE_TTL_SECONDS"`
|
||||
RunStateMax int `json:"run_state_max" env:"CLAWGO_RUN_STATE_MAX"`
|
||||
ToolParallelSafeNames []string `json:"tool_parallel_safe_names"`
|
||||
ToolMaxParallelCalls int `json:"tool_max_parallel_calls"`
|
||||
SystemSummary SystemSummaryPolicyConfig `json:"system_summary"`
|
||||
type AgentExecutionConfig struct {
|
||||
RunStateTTLSeconds int `json:"run_state_ttl_seconds" env:"CLAWGO_RUN_STATE_TTL_SECONDS"`
|
||||
RunStateMax int `json:"run_state_max" env:"CLAWGO_RUN_STATE_MAX"`
|
||||
ToolParallelSafeNames []string `json:"tool_parallel_safe_names"`
|
||||
ToolMaxParallelCalls int `json:"tool_max_parallel_calls"`
|
||||
}
|
||||
|
||||
type SystemSummaryPolicyConfig struct {
|
||||
@@ -397,28 +402,30 @@ func DefaultConfig() *Config {
|
||||
MaxSummaryChars: 6000,
|
||||
MaxTranscriptChars: 20000,
|
||||
},
|
||||
RuntimeControl: RuntimeControlConfig{
|
||||
IntentMaxInputChars: 1200,
|
||||
AutoLearnMaxRoundsWithoutUser: 200,
|
||||
RunStateTTLSeconds: 1800,
|
||||
RunStateMax: 500,
|
||||
ToolParallelSafeNames: []string{"read_file", "list_files", "find_files", "grep_files", "memory_search", "web_search", "repo_map", "system_info"},
|
||||
ToolMaxParallelCalls: 2,
|
||||
SystemSummary: SystemSummaryPolicyConfig{
|
||||
Marker: "## System Task Summary",
|
||||
CompletedPrefix: "- Completed:",
|
||||
ChangesPrefix: "- Changes:",
|
||||
OutcomePrefix: "- Outcome:",
|
||||
CompletedTitle: "Completed Actions",
|
||||
ChangesTitle: "Change Summaries",
|
||||
OutcomesTitle: "Execution Outcomes",
|
||||
},
|
||||
Execution: AgentExecutionConfig{
|
||||
RunStateTTLSeconds: 1800,
|
||||
RunStateMax: 500,
|
||||
ToolParallelSafeNames: []string{"read_file", "list_files", "find_files", "grep_files", "memory_search", "web_search", "repo_map", "system_info"},
|
||||
ToolMaxParallelCalls: 2,
|
||||
},
|
||||
SummaryPolicy: SystemSummaryPolicyConfig{
|
||||
Marker: "## System Task Summary",
|
||||
CompletedPrefix: "- Completed:",
|
||||
ChangesPrefix: "- Changes:",
|
||||
OutcomePrefix: "- Outcome:",
|
||||
CompletedTitle: "Completed Actions",
|
||||
ChangesTitle: "Change Summaries",
|
||||
OutcomesTitle: "Execution Outcomes",
|
||||
},
|
||||
},
|
||||
Router: AgentRouterConfig{
|
||||
Enabled: false,
|
||||
MainAgentID: "main",
|
||||
Strategy: "rules_first",
|
||||
Enabled: false,
|
||||
MainAgentID: "main",
|
||||
Strategy: "rules_first",
|
||||
Policy: AgentRouterPolicyConfig{
|
||||
IntentMaxInputChars: 1200,
|
||||
MaxRoundsWithoutUser: 200,
|
||||
},
|
||||
Rules: []AgentRouteRule{},
|
||||
AllowDirectAgentChat: false,
|
||||
MaxHops: 6,
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -17,43 +18,38 @@ func Validate(cfg *Config) []error {
|
||||
if cfg.Agents.Defaults.MaxToolIterations <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.max_tool_iterations must be > 0"))
|
||||
}
|
||||
rc := cfg.Agents.Defaults.RuntimeControl
|
||||
if rc.IntentMaxInputChars < 200 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.intent_max_input_chars must be >= 200"))
|
||||
exec := cfg.Agents.Defaults.Execution
|
||||
if exec.RunStateTTLSeconds < 60 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.execution.run_state_ttl_seconds must be >= 60"))
|
||||
}
|
||||
if rc.AutoLearnMaxRoundsWithoutUser <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.autolearn_max_rounds_without_user must be > 0"))
|
||||
if exec.RunStateMax <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.execution.run_state_max must be > 0"))
|
||||
}
|
||||
if rc.RunStateTTLSeconds < 60 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.run_state_ttl_seconds must be >= 60"))
|
||||
errs = append(errs, validateNonEmptyStringList("agents.defaults.execution.tool_parallel_safe_names", exec.ToolParallelSafeNames)...)
|
||||
if exec.ToolMaxParallelCalls <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.execution.tool_max_parallel_calls must be > 0"))
|
||||
}
|
||||
if rc.RunStateMax <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.run_state_max must be > 0"))
|
||||
summary := cfg.Agents.Defaults.SummaryPolicy
|
||||
if strings.TrimSpace(summary.Marker) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.summary_policy.marker must be non-empty"))
|
||||
}
|
||||
errs = append(errs, validateNonEmptyStringList("agents.defaults.runtime_control.tool_parallel_safe_names", rc.ToolParallelSafeNames)...)
|
||||
if rc.ToolMaxParallelCalls <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.tool_max_parallel_calls must be > 0"))
|
||||
if strings.TrimSpace(summary.CompletedPrefix) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.summary_policy.completed_prefix must be non-empty"))
|
||||
}
|
||||
if strings.TrimSpace(rc.SystemSummary.Marker) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.system_summary.marker must be non-empty"))
|
||||
if strings.TrimSpace(summary.ChangesPrefix) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.summary_policy.changes_prefix must be non-empty"))
|
||||
}
|
||||
if strings.TrimSpace(rc.SystemSummary.CompletedPrefix) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.system_summary.completed_prefix must be non-empty"))
|
||||
if strings.TrimSpace(summary.OutcomePrefix) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.summary_policy.outcome_prefix must be non-empty"))
|
||||
}
|
||||
if strings.TrimSpace(rc.SystemSummary.ChangesPrefix) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.system_summary.changes_prefix must be non-empty"))
|
||||
if strings.TrimSpace(summary.CompletedTitle) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.summary_policy.completed_title must be non-empty"))
|
||||
}
|
||||
if strings.TrimSpace(rc.SystemSummary.OutcomePrefix) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.system_summary.outcome_prefix must be non-empty"))
|
||||
if strings.TrimSpace(summary.ChangesTitle) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.summary_policy.changes_title must be non-empty"))
|
||||
}
|
||||
if strings.TrimSpace(rc.SystemSummary.CompletedTitle) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.system_summary.completed_title must be non-empty"))
|
||||
}
|
||||
if strings.TrimSpace(rc.SystemSummary.ChangesTitle) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.system_summary.changes_title must be non-empty"))
|
||||
}
|
||||
if strings.TrimSpace(rc.SystemSummary.OutcomesTitle) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.system_summary.outcomes_title must be non-empty"))
|
||||
if strings.TrimSpace(summary.OutcomesTitle) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.summary_policy.outcomes_title must be non-empty"))
|
||||
}
|
||||
hb := cfg.Agents.Defaults.Heartbeat
|
||||
if hb.Enabled {
|
||||
@@ -219,6 +215,12 @@ func Validate(cfg *Config) []error {
|
||||
func validateAgentRouter(cfg *Config) []error {
|
||||
router := cfg.Agents.Router
|
||||
var errs []error
|
||||
if router.Policy.IntentMaxInputChars < 200 {
|
||||
errs = append(errs, fmt.Errorf("agents.router.policy.intent_max_input_chars must be >= 200"))
|
||||
}
|
||||
if router.Policy.MaxRoundsWithoutUser <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.router.policy.max_rounds_without_user must be > 0"))
|
||||
}
|
||||
if strings.TrimSpace(router.Strategy) != "" {
|
||||
switch strings.TrimSpace(router.Strategy) {
|
||||
case "rules_first", "round_robin", "manual":
|
||||
@@ -320,6 +322,14 @@ func validateSubagents(cfg *Config) []error {
|
||||
if raw.Tools.MaxParallelCalls < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.tools.max_parallel_calls must be >= 0", id))
|
||||
}
|
||||
if promptFile := strings.TrimSpace(raw.SystemPromptFile); promptFile != "" {
|
||||
if filepath.IsAbs(promptFile) {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.system_prompt_file must be relative", id))
|
||||
}
|
||||
if cleaned := filepath.Clean(promptFile); strings.HasPrefix(cleaned, "..") {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.system_prompt_file must stay within workspace", id))
|
||||
}
|
||||
}
|
||||
if proxy := strings.TrimSpace(raw.Runtime.Proxy); proxy != "" && !providerExists(cfg, proxy) {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.runtime.proxy %q not found in providers", id, proxy))
|
||||
}
|
||||
|
||||
@@ -42,3 +42,20 @@ func TestValidateSubagentsRejectsUnknownPeer(t *testing.T) {
|
||||
t.Fatalf("expected validation errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSubagentsRejectsAbsolutePromptFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.Agents.Subagents["coder"] = SubagentConfig{
|
||||
Enabled: true,
|
||||
SystemPromptFile: "/tmp/AGENT.md",
|
||||
Runtime: SubagentRuntimeConfig{
|
||||
Proxy: "proxy",
|
||||
},
|
||||
}
|
||||
|
||||
if errs := Validate(cfg); len(errs) == 0 {
|
||||
t.Fatalf("expected validation errors")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,36 +14,37 @@ import (
|
||||
)
|
||||
|
||||
type SubagentTask struct {
|
||||
ID string `json:"id"`
|
||||
Task string `json:"task"`
|
||||
Label string `json:"label"`
|
||||
Role string `json:"role"`
|
||||
AgentID string `json:"agent_id"`
|
||||
SessionKey string `json:"session_key"`
|
||||
MemoryNS string `json:"memory_ns"`
|
||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||
ToolAllowlist []string `json:"tool_allowlist,omitempty"`
|
||||
MaxRetries int `json:"max_retries,omitempty"`
|
||||
RetryBackoff int `json:"retry_backoff,omitempty"`
|
||||
TimeoutSec int `json:"timeout_sec,omitempty"`
|
||||
MaxTaskChars int `json:"max_task_chars,omitempty"`
|
||||
MaxResultChars int `json:"max_result_chars,omitempty"`
|
||||
RetryCount int `json:"retry_count,omitempty"`
|
||||
PipelineID string `json:"pipeline_id,omitempty"`
|
||||
PipelineTask string `json:"pipeline_task,omitempty"`
|
||||
ThreadID string `json:"thread_id,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
ParentRunID string `json:"parent_run_id,omitempty"`
|
||||
LastMessageID string `json:"last_message_id,omitempty"`
|
||||
WaitingReply bool `json:"waiting_for_reply,omitempty"`
|
||||
SharedState map[string]interface{} `json:"shared_state,omitempty"`
|
||||
OriginChannel string `json:"origin_channel,omitempty"`
|
||||
OriginChatID string `json:"origin_chat_id,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Result string `json:"result,omitempty"`
|
||||
Steering []string `json:"steering,omitempty"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
ID string `json:"id"`
|
||||
Task string `json:"task"`
|
||||
Label string `json:"label"`
|
||||
Role string `json:"role"`
|
||||
AgentID string `json:"agent_id"`
|
||||
SessionKey string `json:"session_key"`
|
||||
MemoryNS string `json:"memory_ns"`
|
||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||
SystemPromptFile string `json:"system_prompt_file,omitempty"`
|
||||
ToolAllowlist []string `json:"tool_allowlist,omitempty"`
|
||||
MaxRetries int `json:"max_retries,omitempty"`
|
||||
RetryBackoff int `json:"retry_backoff,omitempty"`
|
||||
TimeoutSec int `json:"timeout_sec,omitempty"`
|
||||
MaxTaskChars int `json:"max_task_chars,omitempty"`
|
||||
MaxResultChars int `json:"max_result_chars,omitempty"`
|
||||
RetryCount int `json:"retry_count,omitempty"`
|
||||
PipelineID string `json:"pipeline_id,omitempty"`
|
||||
PipelineTask string `json:"pipeline_task,omitempty"`
|
||||
ThreadID string `json:"thread_id,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
ParentRunID string `json:"parent_run_id,omitempty"`
|
||||
LastMessageID string `json:"last_message_id,omitempty"`
|
||||
WaitingReply bool `json:"waiting_for_reply,omitempty"`
|
||||
SharedState map[string]interface{} `json:"shared_state,omitempty"`
|
||||
OriginChannel string `json:"origin_channel,omitempty"`
|
||||
OriginChatID string `json:"origin_chat_id,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Result string `json:"result,omitempty"`
|
||||
Steering []string `json:"steering,omitempty"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
}
|
||||
|
||||
type SubagentManager struct {
|
||||
@@ -163,6 +164,7 @@ func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOpti
|
||||
}
|
||||
memoryNS := agentID
|
||||
systemPrompt := ""
|
||||
systemPromptFile := ""
|
||||
toolAllowlist := []string(nil)
|
||||
maxRetries := 0
|
||||
retryBackoff := 1000
|
||||
@@ -190,6 +192,7 @@ func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOpti
|
||||
memoryNS = ns
|
||||
}
|
||||
systemPrompt = strings.TrimSpace(profile.SystemPrompt)
|
||||
systemPromptFile = strings.TrimSpace(profile.SystemPromptFile)
|
||||
toolAllowlist = append([]string(nil), profile.ToolAllowlist...)
|
||||
maxRetries = profile.MaxRetries
|
||||
retryBackoff = profile.RetryBackoff
|
||||
@@ -257,31 +260,32 @@ func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOpti
|
||||
}
|
||||
}
|
||||
subagentTask := &SubagentTask{
|
||||
ID: taskID,
|
||||
Task: task,
|
||||
Label: label,
|
||||
Role: role,
|
||||
AgentID: agentID,
|
||||
SessionKey: sessionKey,
|
||||
MemoryNS: memoryNS,
|
||||
SystemPrompt: systemPrompt,
|
||||
ToolAllowlist: toolAllowlist,
|
||||
MaxRetries: maxRetries,
|
||||
RetryBackoff: retryBackoff,
|
||||
TimeoutSec: timeoutSec,
|
||||
MaxTaskChars: maxTaskChars,
|
||||
MaxResultChars: maxResultChars,
|
||||
RetryCount: 0,
|
||||
PipelineID: pipelineID,
|
||||
PipelineTask: pipelineTask,
|
||||
ThreadID: threadID,
|
||||
CorrelationID: correlationID,
|
||||
ParentRunID: parentRunID,
|
||||
OriginChannel: originChannel,
|
||||
OriginChatID: originChatID,
|
||||
Status: "running",
|
||||
Created: now,
|
||||
Updated: now,
|
||||
ID: taskID,
|
||||
Task: task,
|
||||
Label: label,
|
||||
Role: role,
|
||||
AgentID: agentID,
|
||||
SessionKey: sessionKey,
|
||||
MemoryNS: memoryNS,
|
||||
SystemPrompt: systemPrompt,
|
||||
SystemPromptFile: systemPromptFile,
|
||||
ToolAllowlist: toolAllowlist,
|
||||
MaxRetries: maxRetries,
|
||||
RetryBackoff: retryBackoff,
|
||||
TimeoutSec: timeoutSec,
|
||||
MaxTaskChars: maxTaskChars,
|
||||
MaxResultChars: maxResultChars,
|
||||
RetryCount: 0,
|
||||
PipelineID: pipelineID,
|
||||
PipelineTask: pipelineTask,
|
||||
ThreadID: threadID,
|
||||
CorrelationID: correlationID,
|
||||
ParentRunID: parentRunID,
|
||||
OriginChannel: originChannel,
|
||||
OriginChatID: originChatID,
|
||||
Status: "running",
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
taskCtx, cancel := context.WithCancel(ctx)
|
||||
sm.tasks[taskID] = subagentTask
|
||||
@@ -469,19 +473,7 @@ func (sm *SubagentManager) executeTaskOnce(ctx context.Context, task *SubagentTa
|
||||
return "", fmt.Errorf("no llm provider configured for subagent execution")
|
||||
}
|
||||
|
||||
systemPrompt := "You are a subagent. Follow workspace AGENTS.md and complete the task independently."
|
||||
rolePrompt := strings.TrimSpace(task.SystemPrompt)
|
||||
if ws := strings.TrimSpace(sm.workspace); ws != "" {
|
||||
if data, err := os.ReadFile(filepath.Join(ws, "AGENTS.md")); err == nil {
|
||||
txt := strings.TrimSpace(string(data))
|
||||
if txt != "" {
|
||||
systemPrompt = "Workspace policy (AGENTS.md):\n" + txt + "\n\nComplete the given task independently and report the result."
|
||||
}
|
||||
}
|
||||
}
|
||||
if rolePrompt != "" {
|
||||
systemPrompt += "\n\nRole-specific profile prompt:\n" + rolePrompt
|
||||
}
|
||||
systemPrompt := sm.resolveSystemPrompt(task)
|
||||
messages := []providers.Message{
|
||||
{
|
||||
Role: "system",
|
||||
@@ -510,6 +502,44 @@ func (sm *SubagentManager) executeTaskOnce(ctx context.Context, task *SubagentTa
|
||||
return response.Content, nil
|
||||
}
|
||||
|
||||
func (sm *SubagentManager) resolveSystemPrompt(task *SubagentTask) string {
|
||||
systemPrompt := "You are a subagent. Follow workspace AGENTS.md and complete the task independently."
|
||||
workspacePrompt := sm.readWorkspacePromptFile("AGENTS.md")
|
||||
if workspacePrompt != "" {
|
||||
systemPrompt = "Workspace policy (AGENTS.md):\n" + workspacePrompt + "\n\nComplete the given task independently and report the result."
|
||||
}
|
||||
if task == nil {
|
||||
return systemPrompt
|
||||
}
|
||||
if promptFile := strings.TrimSpace(task.SystemPromptFile); promptFile != "" {
|
||||
if promptText := sm.readWorkspacePromptFile(promptFile); promptText != "" {
|
||||
return systemPrompt + "\n\nSubagent policy (" + promptFile + "):\n" + promptText
|
||||
}
|
||||
}
|
||||
if rolePrompt := strings.TrimSpace(task.SystemPrompt); rolePrompt != "" {
|
||||
return systemPrompt + "\n\nRole-specific profile prompt:\n" + rolePrompt
|
||||
}
|
||||
return systemPrompt
|
||||
}
|
||||
|
||||
func (sm *SubagentManager) readWorkspacePromptFile(relPath string) string {
|
||||
ws := strings.TrimSpace(sm.workspace)
|
||||
relPath = strings.TrimSpace(relPath)
|
||||
if ws == "" || relPath == "" || filepath.IsAbs(relPath) {
|
||||
return ""
|
||||
}
|
||||
fullPath := filepath.Clean(filepath.Join(ws, relPath))
|
||||
relToWorkspace, err := filepath.Rel(ws, fullPath)
|
||||
if err != nil || strings.HasPrefix(relToWorkspace, "..") {
|
||||
return ""
|
||||
}
|
||||
data, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
type SubagentRunFunc func(ctx context.Context, task *SubagentTask) (string, error)
|
||||
|
||||
func (sm *SubagentManager) SetRunFunc(f SubagentRunFunc) {
|
||||
|
||||
@@ -23,15 +23,16 @@ func DraftConfigSubagent(description, agentIDHint string) map[string]interface{}
|
||||
keywords := inferDraftKeywords(role, lower)
|
||||
systemPrompt := inferDraftSystemPrompt(role, desc)
|
||||
return map[string]interface{}{
|
||||
"agent_id": agentID,
|
||||
"role": role,
|
||||
"display_name": displayName,
|
||||
"description": desc,
|
||||
"system_prompt": systemPrompt,
|
||||
"memory_namespace": agentID,
|
||||
"tool_allowlist": toolAllowlist,
|
||||
"routing_keywords": keywords,
|
||||
"type": "worker",
|
||||
"agent_id": agentID,
|
||||
"role": role,
|
||||
"display_name": displayName,
|
||||
"description": desc,
|
||||
"system_prompt": systemPrompt,
|
||||
"system_prompt_file": "agents/" + agentID + "/AGENT.md",
|
||||
"memory_namespace": agentID,
|
||||
"tool_allowlist": toolAllowlist,
|
||||
"routing_keywords": keywords,
|
||||
"type": "worker",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +70,9 @@ func UpsertConfigSubagent(configPath string, args map[string]interface{}) (map[s
|
||||
if v := stringArgFromMap(args, "system_prompt"); v != "" {
|
||||
subcfg.SystemPrompt = v
|
||||
}
|
||||
if v := stringArgFromMap(args, "system_prompt_file"); v != "" {
|
||||
subcfg.SystemPromptFile = v
|
||||
}
|
||||
if v := stringArgFromMap(args, "memory_namespace"); v != "" {
|
||||
subcfg.MemoryNamespace = v
|
||||
}
|
||||
|
||||
@@ -39,12 +39,13 @@ func (t *SubagentConfigTool) Parameters() map[string]interface{} {
|
||||
"type": "string",
|
||||
"description": "Optional preferred agent id seed for draft.",
|
||||
},
|
||||
"agent_id": map[string]interface{}{"type": "string"},
|
||||
"role": map[string]interface{}{"type": "string"},
|
||||
"display_name": map[string]interface{}{"type": "string"},
|
||||
"system_prompt": map[string]interface{}{"type": "string"},
|
||||
"memory_namespace": map[string]interface{}{"type": "string"},
|
||||
"type": map[string]interface{}{"type": "string"},
|
||||
"agent_id": map[string]interface{}{"type": "string"},
|
||||
"role": map[string]interface{}{"type": "string"},
|
||||
"display_name": map[string]interface{}{"type": "string"},
|
||||
"system_prompt": map[string]interface{}{"type": "string"},
|
||||
"system_prompt_file": map[string]interface{}{"type": "string"},
|
||||
"memory_namespace": map[string]interface{}{"type": "string"},
|
||||
"type": map[string]interface{}{"type": "string"},
|
||||
"tool_allowlist": map[string]interface{}{
|
||||
"type": "array",
|
||||
"items": map[string]interface{}{"type": "string"},
|
||||
|
||||
@@ -16,21 +16,22 @@ import (
|
||||
)
|
||||
|
||||
type SubagentProfile struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role,omitempty"`
|
||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||
ToolAllowlist []string `json:"tool_allowlist,omitempty"`
|
||||
MemoryNamespace string `json:"memory_namespace,omitempty"`
|
||||
MaxRetries int `json:"max_retries,omitempty"`
|
||||
RetryBackoff int `json:"retry_backoff_ms,omitempty"`
|
||||
TimeoutSec int `json:"timeout_sec,omitempty"`
|
||||
MaxTaskChars int `json:"max_task_chars,omitempty"`
|
||||
MaxResultChars int `json:"max_result_chars,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
ManagedBy string `json:"managed_by,omitempty"`
|
||||
AgentID string `json:"agent_id"`
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role,omitempty"`
|
||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||
SystemPromptFile string `json:"system_prompt_file,omitempty"`
|
||||
ToolAllowlist []string `json:"tool_allowlist,omitempty"`
|
||||
MemoryNamespace string `json:"memory_namespace,omitempty"`
|
||||
MaxRetries int `json:"max_retries,omitempty"`
|
||||
RetryBackoff int `json:"retry_backoff_ms,omitempty"`
|
||||
TimeoutSec int `json:"timeout_sec,omitempty"`
|
||||
MaxTaskChars int `json:"max_task_chars,omitempty"`
|
||||
MaxResultChars int `json:"max_result_chars,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
ManagedBy string `json:"managed_by,omitempty"`
|
||||
}
|
||||
|
||||
type SubagentProfileStore struct {
|
||||
@@ -176,6 +177,7 @@ func normalizeSubagentProfile(in SubagentProfile) SubagentProfile {
|
||||
}
|
||||
p.Role = strings.TrimSpace(p.Role)
|
||||
p.SystemPrompt = strings.TrimSpace(p.SystemPrompt)
|
||||
p.SystemPromptFile = strings.TrimSpace(p.SystemPromptFile)
|
||||
p.MemoryNamespace = normalizeSubagentIdentifier(p.MemoryNamespace)
|
||||
if p.MemoryNamespace == "" {
|
||||
p.MemoryNamespace = p.AgentID
|
||||
@@ -343,19 +345,20 @@ func profileFromConfig(agentID string, subcfg config.SubagentConfig) SubagentPro
|
||||
status = "disabled"
|
||||
}
|
||||
return normalizeSubagentProfile(SubagentProfile{
|
||||
AgentID: agentID,
|
||||
Name: strings.TrimSpace(subcfg.DisplayName),
|
||||
Role: strings.TrimSpace(subcfg.Role),
|
||||
SystemPrompt: strings.TrimSpace(subcfg.SystemPrompt),
|
||||
ToolAllowlist: append([]string(nil), subcfg.Tools.Allowlist...),
|
||||
MemoryNamespace: strings.TrimSpace(subcfg.MemoryNamespace),
|
||||
MaxRetries: subcfg.Runtime.MaxRetries,
|
||||
RetryBackoff: subcfg.Runtime.RetryBackoffMs,
|
||||
TimeoutSec: subcfg.Runtime.TimeoutSec,
|
||||
MaxTaskChars: subcfg.Runtime.MaxTaskChars,
|
||||
MaxResultChars: subcfg.Runtime.MaxResultChars,
|
||||
Status: status,
|
||||
ManagedBy: "config.json",
|
||||
AgentID: agentID,
|
||||
Name: strings.TrimSpace(subcfg.DisplayName),
|
||||
Role: strings.TrimSpace(subcfg.Role),
|
||||
SystemPrompt: strings.TrimSpace(subcfg.SystemPrompt),
|
||||
SystemPromptFile: strings.TrimSpace(subcfg.SystemPromptFile),
|
||||
ToolAllowlist: append([]string(nil), subcfg.Tools.Allowlist...),
|
||||
MemoryNamespace: strings.TrimSpace(subcfg.MemoryNamespace),
|
||||
MaxRetries: subcfg.Runtime.MaxRetries,
|
||||
RetryBackoff: subcfg.Runtime.RetryBackoffMs,
|
||||
TimeoutSec: subcfg.Runtime.TimeoutSec,
|
||||
MaxTaskChars: subcfg.Runtime.MaxTaskChars,
|
||||
MaxResultChars: subcfg.Runtime.MaxResultChars,
|
||||
Status: status,
|
||||
ManagedBy: "config.json",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -382,11 +385,12 @@ func (t *SubagentProfileTool) Parameters() map[string]interface{} {
|
||||
"type": "string",
|
||||
"description": "Unique subagent id, e.g. coder/writer/tester",
|
||||
},
|
||||
"name": map[string]interface{}{"type": "string"},
|
||||
"role": map[string]interface{}{"type": "string"},
|
||||
"system_prompt": map[string]interface{}{"type": "string"},
|
||||
"memory_namespace": map[string]interface{}{"type": "string"},
|
||||
"status": map[string]interface{}{"type": "string", "description": "active|disabled"},
|
||||
"name": map[string]interface{}{"type": "string"},
|
||||
"role": map[string]interface{}{"type": "string"},
|
||||
"system_prompt": map[string]interface{}{"type": "string"},
|
||||
"system_prompt_file": map[string]interface{}{"type": "string"},
|
||||
"memory_namespace": map[string]interface{}{"type": "string"},
|
||||
"status": map[string]interface{}{"type": "string", "description": "active|disabled"},
|
||||
"tool_allowlist": map[string]interface{}{
|
||||
"type": "array",
|
||||
"description": "Tool allowlist entries. Supports tool names, '*'/'all', and grouped tokens like 'group:files_read' or '@pipeline'.",
|
||||
@@ -450,18 +454,19 @@ func (t *SubagentProfileTool) Execute(ctx context.Context, args map[string]inter
|
||||
return "subagent profile already exists", nil
|
||||
}
|
||||
p := SubagentProfile{
|
||||
AgentID: agentID,
|
||||
Name: stringArg(args, "name"),
|
||||
Role: stringArg(args, "role"),
|
||||
SystemPrompt: stringArg(args, "system_prompt"),
|
||||
MemoryNamespace: stringArg(args, "memory_namespace"),
|
||||
Status: stringArg(args, "status"),
|
||||
ToolAllowlist: parseStringList(args["tool_allowlist"]),
|
||||
MaxRetries: profileIntArg(args, "max_retries"),
|
||||
RetryBackoff: profileIntArg(args, "retry_backoff_ms"),
|
||||
TimeoutSec: profileIntArg(args, "timeout_sec"),
|
||||
MaxTaskChars: profileIntArg(args, "max_task_chars"),
|
||||
MaxResultChars: profileIntArg(args, "max_result_chars"),
|
||||
AgentID: agentID,
|
||||
Name: stringArg(args, "name"),
|
||||
Role: stringArg(args, "role"),
|
||||
SystemPrompt: stringArg(args, "system_prompt"),
|
||||
SystemPromptFile: stringArg(args, "system_prompt_file"),
|
||||
MemoryNamespace: stringArg(args, "memory_namespace"),
|
||||
Status: stringArg(args, "status"),
|
||||
ToolAllowlist: parseStringList(args["tool_allowlist"]),
|
||||
MaxRetries: profileIntArg(args, "max_retries"),
|
||||
RetryBackoff: profileIntArg(args, "retry_backoff_ms"),
|
||||
TimeoutSec: profileIntArg(args, "timeout_sec"),
|
||||
MaxTaskChars: profileIntArg(args, "max_task_chars"),
|
||||
MaxResultChars: profileIntArg(args, "max_result_chars"),
|
||||
}
|
||||
saved, err := t.store.Upsert(p)
|
||||
if err != nil {
|
||||
@@ -489,6 +494,9 @@ func (t *SubagentProfileTool) Execute(ctx context.Context, args map[string]inter
|
||||
if _, ok := args["system_prompt"]; ok {
|
||||
next.SystemPrompt = stringArg(args, "system_prompt")
|
||||
}
|
||||
if _, ok := args["system_prompt_file"]; ok {
|
||||
next.SystemPromptFile = stringArg(args, "system_prompt_file")
|
||||
}
|
||||
if _, ok := args["memory_namespace"]; ok {
|
||||
next.MemoryNamespace = stringArg(args, "memory_namespace")
|
||||
}
|
||||
|
||||
@@ -122,11 +122,12 @@ func TestSubagentProfileStoreReadsProfilesFromRuntimeConfig(t *testing.T) {
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Subagents["coder"] = config.SubagentConfig{
|
||||
Enabled: true,
|
||||
DisplayName: "Code Agent",
|
||||
Role: "coding",
|
||||
SystemPrompt: "write code",
|
||||
MemoryNamespace: "code-ns",
|
||||
Enabled: true,
|
||||
DisplayName: "Code Agent",
|
||||
Role: "coding",
|
||||
SystemPrompt: "write code",
|
||||
SystemPromptFile: "agents/coder/AGENT.md",
|
||||
MemoryNamespace: "code-ns",
|
||||
Tools: config.SubagentToolsConfig{
|
||||
Allowlist: []string{"read_file", "shell"},
|
||||
},
|
||||
@@ -154,6 +155,9 @@ func TestSubagentProfileStoreReadsProfilesFromRuntimeConfig(t *testing.T) {
|
||||
if profile.Name != "Code Agent" || profile.Role != "coding" {
|
||||
t.Fatalf("unexpected profile fields: %+v", profile)
|
||||
}
|
||||
if profile.SystemPromptFile != "agents/coder/AGENT.md" {
|
||||
t.Fatalf("expected system_prompt_file from config, got: %s", profile.SystemPromptFile)
|
||||
}
|
||||
if len(profile.ToolAllowlist) != 2 {
|
||||
t.Fatalf("expected merged allowlist, got: %v", profile.ToolAllowlist)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ package tools
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"clawgo/pkg/bus"
|
||||
"clawgo/pkg/providers"
|
||||
)
|
||||
|
||||
func TestSubagentSpawnEnforcesTaskQuota(t *testing.T) {
|
||||
@@ -480,3 +483,58 @@ func waitSubagentDone(t *testing.T, manager *SubagentManager, timeout time.Durat
|
||||
t.Fatalf("timeout waiting for subagent completion")
|
||||
return nil
|
||||
}
|
||||
|
||||
type captureProvider struct {
|
||||
messages []providers.Message
|
||||
}
|
||||
|
||||
func (p *captureProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, options map[string]interface{}) (*providers.LLMResponse, error) {
|
||||
p.messages = append([]providers.Message(nil), messages...)
|
||||
return &providers.LLMResponse{Content: "ok", FinishReason: "stop"}, nil
|
||||
}
|
||||
|
||||
func (p *captureProvider) GetDefaultModel() string { return "test-model" }
|
||||
|
||||
func TestSubagentUsesConfiguredSystemPromptFile(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(workspace, "agents", "coder"), 0755); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(workspace, "AGENTS.md"), []byte("workspace-policy"), 0644); err != nil {
|
||||
t.Fatalf("write workspace AGENTS failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(workspace, "agents", "coder", "AGENT.md"), []byte("coder-policy-from-file"), 0644); err != nil {
|
||||
t.Fatalf("write coder AGENT failed: %v", err)
|
||||
}
|
||||
provider := &captureProvider{}
|
||||
manager := NewSubagentManager(provider, workspace, nil, nil)
|
||||
if _, err := manager.ProfileStore().Upsert(SubagentProfile{
|
||||
AgentID: "coder",
|
||||
Status: "active",
|
||||
SystemPrompt: "inline-fallback",
|
||||
SystemPromptFile: "agents/coder/AGENT.md",
|
||||
}); err != nil {
|
||||
t.Fatalf("profile upsert failed: %v", err)
|
||||
}
|
||||
|
||||
_, err := manager.Spawn(context.Background(), SubagentSpawnOptions{
|
||||
Task: "implement feature",
|
||||
AgentID: "coder",
|
||||
OriginChannel: "cli",
|
||||
OriginChatID: "direct",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("spawn failed: %v", err)
|
||||
}
|
||||
_ = waitSubagentDone(t, manager, 4*time.Second)
|
||||
if len(provider.messages) == 0 {
|
||||
t.Fatalf("expected provider to receive messages")
|
||||
}
|
||||
systemPrompt := provider.messages[0].Content
|
||||
if !strings.Contains(systemPrompt, "coder-policy-from-file") {
|
||||
t.Fatalf("expected system prompt to include configured file content, got: %s", systemPrompt)
|
||||
}
|
||||
if strings.Contains(systemPrompt, "inline-fallback") {
|
||||
t.Fatalf("expected configured file content to take precedence over inline prompt, got: %s", systemPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,14 +370,15 @@ const resources = {
|
||||
keep_recent_messages: 'Keep Recent Messages',
|
||||
max_summary_chars: 'Max Summary Chars',
|
||||
max_transcript_chars: 'Max Transcript Chars',
|
||||
runtime_control: 'Runtime Control',
|
||||
execution: 'Execution',
|
||||
summary_policy: 'Summary Policy',
|
||||
policy: 'Policy',
|
||||
intent_max_input_chars: 'Intent Max Input Chars',
|
||||
autolearn_max_rounds_without_user: 'Autolearn Max Rounds Without User',
|
||||
max_rounds_without_user: 'Max Rounds Without User',
|
||||
run_state_ttl_seconds: 'Run State TTL (Seconds)',
|
||||
run_state_max: 'Run State Max',
|
||||
tool_parallel_safe_names: 'Tool Parallel Safe Names',
|
||||
tool_max_parallel_calls: 'Tool Max Parallel Calls',
|
||||
system_summary: 'System Summary',
|
||||
marker: 'Summary Marker',
|
||||
completed_prefix: 'Completed Prefix',
|
||||
changes_prefix: 'Changes Prefix',
|
||||
@@ -795,14 +796,15 @@ const resources = {
|
||||
keep_recent_messages: '保留最近消息数',
|
||||
max_summary_chars: '摘要最大字符数',
|
||||
max_transcript_chars: '转录最大字符数',
|
||||
runtime_control: '运行时控制',
|
||||
execution: '执行控制',
|
||||
summary_policy: '摘要策略',
|
||||
policy: '策略',
|
||||
intent_max_input_chars: '意图输入最大字符数',
|
||||
autolearn_max_rounds_without_user: '自学习无用户最大轮数',
|
||||
max_rounds_without_user: '无用户最大轮数',
|
||||
run_state_ttl_seconds: '运行状态 TTL(秒)',
|
||||
run_state_max: '运行状态上限',
|
||||
tool_parallel_safe_names: '工具并行安全名单',
|
||||
tool_max_parallel_calls: '工具最大并行调用数',
|
||||
system_summary: '系统摘要',
|
||||
marker: '摘要标记',
|
||||
completed_prefix: '完成前缀',
|
||||
changes_prefix: '变更前缀',
|
||||
|
||||
@@ -66,6 +66,7 @@ type PendingSubagentDraft = {
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
system_prompt?: string;
|
||||
system_prompt_file?: string;
|
||||
tool_allowlist?: string[];
|
||||
routing_keywords?: string[];
|
||||
};
|
||||
@@ -79,6 +80,7 @@ type RegistrySubagent = {
|
||||
role?: string;
|
||||
description?: string;
|
||||
system_prompt?: string;
|
||||
system_prompt_file?: string;
|
||||
memory_namespace?: string;
|
||||
tool_allowlist?: string[];
|
||||
routing_keywords?: string[];
|
||||
@@ -111,6 +113,7 @@ const Subagents: React.FC = () => {
|
||||
const [configRole, setConfigRole] = useState('');
|
||||
const [configDisplayName, setConfigDisplayName] = useState('');
|
||||
const [configSystemPrompt, setConfigSystemPrompt] = useState('');
|
||||
const [configSystemPromptFile, setConfigSystemPromptFile] = useState('');
|
||||
const [configToolAllowlist, setConfigToolAllowlist] = useState('');
|
||||
const [configRoutingKeywords, setConfigRoutingKeywords] = useState('');
|
||||
const [draftDescription, setDraftDescription] = useState('');
|
||||
@@ -284,6 +287,7 @@ const Subagents: React.FC = () => {
|
||||
role: configRole,
|
||||
display_name: configDisplayName,
|
||||
system_prompt: configSystemPrompt,
|
||||
system_prompt_file: configSystemPromptFile,
|
||||
tool_allowlist: toolAllowlist,
|
||||
routing_keywords: routingKeywords,
|
||||
});
|
||||
@@ -293,6 +297,7 @@ const Subagents: React.FC = () => {
|
||||
setConfigRole('');
|
||||
setConfigDisplayName('');
|
||||
setConfigSystemPrompt('');
|
||||
setConfigSystemPromptFile('');
|
||||
setConfigToolAllowlist('');
|
||||
setConfigRoutingKeywords('');
|
||||
await load();
|
||||
@@ -314,6 +319,7 @@ const Subagents: React.FC = () => {
|
||||
setConfigRole(draft.role || '');
|
||||
setConfigDisplayName(draft.display_name || '');
|
||||
setConfigSystemPrompt(draft.system_prompt || '');
|
||||
setConfigSystemPromptFile(draft.system_prompt_file || '');
|
||||
setConfigToolAllowlist(Array.isArray(draft.tool_allowlist) ? draft.tool_allowlist.join(', ') : '');
|
||||
setConfigRoutingKeywords(Array.isArray(draft.routing_keywords) ? draft.routing_keywords.join(', ') : '');
|
||||
await load();
|
||||
@@ -347,6 +353,7 @@ const Subagents: React.FC = () => {
|
||||
setConfigRole(item.role || '');
|
||||
setConfigDisplayName(item.display_name || '');
|
||||
setConfigSystemPrompt(item.system_prompt || '');
|
||||
setConfigSystemPromptFile((item as any).system_prompt_file || '');
|
||||
setConfigToolAllowlist(Array.isArray(item.tool_allowlist) ? item.tool_allowlist.join(', ') : '');
|
||||
setConfigRoutingKeywords(Array.isArray(item.routing_keywords) ? item.routing_keywords.join(', ') : '');
|
||||
};
|
||||
@@ -500,6 +507,7 @@ const Subagents: React.FC = () => {
|
||||
placeholder="system_prompt"
|
||||
className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded min-h-[96px]"
|
||||
/>
|
||||
<input value={configSystemPromptFile} onChange={(e) => setConfigSystemPromptFile(e.target.value)} placeholder="system_prompt_file (relative AGENT.md path)" className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
|
||||
<input value={configToolAllowlist} onChange={(e) => setConfigToolAllowlist(e.target.value)} placeholder="tool_allowlist (comma separated)" className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
|
||||
<input value={configRoutingKeywords} onChange={(e) => setConfigRoutingKeywords(e.target.value)} placeholder="routing_keywords (comma separated)" className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
|
||||
<button onClick={upsertConfigSubagent} className="px-3 py-1.5 text-xs rounded bg-amber-700/80 hover:bg-amber-600">{t('saveToConfig')}</button>
|
||||
|
||||
Reference in New Issue
Block a user