Merge remote-tracking branch 'origin/HEAD' into feat/better-version

This commit is contained in:
yinwm
2026-02-13 19:34:44 +08:00
20 changed files with 200 additions and 119 deletions

View File

@@ -3,7 +3,6 @@ name: build
on: on:
push: push:
branches: ["main"] branches: ["main"]
pull_request:
jobs: jobs:
build: build:

View File

@@ -1,9 +1,8 @@
name: 🐳 Build & Push Docker Image name: 🐳 Build & Push Docker Image
on: on:
push: release:
branches: [main] types: [published]
tags: ["v*"]
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io

52
.github/workflows/pr.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: pr-check
on:
pull_request:
jobs:
fmt-check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Check formatting
run: |
make fmt
git diff --exit-code || (echo "::error::Code is not formatted. Run 'make fmt' and commit the changes." && exit 1)
vet:
runs-on: ubuntu-latest
needs: fmt-check
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run go vet
run: go vet ./...
test:
runs-on: ubuntu-latest
needs: fmt-check
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run go test
run: go test ./...

View File

@@ -39,7 +39,7 @@
## 📢 News ## 📢 News
2026-02-09 🎉 PicoClaw Launched! Built in 1 day to bring AI Agents to $10 hardware with <10MB RAM. 🦐 皮皮虾,我们走 2026-02-09 🎉 PicoClaw Launched! Built in 1 day to bring AI Agents to $10 hardware with <10MB RAM. 🦐 PicoClawLet's Go
## ✨ Features ## ✨ Features

View File

@@ -170,8 +170,8 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str
// Log system prompt summary for debugging (debug mode only) // Log system prompt summary for debugging (debug mode only)
logger.DebugCF("agent", "System prompt built", logger.DebugCF("agent", "System prompt built",
map[string]interface{}{ map[string]interface{}{
"total_chars": len(systemPrompt), "total_chars": len(systemPrompt),
"total_lines": strings.Count(systemPrompt, "\n") + 1, "total_lines": strings.Count(systemPrompt, "\n") + 1,
"section_count": strings.Count(systemPrompt, "\n\n---\n\n") + 1, "section_count": strings.Count(systemPrompt, "\n\n---\n\n") + 1,
}) })
@@ -193,9 +193,9 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str
// --- INICIO DEL FIX --- // --- INICIO DEL FIX ---
//Diegox-17 //Diegox-17
for len(history) > 0 && (history[0].Role == "tool") { for len(history) > 0 && (history[0].Role == "tool") {
logger.DebugCF("agent", "Removing orphaned tool message from history to prevent LLM error", logger.DebugCF("agent", "Removing orphaned tool message from history to prevent LLM error",
map[string]interface{}{"role": history[0].Role}) map[string]interface{}{"role": history[0].Role})
history = history[1:] history = history[1:]
} }
//Diegox-17 //Diegox-17
// --- FIN DEL FIX --- // --- FIN DEL FIX ---

View File

@@ -33,14 +33,14 @@ type AgentLoop struct {
provider providers.LLMProvider provider providers.LLMProvider
workspace string workspace string
model string model string
contextWindow int // Maximum context window size in tokens contextWindow int // Maximum context window size in tokens
maxIterations int maxIterations int
sessions *session.SessionManager sessions *session.SessionManager
state *state.Manager state *state.Manager
contextBuilder *ContextBuilder contextBuilder *ContextBuilder
tools *tools.ToolRegistry tools *tools.ToolRegistry
running atomic.Bool running atomic.Bool
summarizing sync.Map // Tracks which sessions are currently being summarized summarizing sync.Map // Tracks which sessions are currently being summarized
} }
// processOptions configures how a message is processed // processOptions configures how a message is processed
@@ -157,11 +157,22 @@ func (al *AgentLoop) Run(ctx context.Context) error {
} }
if response != "" { if response != "" {
al.bus.PublishOutbound(bus.OutboundMessage{ // Check if the message tool already sent a response during this round.
Channel: msg.Channel, // If so, skip publishing to avoid duplicate messages to the user.
ChatID: msg.ChatID, alreadySent := false
Content: response, if tool, ok := al.tools.Get("message"); ok {
}) if mt, ok := tool.(*tools.MessageTool); ok {
alreadySent = mt.HasSentInRound()
}
}
if !alreadySent {
al.bus.PublishOutbound(bus.OutboundMessage{
Channel: msg.Channel,
ChatID: msg.ChatID,
Content: response,
})
}
} }
} }
} }
@@ -285,9 +296,9 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
if constants.IsInternalChannel(originChannel) { if constants.IsInternalChannel(originChannel) {
logger.InfoCF("agent", "Subagent completed (internal channel)", logger.InfoCF("agent", "Subagent completed (internal channel)",
map[string]interface{}{ map[string]interface{}{
"sender_id": msg.SenderID, "sender_id": msg.SenderID,
"content_len": len(content), "content_len": len(content),
"channel": originChannel, "channel": originChannel,
}) })
return "", nil return "", nil
} }
@@ -296,9 +307,9 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
// Don't forward result here, subagent should use message tool to communicate with user // Don't forward result here, subagent should use message tool to communicate with user
logger.InfoCF("agent", "Subagent completed", logger.InfoCF("agent", "Subagent completed",
map[string]interface{}{ map[string]interface{}{
"sender_id": msg.SenderID, "sender_id": msg.SenderID,
"channel": originChannel, "channel": originChannel,
"content_len": len(content), "content_len": len(content),
}) })
// Agent only logs, does not respond to user // Agent only logs, does not respond to user

