diff --git a/config.example.json b/config.example.json index 975d6fe..4aa4b25 100644 --- a/config.example.json +++ b/config.example.json @@ -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"], diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index a73a93a..98bf0ae 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -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 diff --git a/pkg/agent/runtime_admin.go b/pkg/agent/runtime_admin.go index cc03239..d72c419 100644 --- a/pkg/agent/runtime_admin.go +++ b/pkg/agent/runtime_admin.go @@ -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 { diff --git a/pkg/agent/runtime_admin_test.go b/pkg/agent/runtime_admin_test.go index 115514a..257bf5f 100644 --- a/pkg/agent/runtime_admin_test.go +++ b/pkg/agent/runtime_admin_test.go @@ -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") } diff --git a/pkg/agent/subagent_prompt_test.go b/pkg/agent/subagent_prompt_test.go new file mode 100644 index 0000000..3414248 --- /dev/null +++ b/pkg/agent/subagent_prompt_test.go @@ -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) + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index ae6d6e3..af6a899 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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, diff --git a/pkg/config/validate.go b/pkg/config/validate.go index 5a8d55f..65b2c8b 100644 --- a/pkg/config/validate.go +++ b/pkg/config/validate.go @@ -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)) } diff --git a/pkg/config/validate_test.go b/pkg/config/validate_test.go index 549927b..4958bf6 100644 --- a/pkg/config/validate_test.go +++ b/pkg/config/validate_test.go @@ -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") + } +} diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index e7d8c73..7c11356 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -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) { diff --git a/pkg/tools/subagent_config_manager.go b/pkg/tools/subagent_config_manager.go index 07331ad..f337fde 100644 --- a/pkg/tools/subagent_config_manager.go +++ b/pkg/tools/subagent_config_manager.go @@ -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 } diff --git a/pkg/tools/subagent_config_tool.go b/pkg/tools/subagent_config_tool.go index 365155f..52db482 100644 --- a/pkg/tools/subagent_config_tool.go +++ b/pkg/tools/subagent_config_tool.go @@ -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"}, diff --git a/pkg/tools/subagent_profile.go b/pkg/tools/subagent_profile.go index 8d26205..c2cd06e 100644 --- a/pkg/tools/subagent_profile.go +++ b/pkg/tools/subagent_profile.go @@ -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") } diff --git a/pkg/tools/subagent_profile_test.go b/pkg/tools/subagent_profile_test.go index 07cf39d..f67fd10 100644 --- a/pkg/tools/subagent_profile_test.go +++ b/pkg/tools/subagent_profile_test.go @@ -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) } diff --git a/pkg/tools/subagent_runtime_control_test.go b/pkg/tools/subagent_runtime_control_test.go index 1102c8a..efa4fbb 100644 --- a/pkg/tools/subagent_runtime_control_test.go +++ b/pkg/tools/subagent_runtime_control_test.go @@ -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) + } +} diff --git a/webui/src/i18n/index.ts b/webui/src/i18n/index.ts index 15b23ff..9d5e3be 100644 --- a/webui/src/i18n/index.ts +++ b/webui/src/i18n/index.ts @@ -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: '变更前缀', diff --git a/webui/src/pages/Subagents.tsx b/webui/src/pages/Subagents.tsx index 2e36859..f6cad0e 100644 --- a/webui/src/pages/Subagents.tsx +++ b/webui/src/pages/Subagents.tsx @@ -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]" /> + 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" /> 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" /> 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" />