feat: files built-in plugin, doc rewrite, architecture cleanup

- Add files plugin as built-in (internal/plugins/files/) with read/write/edit/ls tools,
  supporting overwrite/append/insert/create modes and offset/limit segmented reading
- Rewrite README.md with core domain separation and three-layer memory highlights
- Rewrite docs/OVERVIEW.md with per-subsystem file path references
- Rewrite docs/ARCHITECTURE.md (783→~300 lines), merge redundant sections
- Clean docs/PLUGIN_DEV.md: remove emoji, simplify SDK examples
- Fix provider Model pollution in LuaAdaptedProvider.Chat()
- Fix executeToolCall to return actual error vs quiet not-found
- Fix plugin.Open path caching with SHA256 temp-path workaround
- Add knowledge/homeagent_architecture demo entry
- Add config/personal/personal.md identity configuration
This commit is contained in:
root
2026-07-06 14:33:09 +08:00
parent 8cec92d947
commit df0abcd298
12 changed files with 1111 additions and 890 deletions

View File

@ -407,9 +407,7 @@ func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, adapter string) *LuaAda
func (p *LuaAdaptedProvider) Name() string { return p.name }
func (p *LuaAdaptedProvider) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
if req.Model == "" {
req.Model = p.cfg.Model
}
req.Model = p.cfg.Model
rawReq, _ := json.Marshal(req)
@ -464,9 +462,7 @@ func (p *LuaAdaptedProvider) Chat(ctx context.Context, req *CompletionRequest) (
}
func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
if req.Model == "" {
req.Model = p.cfg.Model
}
req.Model = p.cfg.Model
req.Stream = true
rawReq, _ := json.Marshal(req)
@ -585,6 +581,7 @@ func (s *SSEScanner) Text() string { return s.pending }
type ProviderManager struct {
mu sync.RWMutex
providers map[string]Provider
order []string
default_ string
}
@ -598,6 +595,7 @@ func (m *ProviderManager) Register(name string, p Provider) {
m.mu.Lock()
defer m.mu.Unlock()
m.providers[name] = p
m.order = append(m.order, name)
if m.default_ == "" {
m.default_ = name
}
@ -647,13 +645,29 @@ func (m *ProviderManager) QuickChat(ctx context.Context, prompt string) (*Comple
func (m *ProviderManager) List() []string {
m.mu.RLock()
defer m.mu.RUnlock()
var names []string
for n := range m.providers {
names = append(names, n)
}
names := make([]string, len(m.order))
copy(names, m.order)
return names
}
func (m *ProviderManager) OrderedProviders() []Provider {
m.mu.RLock()
defer m.mu.RUnlock()
list := make([]Provider, 0, len(m.order))
for _, name := range m.order {
if p, ok := m.providers[name]; ok {
list = append(list, p)
}
}
return list
}
func (m *ProviderManager) ProviderCount() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.providers)
}
type rawToolCall struct {
ID string `json:"id"`
Type string `json:"type"`

View File

@ -42,7 +42,6 @@ type Agent struct {
systemPrompt string
ctx context.Context
cancel context.CancelFunc
maxTurns int
// 文档记忆(第二层)
docStore *document.Store
@ -114,7 +113,6 @@ type AgentConfig struct {
Indexer *memory.Indexer
Skills *skill.Manager
Tracker *tracker.Tracker
MaxToolTurns int
DocStore *document.Store
Knowledge *knowledge.Store
@ -135,9 +133,6 @@ type AgentConfig struct {
func New(cfg AgentConfig) *Agent {
ctx, cancel := context.WithCancel(context.Background())
if cfg.MaxToolTurns <= 0 {
cfg.MaxToolTurns = 10
}
if cfg.DistillInterval <= 0 {
cfg.DistillInterval = 30 * time.Minute
}
@ -158,7 +153,6 @@ func New(cfg AgentConfig) *Agent {
systemPrompt: cfg.SystemPrompt,
ctx: ctx,
cancel: cancel,
maxTurns: cfg.MaxToolTurns,
docStore: cfg.DocStore,
knowledge: cfg.Knowledge,
social: cfg.SocialStore,
@ -577,7 +571,7 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
}
}
for turn := 0; turn < a.maxTurns; turn++ {
for turn := 0; ; turn++ {
// === 高优先级打断:每次 LLM 调用前检查拦截通道 ===
if text := a.drainInterrupt(); text != "" {
msgs = append(msgs, agentAPI.Message{
@ -600,20 +594,49 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
}
// 可取消的 LLM 调用:interceptLoop 通过 cancelLLM 打断进行中的请求
reqCtx, reqCancel := context.WithCancel(a.ctx)
a.llmMu.Lock()
a.cancelLLM = reqCancel
a.llmMu.Unlock()
// 多 LLM 源顺位降级:当当前 provider 失败时,按注册顺序依次尝试
var providers []agentAPI.Provider
if a.providerManager != nil {
providers = a.providerManager.OrderedProviders()
}
if len(providers) == 0 {
providers = []agentAPI.Provider{a.provider}
}
var resp *agentAPI.CompletionResponse
var llmErr error
resp, err := a.provider.Chat(reqCtx, req)
for pi, fbProvider := range providers {
if pi > 0 {
log.Printf("[agent] LLM fallback: trying provider %q (fallback #%d/%d)",
fbProvider.Name(), pi, len(providers)-1)
}
a.llmMu.Lock()
a.cancelLLM = nil
a.llmMu.Unlock()
reqCancel()
fCtx, fCancel := context.WithCancel(a.ctx)
a.llmMu.Lock()
a.cancelLLM = fCancel
a.llmMu.Unlock()
if err != nil {
return "", toolsUsed, fmt.Errorf("provider: %w", err)
resp, llmErr = fbProvider.Chat(fCtx, req)
a.llmMu.Lock()
a.cancelLLM = nil
a.llmMu.Unlock()
fCancel()
if llmErr == nil {
if fbProvider != a.provider {
a.provider = fbProvider
log.Printf("[agent] switched active provider to %q after fallback",
fbProvider.Name())
}
break
}
log.Printf("[agent] provider %q failed: %v", fbProvider.Name(), llmErr)
}
if llmErr != nil {
return "", toolsUsed, fmt.Errorf("all %d providers failed, last error: %w",
len(providers), llmErr)
}
// === Stage: post_action — LLM 返回,插件可审查/修改 ===
@ -678,10 +701,8 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
"result": result,
"status": "ok",
})
}
}
return "", toolsUsed, fmt.Errorf("tool execution exceeded %d turns", a.maxTurns)
}
}
func convertToolCalls(tcs []agentAPI.ToolCall) []sdk.ToolCall {
@ -767,6 +788,8 @@ func (a *Agent) executeToolCall(tc agentAPI.ToolCall) string {
if a.stageHost != nil {
if result, err := a.stageHost.ExecuteTool(tc.Name, tc.Arguments); err == nil {
return fmt.Sprintf("%v", result)
} else if !strings.Contains(err.Error(), "not found in any plugin") {
return fmt.Sprintf("工具 %s 执行失败: %v", tc.Name, err)
}
}
@ -1242,6 +1265,30 @@ func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
return prompt
}
// cleanParams removes empty required arrays from tool parameters that strict APIs reject.
func cleanParams(params map[string]interface{}) map[string]interface{} {
if params == nil {
return nil
}
cleaned := make(map[string]interface{}, len(params))
for k, v := range params {
cleaned[k] = v
}
if req, ok := cleaned["required"]; ok {
switch v := req.(type) {
case []interface{}:
if len(v) == 0 {
delete(cleaned, "required")
}
case []string:
if len(v) == 0 {
delete(cleaned, "required")
}
}
}
return cleaned
}
func (a *Agent) buildToolDefs() []interface{} {
var tools []interface{}
@ -1252,7 +1299,7 @@ func (a *Agent) buildToolDefs() []interface{} {
"function": map[string]interface{}{
"name": td.Name,
"description": td.Description,
"parameters": td.Parameters,
"parameters": cleanParams(td.Parameters),
},
})
}
@ -1266,7 +1313,7 @@ func (a *Agent) buildToolDefs() []interface{} {
"function": map[string]interface{}{
"name": td.Name,
"description": td.Description,
"parameters": td.Parameters,
"parameters": cleanParams(td.Parameters),
},
})
}