View File

@@ -18,7 +18,7 @@ type mockProvider struct{}
func (m *mockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) { func (m *mockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) {
return &providers.LLMResponse{ return &providers.LLMResponse{
Content: "Mock response", Content: "Mock response",
ToolCalls: []providers.ToolCall{}, ToolCalls: []providers.ToolCall{},
}, nil }, nil
} }
@@ -364,7 +364,7 @@ type simpleMockProvider struct {
func (m *simpleMockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) { func (m *simpleMockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) {
return &providers.LLMResponse{ return &providers.LLMResponse{
Content: m.response, Content: m.response,
ToolCalls: []providers.ToolCall{}, ToolCalls: []providers.ToolCall{},
}, nil }, nil
} }
@@ -475,7 +475,7 @@ func TestToolResult_SilentToolDoesNotSendUserMessage(t *testing.T) {
SenderID: "user1", SenderID: "user1",
ChatID: "chat1", ChatID: "chat1",
Content: "read test.txt", Content: "read test.txt",
SessionKey: "test-session", SessionKey: "test-session",
} }
response := helper.executeAndGetResponse(t, ctx, msg) response := helper.executeAndGetResponse(t, ctx, msg)
@@ -517,7 +517,7 @@ func TestToolResult_UserFacingToolDoesSendMessage(t *testing.T) {
SenderID: "user1", SenderID: "user1",
ChatID: "chat1", ChatID: "chat1",
Content: "run hello", Content: "run hello",
SessionKey: "test-session", SessionKey: "test-session",
} }
response := helper.executeAndGetResponse(t, ctx, msg) response := helper.executeAndGetResponse(t, ctx, msg)

View File

