- 新增 Plan/Build 模式切换(Tab 键),Plan 模式阻止任务执行 - Build 模式:成员输出智能判断,文档存 artifact,提问显示聊天 - 成员可直接与用户对话(多轮),不经过 master 传话 - 任务计划文档自动生成,沟通记录自动追加 - 右侧面板重构为产出物面板,支持查看/编辑/保存 - 输入框改为 textarea,支持 Shift+Enter 换行,修复输入法 Enter 误发送 - Master 会话历史持久化,支持多轮上下文 - parseAssignments 支持多行任务描述 - SOUL.md 热重载、skill 递归发现与内容注入 - 需求确认 skill(HARD-GATE 模式) - air 热重载配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
120 lines
3.1 KiB
Go
120 lines
3.1 KiB
Go
package agent
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"github.com/sdaduanbilei/agent-team/internal/llm"
|
||
"gopkg.in/yaml.v3"
|
||
)
|
||
|
||
type Config struct {
|
||
Name string `yaml:"name"`
|
||
Description string `yaml:"description"`
|
||
Provider string `yaml:"provider"`
|
||
Model string `yaml:"model"`
|
||
BaseURL string `yaml:"base_url"`
|
||
APIKeyEnv string `yaml:"api_key_env"`
|
||
Skills []string `yaml:"skills"`
|
||
CanChallenge bool `yaml:"can_challenge"`
|
||
}
|
||
|
||
type Agent struct {
|
||
Config Config
|
||
Soul string // system prompt from SOUL.md
|
||
Dir string // agents/<name>/
|
||
client *llm.Client
|
||
}
|
||
|
||
func Load(dir string) (*Agent, error) {
|
||
agentMD, err := os.ReadFile(filepath.Join(dir, "AGENT.md"))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
cfg, err := parseFrontmatter(agentMD)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("parse AGENT.md: %w", err)
|
||
}
|
||
soul, _ := os.ReadFile(filepath.Join(dir, "SOUL.md"))
|
||
|
||
if cfg.Provider == "" {
|
||
cfg.Provider = "deepseek"
|
||
}
|
||
if cfg.APIKeyEnv == "" {
|
||
cfg.APIKeyEnv = "DEEPSEEK_API_KEY"
|
||
}
|
||
|
||
client, err := llm.New(cfg.Provider, cfg.Model, cfg.BaseURL, cfg.APIKeyEnv)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &Agent{Config: cfg, Soul: string(soul), Dir: dir, client: client}, nil
|
||
}
|
||
|
||
func parseFrontmatter(data []byte) (Config, error) {
|
||
var cfg Config
|
||
if !bytes.HasPrefix(data, []byte("---")) {
|
||
return cfg, fmt.Errorf("missing frontmatter")
|
||
}
|
||
parts := bytes.SplitN(data, []byte("---"), 3)
|
||
if len(parts) < 3 {
|
||
return cfg, fmt.Errorf("invalid frontmatter")
|
||
}
|
||
return cfg, yaml.Unmarshal(parts[1], &cfg)
|
||
}
|
||
|
||
// Memory returns concatenated memory files content.
|
||
func (a *Agent) Memory() string {
|
||
memDir := filepath.Join(a.Dir, "memory")
|
||
entries, err := os.ReadDir(memDir)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
var sb strings.Builder
|
||
for _, e := range entries {
|
||
if strings.HasSuffix(e.Name(), ".md") {
|
||
data, _ := os.ReadFile(filepath.Join(memDir, e.Name()))
|
||
sb.Write(data)
|
||
sb.WriteString("\n")
|
||
}
|
||
}
|
||
return sb.String()
|
||
}
|
||
|
||
// SaveMemory appends/updates a memory file.
|
||
func (a *Agent) SaveMemory(filename, content string) error {
|
||
memDir := filepath.Join(a.Dir, "memory")
|
||
os.MkdirAll(memDir, 0755)
|
||
return os.WriteFile(filepath.Join(memDir, filename), []byte(content), 0644)
|
||
}
|
||
|
||
// Chat sends messages and streams response tokens via onToken callback.
|
||
func (a *Agent) Chat(ctx context.Context, msgs []llm.Message, onToken func(string)) (string, error) {
|
||
return a.client.Stream(ctx, msgs, onToken)
|
||
}
|
||
|
||
// BuildSystemPrompt 构建完整的 system prompt,每次实时读取 SOUL.md 以支持热更新。
|
||
func (a *Agent) BuildSystemPrompt(extraContext string) string {
|
||
// 实时读取 SOUL.md,支持在页面上修改后立即生效
|
||
soul := a.Soul
|
||
if data, err := os.ReadFile(filepath.Join(a.Dir, "SOUL.md")); err == nil {
|
||
soul = string(data)
|
||
}
|
||
var sb strings.Builder
|
||
sb.WriteString(soul)
|
||
if mem := a.Memory(); mem != "" {
|
||
sb.WriteString("\n\n<memory>\n")
|
||
sb.WriteString(mem)
|
||
sb.WriteString("</memory>")
|
||
}
|
||
if extraContext != "" {
|
||
sb.WriteString("\n\n")
|
||
sb.WriteString(extraContext)
|
||
}
|
||
return sb.String()
|
||
}
|