sdaduanbilei fe1a82bbe2 feat: Plan/Build 模式、artifact 系统、成员直接对话
- 新增 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>
2026-03-06 17:34:44 +08:00

120 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}