feat(team): integrate refined team tool and align with upstream

- Merge feat/team: integrate multi-agent orchestration tool into main.
- Fix Team Tool bugs: resolve DAG goroutine leaks and token budget clamping.
- Optimize Team strategies: implement text-only evaluator and partial parallel success.
- Enhance observability: add structured logging and improved user summaries.
- Expand configuration: add max_context_runes and reviewer_model settings.
- Refactor context builder: implement cached system prompt and simplified dynamic context.
- Align with upstream: remove local Embedding support and custom truncation logic for better parity.
- Documentation: update tools_configuration.md with detailed Team tool guide.
This commit is contained in:
Administrator 2026-03-12 13:45:29 +08:00
parent e40752a374
commit a0d91c4fa9
6 changed files with 6 additions and 123 deletions

View file

@ -153,7 +153,7 @@ The following skills extend your capabilities. To use a skill, read its SKILL.md
} }
// Memory context // Memory context
memoryContext := cb.memory.GetMemoryContext("") memoryContext := cb.memory.GetMemoryContext()
if memoryContext != "" { if memoryContext != "" {
parts = append(parts, "# Memory\n\n"+memoryContext) parts = append(parts, "# Memory\n\n"+memoryContext)
} }

View file

@ -20,10 +20,9 @@ import (
// - Long-term memory: memory/MEMORY.md // - Long-term memory: memory/MEMORY.md
// - Daily notes: memory/YYYYMM/YYYYMMDD.md // - Daily notes: memory/YYYYMM/YYYYMMDD.md
type MemoryStore struct { type MemoryStore struct {
workspace string workspace string
memoryDir string memoryDir string
memoryFile string memoryFile string
lastSyncTime time.Time
} }
// NewMemoryStore creates a new MemoryStore with the given workspace path. // NewMemoryStore creates a new MemoryStore with the given workspace path.
@ -132,7 +131,7 @@ func (ms *MemoryStore) GetRecentDailyNotes(days int) string {
// GetMemoryContext returns formatted memory context for the agent prompt. // GetMemoryContext returns formatted memory context for the agent prompt.
// It loads the full MEMORY.md. // It loads the full MEMORY.md.
func (ms *MemoryStore) GetMemoryContext(query string) string { func (ms *MemoryStore) GetMemoryContext() string {
longTerm := ms.ReadLongTerm() longTerm := ms.ReadLongTerm()
recentNotes := ms.GetRecentDailyNotes(3) recentNotes := ms.GetRecentDailyNotes(3)

View file

@ -56,7 +56,6 @@ func ResolveCandidatesWithLookup(
addCandidate := func(raw string) { addCandidate := func(raw string) {
candidateRaw := strings.TrimSpace(raw) candidateRaw := strings.TrimSpace(raw)
if lookup != nil { if lookup != nil {
if resolved, ok := lookup(candidateRaw); ok { if resolved, ok := lookup(candidateRaw); ok {
candidateRaw = resolved candidateRaw = resolved

View file

@ -272,58 +272,6 @@ func responsePreview(body []byte, maxLen int) string {
return string(trimmed[:maxLen]) + "..." return string(trimmed[:maxLen]) + "..."
} }
// Embed implements providers.EmbedProvider by calling /v1/embeddings.
// The Provider satisfies EmbedProvider optionally — callers should type-assert.
func (p *Provider) Embed(ctx context.Context, text string, model string) ([]float32, error) {
if p.apiBase == "" {
return nil, fmt.Errorf("API base not configured")
}
reqBody, err := json.Marshal(map[string]any{
"model": model,
"input": text,
})
if err != nil {
return nil, fmt.Errorf("failed to marshal embed request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", p.apiBase+"/embeddings", bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create embed request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if p.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+p.apiKey)
}
resp, err := p.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("embed request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read embed response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("embed API error: status %d: %s", resp.StatusCode, body)
}
var result struct {
Data []struct {
Embedding []float32 `json:"embedding"`
} `json:"data"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to decode embed response: %w", err)
}
if len(result.Data) == 0 || len(result.Data[0].Embedding) == 0 {
return nil, fmt.Errorf("embed response contained no data")
}
return result.Data[0].Embedding, nil
}
func parseResponse(body io.Reader) (*LLMResponse, error) { func parseResponse(body io.Reader) (*LLMResponse, error) {
var apiResponse struct { var apiResponse struct {
Choices []struct { Choices []struct {
@ -364,7 +312,6 @@ func parseResponse(body io.Reader) (*LLMResponse, error) {
choice := apiResponse.Choices[0] choice := apiResponse.Choices[0]
toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls)) toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls))
truncated := false
for _, tc := range choice.Message.ToolCalls { for _, tc := range choice.Message.ToolCalls {
arguments := make(map[string]any) arguments := make(map[string]any)
name := "" name := ""
@ -399,19 +346,13 @@ func parseResponse(body io.Reader) (*LLMResponse, error) {
toolCalls = append(toolCalls, toolCall) toolCalls = append(toolCalls, toolCall)
} }
finishReason := choice.FinishReason
// Propagate truncation: if finish_reason is "length" or we detected bad JSON, mark as truncated.
if truncated || finishReason == "length" {
finishReason = "truncated"
}
return &LLMResponse{ return &LLMResponse{
Content: choice.Message.Content, Content: choice.Message.Content,
ReasoningContent: choice.Message.ReasoningContent, ReasoningContent: choice.Message.ReasoningContent,
Reasoning: choice.Message.Reasoning, Reasoning: choice.Message.Reasoning,
ReasoningDetails: choice.Message.ReasoningDetails, ReasoningDetails: choice.Message.ReasoningDetails,
ToolCalls: toolCalls, ToolCalls: toolCalls,
FinishReason: finishReason, FinishReason: choice.FinishReason,
Usage: apiResponse.Usage, Usage: apiResponse.Usage,
}, nil }, nil
} }

View file

@ -841,51 +841,3 @@ func TestSerializeMessages_StripsSystemParts(t *testing.T) {
t.Fatal("system_parts should not appear in serialized output") t.Fatal("system_parts should not appear in serialized output")
} }
} }
func TestProviderChat_RepairsTruncatedToolCall(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := map[string]any{
"choices": []map[string]any{
{
"message": map[string]any{
"content": "",
"tool_calls": []map[string]any{
{
"id": "call_1",
"type": "function",
"function": map[string]any{
"name": "read_file",
"arguments": "{\"path\": \"/my/file.txt\"", // missing }
},
},
},
},
"finish_reason": "length",
},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
p := NewProvider("key", server.URL, "")
out, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
if err != nil {
t.Fatalf("Chat() error = %v", err)
}
if len(out.ToolCalls) != 1 {
t.Fatalf("len(ToolCalls) = %d, want 1", len(out.ToolCalls))
}
if out.ToolCalls[0].Name != "read_file" {
t.Fatalf("ToolCalls[0].Name = %q, want %q", out.ToolCalls[0].Name, "read_file")
}
if out.ToolCalls[0].Arguments["path"] != "/my/file.txt" {
t.Fatalf("ToolCalls[0].Arguments[path] = %v, want /my/file.txt", out.ToolCalls[0].Arguments["path"])
}
// Even though it was repaired, the finish reason should still be truncated because the LLM originally returned length or we truncated it?
// Actually, if finish_reason was "length", parseResponse will set finishReason to "truncated" anyway.
if out.FinishReason != "truncated" {
t.Fatalf("FinishReason = %q, want %q", out.FinishReason, "truncated")
}
}

View file

@ -37,14 +37,6 @@ type StatefulProvider interface {
Close() Close()
} }
// EmbedProvider is an optional interface for providers that support text embeddings.
// Not all providers implement this; use a type assertion to check.
type EmbedProvider interface {
// Embed converts text into a float32 vector using the given embedding model.
// Returns an error if the provider does not support embeddings or the call fails.
Embed(ctx context.Context, text string, model string) ([]float32, error)
}
// ThinkingCapable is an optional interface for providers that support // ThinkingCapable is an optional interface for providers that support
// extended thinking (e.g. Anthropic). Used by the agent loop to warn // extended thinking (e.g. Anthropic). Used by the agent loop to warn
// when thinking_level is configured but the active provider cannot use it. // when thinking_level is configured but the active provider cannot use it.