@@ -40,8 +40,8 @@ func NewMemoryStore(workspace string) *MemoryStore {
// getTodayFile returns the path to today's daily note file (memory/YYYYMM/YYYYMMDD.md). // getTodayFile returns the path to today's daily note file (memory/YYYYMM/YYYYMMDD.md).
func (ms *MemoryStore) getTodayFile() string { func (ms *MemoryStore) getTodayFile() string {
today := time.Now().Format("20060102") // YYYYMMDD today := time.Now().Format("20060102") // YYYYMMDD
monthDir := today[:6] // YYYYMM monthDir := today[:6] // YYYYMM
filePath := filepath.Join(ms.memoryDir, monthDir, today+".md") filePath := filepath.Join(ms.memoryDir, monthDir, today+".md")
return filePath return filePath
} }
@@ -104,8 +104,8 @@ func (ms *MemoryStore) GetRecentDailyNotes(days int) string {
for i := 0; i < days; i++ { for i := 0; i < days; i++ {
date := time.Now().AddDate(0, 0, -i) date := time.Now().AddDate(0, 0, -i)
dateStr := date.Format("20060102") // YYYYMMDD dateStr := date.Format("20060102") // YYYYMMDD
monthDir := dateStr[:6] // YYYYMM monthDir := dateStr[:6] // YYYYMM
filePath := filepath.Join(ms.memoryDir, monthDir, dateStr+".md") filePath := filepath.Join(ms.memoryDir, monthDir, dateStr+".md")
if data, err := os.ReadFile(filePath); err == nil { if data, err := os.ReadFile(filePath); err == nil {

View File

@@ -20,12 +20,12 @@ import (
// It uses WebSocket for receiving messages via stream mode and API for sending // It uses WebSocket for receiving messages via stream mode and API for sending
type DingTalkChannel struct { type DingTalkChannel struct {
*BaseChannel *BaseChannel
config config.DingTalkConfig config config.DingTalkConfig
clientID string clientID string
clientSecret string clientSecret string
streamClient *client.StreamClient streamClient *client.StreamClient
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
// Map to store session webhooks for each chat // Map to store session webhooks for each chat
sessionWebhooks sync.Map // chatID -> sessionWebhook sessionWebhooks sync.Map // chatID -> sessionWebhook
} }
@@ -109,8 +109,8 @@ func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
} }
logger.DebugCF("dingtalk", "Sending message", map[string]interface{}{ logger.DebugCF("dingtalk", "Sending message", map[string]interface{}{
"chat_id": msg.ChatID, "chat_id": msg.ChatID,
"preview": utils.Truncate(msg.Content, 100), "preview": utils.Truncate(msg.Content, 100),
}) })
// Use the session webhook to send the reply // Use the session webhook to send the reply

View File

@@ -282,9 +282,9 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) {
} }
logger.DebugCF("slack", "Received message", map[string]interface{}{ logger.DebugCF("slack", "Received message", map[string]interface{}{
"sender_id": senderID, "sender_id": senderID,
"chat_id": chatID, "chat_id": chatID,
"preview": utils.Truncate(content, 50), "preview": utils.Truncate(content, 50),
"has_thread": threadTS != "", "has_thread": threadTS != "",
}) })

View File

@@ -58,13 +58,13 @@ type AgentsConfig struct {
} }
type AgentDefaults struct { type AgentDefaults struct {
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
} }
type ChannelsConfig struct { type ChannelsConfig struct {
@@ -140,15 +140,16 @@ type HeartbeatConfig struct {
} }
type ProvidersConfig struct { type ProvidersConfig struct {
Anthropic ProviderConfig `json:"anthropic"` Anthropic ProviderConfig `json:"anthropic"`
OpenAI ProviderConfig `json:"openai"` OpenAI ProviderConfig `json:"openai"`
OpenRouter ProviderConfig `json:"openrouter"` OpenRouter ProviderConfig `json:"openrouter"`
Groq ProviderConfig `json:"groq"` Groq ProviderConfig `json:"groq"`
Zhipu ProviderConfig `json:"zhipu"` Zhipu ProviderConfig `json:"zhipu"`
VLLM ProviderConfig `json:"vllm"` VLLM ProviderConfig `json:"vllm"`
Gemini ProviderConfig `json:"gemini"` Gemini ProviderConfig `json:"gemini"`
Nvidia ProviderConfig `json:"nvidia"` Nvidia ProviderConfig `json:"nvidia"`
Moonshot ProviderConfig `json:"moonshot"` Moonshot ProviderConfig `json:"moonshot"`
ShengSuanYun ProviderConfig `json:"shengsuanyun"`
} }
type ProviderConfig struct { type ProviderConfig struct {
@@ -180,13 +181,13 @@ func DefaultConfig() *Config {
return &Config{ return &Config{
Agents: AgentsConfig{ Agents: AgentsConfig{
Defaults: AgentDefaults{ Defaults: AgentDefaults{
Workspace: "~/.picoclaw/workspace", Workspace: "~/.picoclaw/workspace",
RestrictToWorkspace: true, RestrictToWorkspace: true,
Provider: "", Provider: "",
Model: "glm-4.7", Model: "glm-4.7",
MaxTokens: 8192, MaxTokens: 8192,
Temperature: 0.7, Temperature: 0.7,
MaxToolIterations: 20, MaxToolIterations: 20,
}, },
}, },
Channels: ChannelsConfig{ Channels: ChannelsConfig{
@@ -239,15 +240,16 @@ func DefaultConfig() *Config {
}, },
}, },
Providers: ProvidersConfig{ Providers: ProvidersConfig{
Anthropic: ProviderConfig{}, Anthropic: ProviderConfig{},
OpenAI: ProviderConfig{}, OpenAI: ProviderConfig{},
OpenRouter: ProviderConfig{}, OpenRouter: ProviderConfig{},
Groq: ProviderConfig{}, Groq: ProviderConfig{},
Zhipu: ProviderConfig{}, Zhipu: ProviderConfig{},
VLLM: ProviderConfig{}, VLLM: ProviderConfig{},
Gemini: ProviderConfig{}, Gemini: ProviderConfig{},
Nvidia: ProviderConfig{}, Nvidia: ProviderConfig{},
Moonshot: ProviderConfig{}, Moonshot: ProviderConfig{},
ShengSuanYun: ProviderConfig{},
}, },
Gateway: GatewayConfig{ Gateway: GatewayConfig{
Host: "0.0.0.0", Host: "0.0.0.0",
@@ -337,6 +339,9 @@ func (c *Config) GetAPIKey() string {
if c.Providers.VLLM.APIKey != "" { if c.Providers.VLLM.APIKey != "" {
return c.Providers.VLLM.APIKey return c.Providers.VLLM.APIKey
} }
if c.Providers.ShengSuanYun.APIKey != "" {
return c.Providers.ShengSuanYun.APIKey
}
return "" return ""
} }

View File

@@ -27,7 +27,7 @@ var supportedChannels = map[string]bool{
"whatsapp": true, "whatsapp": true,
"feishu": true, "feishu": true,
"qq": true, "qq": true,
"dingtalk": true, "dingtalk": true,
"maixcam": true, "maixcam": true,
} }

View File

@@ -44,8 +44,8 @@ func TestConvertKeysToSnake(t *testing.T) {
"apiKey": "test-key", "apiKey": "test-key",
"apiBase": "https://example.com", "apiBase": "https://example.com",
"nested": map[string]interface{}{ "nested": map[string]interface{}{
"maxTokens": float64(8192), "maxTokens": float64(8192),
"allowFrom": []interface{}{"user1", "user2"}, "allowFrom": []interface{}{"user1", "user2"},
"deeperLevel": map[string]interface{}{ "deeperLevel": map[string]interface{}{
"clientId": "abc", "clientId": "abc",
}, },
@@ -256,11 +256,11 @@ func TestConvertConfig(t *testing.T) {
data := map[string]interface{}{ data := map[string]interface{}{
"agents": map[string]interface{}{ "agents": map[string]interface{}{
"defaults": map[string]interface{}{ "defaults": map[string]interface{}{
"model": "claude-3-opus", "model": "claude-3-opus",
"max_tokens": float64(4096), "max_tokens": float64(4096),
"temperature": 0.5, "temperature": 0.5,
"max_tool_iterations": float64(10), "max_tool_iterations": float64(10),
"workspace": "~/.openclaw/workspace", "workspace": "~/.openclaw/workspace",
}, },
}, },
} }

View File

@@ -254,22 +254,22 @@ func findMatchingBrace(text string, pos int) int {
// claudeCliJSONResponse represents the JSON output from the claude CLI. // claudeCliJSONResponse represents the JSON output from the claude CLI.
// Matches the real claude CLI v2.x output format. // Matches the real claude CLI v2.x output format.
type claudeCliJSONResponse struct { type claudeCliJSONResponse struct {
Type string `json:"type"` Type string `json:"type"`
Subtype string `json:"subtype"` Subtype string `json:"subtype"`
IsError bool `json:"is_error"` IsError bool `json:"is_error"`
Result string `json:"result"` Result string `json:"result"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
TotalCostUSD float64 `json:"total_cost_usd"` TotalCostUSD float64 `json:"total_cost_usd"`
DurationMS int `json:"duration_ms"` DurationMS int `json:"duration_ms"`
DurationAPI int `json:"duration_api_ms"` DurationAPI int `json:"duration_api_ms"`
NumTurns int `json:"num_turns"` NumTurns int `json:"num_turns"`
Usage claudeCliUsageInfo `json:"usage"` Usage claudeCliUsageInfo `json:"usage"`
} }
// claudeCliUsageInfo represents token usage from the claude CLI response. // claudeCliUsageInfo represents token usage from the claude CLI response.
type claudeCliUsageInfo struct { type claudeCliUsageInfo struct {
InputTokens int `json:"input_tokens"` InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"` OutputTokens int `json:"output_tokens"`
CacheCreationInputTokens int `json:"cache_creation_input_tokens"` CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
CacheReadInputTokens int `json:"cache_read_input_tokens"` CacheReadInputTokens int `json:"cache_read_input_tokens"`
} }

View File

@@ -967,9 +967,9 @@ func TestFindMatchingBrace(t *testing.T) {
{`{"a":1}`, 0, 7}, {`{"a":1}`, 0, 7},
{`{"a":{"b":2}}`, 0, 13}, {`{"a":{"b":2}}`, 0, 13},
{`text {"a":1} more`, 5, 12}, {`text {"a":1} more`, 5, 12},
{`{unclosed`, 0, 0}, // no match returns pos {`{unclosed`, 0, 0}, // no match returns pos
{`{}`, 0, 2}, // empty object {`{}`, 0, 2}, // empty object
{`{{{}}}`, 0, 6}, // deeply nested {`{{{}}}`, 0, 6}, // deeply nested
{`{"a":"b{c}d"}`, 0, 13}, // braces in strings (simplified matcher) {`{"a":"b{c}d"}`, 0, 13}, // braces in strings (simplified matcher)
} }
for _, tt := range tests { for _, tt := range tests {

View File

@@ -289,6 +289,14 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
apiKey = cfg.Providers.VLLM.APIKey apiKey = cfg.Providers.VLLM.APIKey
apiBase = cfg.Providers.VLLM.APIBase apiBase = cfg.Providers.VLLM.APIBase
} }
case "shengsuanyun":
if cfg.Providers.ShengSuanYun.APIKey != "" {
apiKey = cfg.Providers.ShengSuanYun.APIKey
apiBase = cfg.Providers.ShengSuanYun.APIBase
if apiBase == "" {
apiBase = "https://router.shengsuanyun.com/api/v1"
}
}
case "claude-cli", "claudecode", "claude-code": case "claude-cli", "claudecode", "claude-code":
workspace := cfg.Agents.Defaults.Workspace workspace := cfg.Agents.Defaults.Workspace
if workspace == "" { if workspace == "" {

View File

@@ -1,4 +1,4 @@
package tools package tools
import ( import (
"context" "context"
@@ -147,8 +147,8 @@ func (t *CronTool) addJob(args map[string]interface{}) *ToolResult {
if hasAt { if hasAt {
atMS := time.Now().UnixMilli() + int64(atSeconds)*1000 atMS := time.Now().UnixMilli() + int64(atSeconds)*1000
schedule = cron.CronSchedule{ schedule = cron.CronSchedule{
Kind: "at", Kind: "at",
AtMS: &atMS, AtMS: &atMS,
} }
} else if hasEvery { } else if hasEvery {
everyMS := int64(everySeconds) * 1000 everyMS := int64(everySeconds) * 1000

View File

@@ -11,6 +11,7 @@ type MessageTool struct {
sendCallback SendCallback sendCallback SendCallback
defaultChannel string defaultChannel string
defaultChatID string defaultChatID string
sentInRound bool // Tracks whether a message was sent in the current processing round
} }
func NewMessageTool() *MessageTool { func NewMessageTool() *MessageTool {
@@ -49,6 +50,12 @@ func (t *MessageTool) Parameters() map[string]interface{} {
func (t *MessageTool) SetContext(channel, chatID string) { func (t *MessageTool) SetContext(channel, chatID string) {
t.defaultChannel = channel t.defaultChannel = channel
t.defaultChatID = chatID t.defaultChatID = chatID
t.sentInRound = false // Reset send tracking for new processing round
}
// HasSentInRound returns true if the message tool sent a message during the current round.
func (t *MessageTool) HasSentInRound() bool {
return t.sentInRound
} }
func (t *MessageTool) SetSendCallback(callback SendCallback) { func (t *MessageTool) SetSendCallback(callback SendCallback) {
@@ -87,9 +94,10 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{})
} }
} }
t.sentInRound = true
// Silent: user already received the message directly // Silent: user already received the message directly
return &ToolResult{ return &ToolResult{
ForLLM: fmt.Sprintf("Message sent to %s:%s", channel, chatID), ForLLM: fmt.Sprintf("Message sent to %s:%s", channel, chatID),
Silent: true, Silent: true,
} }
} }

View File

@@ -13,7 +13,6 @@ import (
"time" "time"
) )
type ExecTool struct { type ExecTool struct {
workingDir string workingDir string
timeout time.Duration timeout time.Duration

View File

@@ -22,27 +22,27 @@ type SubagentTask struct {
} }
type SubagentManager struct { type SubagentManager struct {
tasks map[string]*SubagentTask tasks map[string]*SubagentTask
mu sync.RWMutex mu sync.RWMutex
provider providers.LLMProvider provider providers.LLMProvider
defaultModel string defaultModel string
bus *bus.MessageBus bus *bus.MessageBus
workspace string workspace string
tools *ToolRegistry tools *ToolRegistry
maxIterations int maxIterations int
nextID int nextID int
} }
func NewSubagentManager(provider providers.LLMProvider, defaultModel, workspace string, bus *bus.MessageBus) *SubagentManager { func NewSubagentManager(provider providers.LLMProvider, defaultModel, workspace string, bus *bus.MessageBus) *SubagentManager {
return &SubagentManager{ return &SubagentManager{
tasks: make(map[string]*SubagentTask), tasks: make(map[string]*SubagentTask),
provider: provider, provider: provider,
defaultModel: defaultModel, defaultModel: defaultModel,
bus: bus, bus: bus,
workspace: workspace, workspace: workspace,
tools: NewToolRegistry(), tools: NewToolRegistry(),
maxIterations: 10, maxIterations: 10,
nextID: 1, nextID: 1,
} }
} }