mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 22:09:37 +08:00
Add automatic task
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -32,6 +33,10 @@ import (
|
||||
var errGatewayNotRunningSlash = errors.New("gateway not running")
|
||||
|
||||
const perSessionQueueSize = 64
|
||||
const autoLearnDefaultInterval = 10 * time.Minute
|
||||
const autoLearnMinInterval = 30 * time.Second
|
||||
const autonomyDefaultIdleInterval = 30 * time.Minute
|
||||
const autonomyMinIdleInterval = 1 * time.Minute
|
||||
|
||||
type sessionWorker struct {
|
||||
queue chan bus.InboundMessage
|
||||
@@ -39,6 +44,23 @@ type sessionWorker struct {
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type autoLearner struct {
|
||||
cancel context.CancelFunc
|
||||
started time.Time
|
||||
interval time.Duration
|
||||
rounds int
|
||||
}
|
||||
|
||||
type autonomySession struct {
|
||||
cancel context.CancelFunc
|
||||
started time.Time
|
||||
idleInterval time.Duration
|
||||
rounds int
|
||||
lastUserAt time.Time
|
||||
lastNudgeAt time.Time
|
||||
pending bool
|
||||
}
|
||||
|
||||
type AgentLoop struct {
|
||||
bus *bus.MessageBus
|
||||
provider providers.LLMProvider
|
||||
@@ -55,6 +77,44 @@ type AgentLoop struct {
|
||||
llmCallTimeout time.Duration
|
||||
workersMu sync.Mutex
|
||||
workers map[string]*sessionWorker
|
||||
autoLearnMu sync.Mutex
|
||||
autoLearners map[string]*autoLearner
|
||||
autonomyMu sync.Mutex
|
||||
autonomyBySess map[string]*autonomySession
|
||||
}
|
||||
|
||||
type taskExecutionDirectives struct {
|
||||
task string
|
||||
stageReport bool
|
||||
}
|
||||
|
||||
type autoLearnIntent struct {
|
||||
action string
|
||||
interval *time.Duration
|
||||
}
|
||||
|
||||
type autonomyIntent struct {
|
||||
action string
|
||||
idleInterval *time.Duration
|
||||
}
|
||||
|
||||
type stageReporter struct {
|
||||
onUpdate func(content string)
|
||||
}
|
||||
|
||||
func (sr *stageReporter) Publish(stage int, total int, status string, detail string) {
|
||||
if sr == nil || sr.onUpdate == nil {
|
||||
return
|
||||
}
|
||||
detail = strings.TrimSpace(detail)
|
||||
if detail == "" {
|
||||
detail = "-"
|
||||
}
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" {
|
||||
status = "进度更新"
|
||||
}
|
||||
sr.onUpdate(fmt.Sprintf("[进度] %s:%s", status, detail))
|
||||
}
|
||||
|
||||
func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers.LLMProvider, cs *cron.CronService) *AgentLoop {
|
||||
@@ -140,6 +200,8 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
compactionCfg: cfg.Agents.Defaults.ContextCompaction,
|
||||
llmCallTimeout: time.Duration(cfg.Providers.Proxy.TimeoutSec) * time.Second,
|
||||
workers: make(map[string]*sessionWorker),
|
||||
autoLearners: make(map[string]*autoLearner),
|
||||
autonomyBySess: make(map[string]*autonomySession),
|
||||
}
|
||||
|
||||
// 注入递归运行逻辑,使 subagent 具备 full tool-calling 能力
|
||||
@@ -158,11 +220,15 @@ func (al *AgentLoop) Run(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
al.stopAllWorkers()
|
||||
al.stopAllAutoLearners()
|
||||
al.stopAllAutonomySessions()
|
||||
return nil
|
||||
default:
|
||||
msg, ok := al.bus.ConsumeInbound(ctx)
|
||||
if !ok {
|
||||
al.stopAllWorkers()
|
||||
al.stopAllAutoLearners()
|
||||
al.stopAllAutonomySessions()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -181,6 +247,8 @@ func (al *AgentLoop) Run(ctx context.Context) error {
|
||||
func (al *AgentLoop) Stop() {
|
||||
al.running.Store(false)
|
||||
al.stopAllWorkers()
|
||||
al.stopAllAutoLearners()
|
||||
al.stopAllAutonomySessions()
|
||||
}
|
||||
|
||||
func isStopCommand(content string) bool {
|
||||
@@ -277,7 +345,7 @@ func (al *AgentLoop) runSessionWorker(ctx context.Context, sessionKey string, wo
|
||||
|
||||
if response != "" {
|
||||
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||
Buttons: nil,
|
||||
Buttons: nil,
|
||||
Channel: msg.Channel,
|
||||
ChatID: msg.ChatID,
|
||||
Content: response,
|
||||
@@ -320,6 +388,353 @@ func (al *AgentLoop) stopAllWorkers() {
|
||||
}
|
||||
}
|
||||
|
||||
func (al *AgentLoop) stopAllAutoLearners() {
|
||||
al.autoLearnMu.Lock()
|
||||
defer al.autoLearnMu.Unlock()
|
||||
|
||||
for sessionKey, learner := range al.autoLearners {
|
||||
if learner != nil && learner.cancel != nil {
|
||||
learner.cancel()
|
||||
}
|
||||
delete(al.autoLearners, sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
func (al *AgentLoop) stopAllAutonomySessions() {
|
||||
al.autonomyMu.Lock()
|
||||
defer al.autonomyMu.Unlock()
|
||||
|
||||
for sessionKey, s := range al.autonomyBySess {
|
||||
if s != nil && s.cancel != nil {
|
||||
s.cancel()
|
||||
}
|
||||
delete(al.autonomyBySess, sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
func (al *AgentLoop) startAutonomy(msg bus.InboundMessage, idleInterval time.Duration) string {
|
||||
if msg.Channel == "cli" {
|
||||
return "自主模式需要在 gateway 运行模式下使用(持续消息循环)。"
|
||||
}
|
||||
|
||||
if idleInterval <= 0 {
|
||||
idleInterval = autonomyDefaultIdleInterval
|
||||
}
|
||||
if idleInterval < autonomyMinIdleInterval {
|
||||
idleInterval = autonomyMinIdleInterval
|
||||
}
|
||||
|
||||
al.autonomyMu.Lock()
|
||||
if old, ok := al.autonomyBySess[msg.SessionKey]; ok {
|
||||
if old != nil && old.cancel != nil {
|
||||
old.cancel()
|
||||
}
|
||||
delete(al.autonomyBySess, msg.SessionKey)
|
||||
}
|
||||
|
||||
sessionCtx, cancel := context.WithCancel(context.Background())
|
||||
s := &autonomySession{
|
||||
cancel: cancel,
|
||||
started: time.Now(),
|
||||
idleInterval: idleInterval,
|
||||
lastUserAt: time.Now(),
|
||||
}
|
||||
al.autonomyBySess[msg.SessionKey] = s
|
||||
al.autonomyMu.Unlock()
|
||||
|
||||
go al.runAutonomyLoop(sessionCtx, msg)
|
||||
return fmt.Sprintf("自主模式已开启:自动拆解执行 + 阶段回报;空闲超过 %s 会主动推进并汇报。", idleInterval.Truncate(time.Second))
|
||||
}
|
||||
|
||||
func (al *AgentLoop) stopAutonomy(sessionKey string) bool {
|
||||
al.autonomyMu.Lock()
|
||||
defer al.autonomyMu.Unlock()
|
||||
|
||||
s, ok := al.autonomyBySess[sessionKey]
|
||||
if !ok || s == nil {
|
||||
return false
|
||||
}
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
}
|
||||
delete(al.autonomyBySess, sessionKey)
|
||||
return true
|
||||
}
|
||||
|
||||
func (al *AgentLoop) isAutonomyEnabled(sessionKey string) bool {
|
||||
al.autonomyMu.Lock()
|
||||
defer al.autonomyMu.Unlock()
|
||||
_, ok := al.autonomyBySess[sessionKey]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (al *AgentLoop) noteAutonomyUserActivity(msg bus.InboundMessage) {
|
||||
if isSyntheticMessage(msg) {
|
||||
return
|
||||
}
|
||||
al.autonomyMu.Lock()
|
||||
defer al.autonomyMu.Unlock()
|
||||
|
||||
s, ok := al.autonomyBySess[msg.SessionKey]
|
||||
if !ok || s == nil {
|
||||
return
|
||||
}
|
||||
s.lastUserAt = time.Now()
|
||||
}
|
||||
|
||||
func (al *AgentLoop) autonomyStatus(sessionKey string) string {
|
||||
al.autonomyMu.Lock()
|
||||
defer al.autonomyMu.Unlock()
|
||||
|
||||
s, ok := al.autonomyBySess[sessionKey]
|
||||
if !ok || s == nil {
|
||||
return "自主模式未开启。"
|
||||
}
|
||||
|
||||
uptime := time.Since(s.started).Truncate(time.Second)
|
||||
idle := time.Since(s.lastUserAt).Truncate(time.Second)
|
||||
return fmt.Sprintf("自主模式运行中:空闲阈值 %s,已运行 %s,最近用户活跃距今 %s,自动推进 %d 轮。",
|
||||
s.idleInterval.Truncate(time.Second),
|
||||
uptime,
|
||||
idle,
|
||||
s.rounds,
|
||||
)
|
||||
}
|
||||
|
||||
func (al *AgentLoop) runAutonomyLoop(ctx context.Context, msg bus.InboundMessage) {
|
||||
ticker := time.NewTicker(20 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if !al.maybeRunAutonomyRound(msg) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (al *AgentLoop) maybeRunAutonomyRound(msg bus.InboundMessage) bool {
|
||||
al.autonomyMu.Lock()
|
||||
s, ok := al.autonomyBySess[msg.SessionKey]
|
||||
if !ok || s == nil {
|
||||
al.autonomyMu.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if s.pending || now.Sub(s.lastUserAt) < s.idleInterval || now.Sub(s.lastNudgeAt) < s.idleInterval {
|
||||
al.autonomyMu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
s.rounds++
|
||||
round := s.rounds
|
||||
s.lastNudgeAt = now
|
||||
s.pending = true
|
||||
idleFor := now.Sub(s.lastUserAt).Truncate(time.Second)
|
||||
al.autonomyMu.Unlock()
|
||||
|
||||
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||
Channel: msg.Channel,
|
||||
ChatID: msg.ChatID,
|
||||
Content: fmt.Sprintf("[自主模式] 你已空闲 %s,我已启动第 %d 轮自主推进。", idleFor, round),
|
||||
})
|
||||
|
||||
al.bus.PublishInbound(bus.InboundMessage{
|
||||
Channel: msg.Channel,
|
||||
SenderID: "autonomy",
|
||||
ChatID: msg.ChatID,
|
||||
SessionKey: msg.SessionKey,
|
||||
Content: buildAutonomyFollowUpPrompt(round),
|
||||
Metadata: map[string]string{
|
||||
"source": "autonomy",
|
||||
"round": strconv.Itoa(round),
|
||||
},
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (al *AgentLoop) finishAutonomyRound(sessionKey string) {
|
||||
al.autonomyMu.Lock()
|
||||
defer al.autonomyMu.Unlock()
|
||||
if s, ok := al.autonomyBySess[sessionKey]; ok && s != nil {
|
||||
s.pending = false
|
||||
}
|
||||
}
|
||||
|
||||
func buildAutonomyFollowUpPrompt(round int) string {
|
||||
return fmt.Sprintf("自主模式第 %d 轮推进:用户暂时未继续输入。请基于当前会话上下文和已完成工作,自主完成一个高价值下一步,并给出简短进展汇报。", round)
|
||||
}
|
||||
|
||||
func (al *AgentLoop) startAutoLearner(msg bus.InboundMessage, interval time.Duration) string {
|
||||
if msg.Channel == "cli" {
|
||||
return "自动学习需要在 gateway 运行模式下使用(持续消息循环)。"
|
||||
}
|
||||
|
||||
if interval <= 0 {
|
||||
interval = autoLearnDefaultInterval
|
||||
}
|
||||
if interval < autoLearnMinInterval {
|
||||
interval = autoLearnMinInterval
|
||||
}
|
||||
|
||||
al.autoLearnMu.Lock()
|
||||
if old, ok := al.autoLearners[msg.SessionKey]; ok {
|
||||
if old != nil && old.cancel != nil {
|
||||
old.cancel()
|
||||
}
|
||||
delete(al.autoLearners, msg.SessionKey)
|
||||
}
|
||||
|
||||
learnerCtx, cancel := context.WithCancel(context.Background())
|
||||
learner := &autoLearner{
|
||||
cancel: cancel,
|
||||
started: time.Now(),
|
||||
interval: interval,
|
||||
}
|
||||
al.autoLearners[msg.SessionKey] = learner
|
||||
al.autoLearnMu.Unlock()
|
||||
|
||||
go al.runAutoLearnerLoop(learnerCtx, msg)
|
||||
|
||||
return fmt.Sprintf("自动学习已开启:每 %s 执行 1 轮。使用 /autolearn stop 可停止。", interval.Truncate(time.Second))
|
||||
}
|
||||
|
||||
func (al *AgentLoop) runAutoLearnerLoop(ctx context.Context, msg bus.InboundMessage) {
|
||||
runOnce := func() bool {
|
||||
round, ok := al.bumpAutoLearnRound(msg.SessionKey)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||
Channel: msg.Channel,
|
||||
ChatID: msg.ChatID,
|
||||
Content: fmt.Sprintf("[自动学习] 第 %d 轮开始。", round),
|
||||
})
|
||||
|
||||
al.bus.PublishInbound(bus.InboundMessage{
|
||||
Channel: msg.Channel,
|
||||
SenderID: "autolearn",
|
||||
ChatID: msg.ChatID,
|
||||
SessionKey: msg.SessionKey,
|
||||
Content: buildAutoLearnPrompt(round),
|
||||
Metadata: map[string]string{
|
||||
"source": "autolearn",
|
||||
"round": strconv.Itoa(round),
|
||||
},
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
if !runOnce() {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(al.autoLearnInterval(msg.SessionKey))
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if !runOnce() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (al *AgentLoop) autoLearnInterval(sessionKey string) time.Duration {
|
||||
al.autoLearnMu.Lock()
|
||||
defer al.autoLearnMu.Unlock()
|
||||
|
||||
learner, ok := al.autoLearners[sessionKey]
|
||||
if !ok || learner == nil || learner.interval <= 0 {
|
||||
return autoLearnDefaultInterval
|
||||
}
|
||||
return learner.interval
|
||||
}
|
||||
|
||||
func (al *AgentLoop) bumpAutoLearnRound(sessionKey string) (int, bool) {
|
||||
al.autoLearnMu.Lock()
|
||||
defer al.autoLearnMu.Unlock()
|
||||
|
||||
learner, ok := al.autoLearners[sessionKey]
|
||||
if !ok || learner == nil {
|
||||
return 0, false
|
||||
}
|
||||
learner.rounds++
|
||||
return learner.rounds, true
|
||||
}
|
||||
|
||||
func (al *AgentLoop) stopAutoLearner(sessionKey string) bool {
|
||||
al.autoLearnMu.Lock()
|
||||
defer al.autoLearnMu.Unlock()
|
||||
|
||||
learner, ok := al.autoLearners[sessionKey]
|
||||
if !ok || learner == nil {
|
||||
return false
|
||||
}
|
||||
if learner.cancel != nil {
|
||||
learner.cancel()
|
||||
}
|
||||
delete(al.autoLearners, sessionKey)
|
||||
return true
|
||||
}
|
||||
|
||||
func (al *AgentLoop) autoLearnerStatus(sessionKey string) string {
|
||||
al.autoLearnMu.Lock()
|
||||
defer al.autoLearnMu.Unlock()
|
||||
|
||||
learner, ok := al.autoLearners[sessionKey]
|
||||
if !ok || learner == nil {
|
||||
return "自动学习未开启。使用 /autolearn start 可开启。"
|
||||
}
|
||||
|
||||
uptime := time.Since(learner.started).Truncate(time.Second)
|
||||
return fmt.Sprintf("自动学习运行中:每 %s 一轮,已运行 %s,累计 %d 轮。",
|
||||
learner.interval.Truncate(time.Second),
|
||||
uptime,
|
||||
learner.rounds,
|
||||
)
|
||||
}
|
||||
|
||||
func buildAutoLearnPrompt(round int) string {
|
||||
return fmt.Sprintf("自动学习模式第 %d 轮:无需用户下发任务。请基于当前会话与项目上下文,自主选择一个高价值的小任务并完成。要求:1) 明确本轮学习目标;2) 必要时调用工具执行;3) 将关键结论写入 memory/MEMORY.md;4) 输出简短进展报告。", round)
|
||||
}
|
||||
|
||||
func buildAutonomyTaskPrompt(task string) string {
|
||||
return fmt.Sprintf("开启自主执行策略。请直接推进任务并在关键节点自然汇报进展,最后给出结果与下一步建议。\n\n用户任务:%s", strings.TrimSpace(task))
|
||||
}
|
||||
|
||||
func isSyntheticMessage(msg bus.InboundMessage) bool {
|
||||
if msg.SenderID == "autolearn" || msg.SenderID == "autonomy" {
|
||||
return true
|
||||
}
|
||||
if msg.Metadata == nil {
|
||||
return false
|
||||
}
|
||||
source := strings.ToLower(strings.TrimSpace(msg.Metadata["source"]))
|
||||
return source == "autolearn" || source == "autonomy"
|
||||
}
|
||||
|
||||
func isAutonomySyntheticMessage(msg bus.InboundMessage) bool {
|
||||
if msg.SenderID == "autonomy" {
|
||||
return true
|
||||
}
|
||||
if msg.Metadata == nil {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(msg.Metadata["source"]), "autonomy")
|
||||
}
|
||||
|
||||
func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey string) (string, error) {
|
||||
msg := bus.InboundMessage{
|
||||
Channel: "cli",
|
||||
@@ -343,16 +758,83 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
"session_key": msg.SessionKey,
|
||||
})
|
||||
|
||||
if isAutonomySyntheticMessage(msg) {
|
||||
defer al.finishAutonomyRound(msg.SessionKey)
|
||||
}
|
||||
|
||||
// Route system messages to processSystemMessage
|
||||
if msg.Channel == "system" {
|
||||
return al.processSystemMessage(ctx, msg)
|
||||
}
|
||||
|
||||
// Built-in slash commands (deterministic, no LLM roundtrip)
|
||||
if handled, result, err := al.handleSlashCommand(msg.Content); handled {
|
||||
if handled, result, err := al.handleSlashCommand(msg); handled {
|
||||
return result, err
|
||||
}
|
||||
|
||||
al.noteAutonomyUserActivity(msg)
|
||||
|
||||
if intent, ok := parseAutonomyIntent(msg.Content); ok {
|
||||
switch intent.action {
|
||||
case "start":
|
||||
idle := autonomyDefaultIdleInterval
|
||||
if intent.idleInterval != nil {
|
||||
idle = *intent.idleInterval
|
||||
}
|
||||
return al.startAutonomy(msg, idle), nil
|
||||
case "stop":
|
||||
if al.stopAutonomy(msg.SessionKey) {
|
||||
return "自主模式已关闭。", nil
|
||||
}
|
||||
return "自主模式当前未运行。", nil
|
||||
case "status":
|
||||
return al.autonomyStatus(msg.SessionKey), nil
|
||||
}
|
||||
}
|
||||
|
||||
if intent, ok := parseAutoLearnIntent(msg.Content); ok {
|
||||
switch intent.action {
|
||||
case "start":
|
||||
interval := autoLearnDefaultInterval
|
||||
if intent.interval != nil {
|
||||
interval = *intent.interval
|
||||
}
|
||||
return al.startAutoLearner(msg, interval), nil
|
||||
case "stop":
|
||||
if al.stopAutoLearner(msg.SessionKey) {
|
||||
return "自动学习已停止。", nil
|
||||
}
|
||||
return "自动学习当前未运行。", nil
|
||||
case "status":
|
||||
return al.autoLearnerStatus(msg.SessionKey), nil
|
||||
}
|
||||
}
|
||||
|
||||
directives := parseTaskExecutionDirectives(msg.Content)
|
||||
userPrompt := directives.task
|
||||
if strings.TrimSpace(userPrompt) == "" {
|
||||
userPrompt = msg.Content
|
||||
}
|
||||
if al.isAutonomyEnabled(msg.SessionKey) && !isSyntheticMessage(msg) {
|
||||
directives.stageReport = true
|
||||
userPrompt = buildAutonomyTaskPrompt(userPrompt)
|
||||
}
|
||||
|
||||
var progress *stageReporter
|
||||
if directives.stageReport {
|
||||
progress = &stageReporter{
|
||||
onUpdate: func(content string) {
|
||||
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||
Channel: msg.Channel,
|
||||
ChatID: msg.ChatID,
|
||||
Content: content,
|
||||
})
|
||||
},
|
||||
}
|
||||
progress.Publish(1, 5, "开始", "已接收任务")
|
||||
progress.Publish(2, 5, "分析", "正在构建上下文")
|
||||
}
|
||||
|
||||
// Update tool contexts
|
||||
if tool, ok := al.tools.Get("message"); ok {
|
||||
if mt, ok := tool.(*tools.MessageTool); ok {
|
||||
@@ -371,14 +853,21 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
messages := al.contextBuilder.BuildMessages(
|
||||
history,
|
||||
summary,
|
||||
msg.Content,
|
||||
userPrompt,
|
||||
nil,
|
||||
msg.Channel,
|
||||
msg.ChatID,
|
||||
)
|
||||
|
||||
finalContent, iteration, err := al.runLLMToolLoop(ctx, messages, msg.SessionKey, false)
|
||||
if progress != nil {
|
||||
progress.Publish(3, 5, "执行", "正在运行任务")
|
||||
}
|
||||
|
||||
finalContent, iteration, err := al.runLLMToolLoop(ctx, messages, msg.SessionKey, false, progress)
|
||||
if err != nil {
|
||||
if progress != nil {
|
||||
progress.Publish(5, 5, "失败", err.Error())
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -424,6 +913,11 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
logger.FieldUserResponseContentLength: len(userContent),
|
||||
})
|
||||
|
||||
if progress != nil {
|
||||
progress.Publish(4, 5, "收敛", "已生成最终回复")
|
||||
progress.Publish(5, 5, "完成", fmt.Sprintf("任务执行结束(迭代 %d)", iteration))
|
||||
}
|
||||
|
||||
return userContent, nil
|
||||
}
|
||||
|
||||
@@ -477,7 +971,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
|
||||
originChatID,
|
||||
)
|
||||
|
||||
finalContent, iteration, err := al.runLLMToolLoop(ctx, messages, sessionKey, true)
|
||||
finalContent, iteration, err := al.runLLMToolLoop(ctx, messages, sessionKey, true, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -518,6 +1012,7 @@ func (al *AgentLoop) runLLMToolLoop(
|
||||
messages []providers.Message,
|
||||
sessionKey string,
|
||||
systemMode bool,
|
||||
progress *stageReporter,
|
||||
) (string, int, error) {
|
||||
messages = sanitizeMessagesForToolCalling(messages)
|
||||
|
||||
@@ -527,6 +1022,9 @@ func (al *AgentLoop) runLLMToolLoop(
|
||||
|
||||
for iteration < al.maxIterations {
|
||||
iteration++
|
||||
if progress != nil {
|
||||
progress.Publish(3, 5, "执行", fmt.Sprintf("第 %d 轮推理", iteration))
|
||||
}
|
||||
|
||||
if !systemMode {
|
||||
logger.DebugCF("agent", "LLM iteration",
|
||||
@@ -654,6 +1152,13 @@ func (al *AgentLoop) runLLMToolLoop(
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("Error: %v", err)
|
||||
}
|
||||
if progress != nil {
|
||||
if err != nil {
|
||||
progress.Publish(3, 5, "执行", fmt.Sprintf("工具 %s 失败: %v", tc.Name, err))
|
||||
} else {
|
||||
progress.Publish(3, 5, "执行", fmt.Sprintf("工具 %s 完成", tc.Name))
|
||||
}
|
||||
}
|
||||
lastToolResult = result
|
||||
|
||||
toolResultMsg := providers.Message{
|
||||
@@ -1131,8 +1636,222 @@ func formatCompactionTranscript(messages []providers.Message, maxChars int) stri
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
func (al *AgentLoop) handleSlashCommand(content string) (bool, string, error) {
|
||||
func parseTaskExecutionDirectives(content string) taskExecutionDirectives {
|
||||
text := strings.TrimSpace(content)
|
||||
if text == "" {
|
||||
return taskExecutionDirectives{}
|
||||
}
|
||||
|
||||
directive := taskExecutionDirectives{
|
||||
task: text,
|
||||
}
|
||||
|
||||
fields := strings.Fields(text)
|
||||
if len(fields) > 0 {
|
||||
switch strings.ToLower(fields[0]) {
|
||||
case "/run", "@run":
|
||||
taskParts := make([]string, 0, len(fields)-1)
|
||||
for _, f := range fields[1:] {
|
||||
switch strings.ToLower(strings.TrimSpace(f)) {
|
||||
case "--stage-report":
|
||||
directive.stageReport = true
|
||||
continue
|
||||
case "--report=each-stage":
|
||||
directive.stageReport = true
|
||||
continue
|
||||
case "--report=off":
|
||||
directive.stageReport = false
|
||||
continue
|
||||
}
|
||||
taskParts = append(taskParts, f)
|
||||
}
|
||||
directive.task = strings.TrimSpace(strings.Join(taskParts, " "))
|
||||
return directive
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(text, "自动运行任务") {
|
||||
directive.stageReport = directive.stageReport || hasStageReportHint(text)
|
||||
start := strings.Index(text, "自动运行任务")
|
||||
task := strings.TrimSpace(text[start+len("自动运行任务"):])
|
||||
task = strings.TrimLeft(task, "::,,。;; ")
|
||||
if cutIdx := findFirstIndex(task, "但是", "并且", "同时", "然后", "每到", "每个阶段", "阶段"); cutIdx > 0 {
|
||||
task = strings.TrimSpace(task[:cutIdx])
|
||||
}
|
||||
task = strings.Trim(task, "::,,。;; ")
|
||||
if task != "" {
|
||||
directive.task = task
|
||||
}
|
||||
}
|
||||
|
||||
if directive.stageReport || hasStageReportHint(text) {
|
||||
directive.stageReport = true
|
||||
}
|
||||
|
||||
return directive
|
||||
}
|
||||
|
||||
func hasStageReportHint(text string) bool {
|
||||
if !strings.Contains(text, "阶段") {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(text, "报告") || strings.Contains(text, "汇报") || strings.Contains(strings.ToLower(text), "report")
|
||||
}
|
||||
|
||||
func findFirstIndex(text string, markers ...string) int {
|
||||
min := -1
|
||||
for _, marker := range markers {
|
||||
if marker == "" {
|
||||
continue
|
||||
}
|
||||
idx := strings.Index(text, marker)
|
||||
if idx >= 0 && (min == -1 || idx < min) {
|
||||
min = idx
|
||||
}
|
||||
}
|
||||
return min
|
||||
}
|
||||
|
||||
func parseAutoLearnInterval(raw string) (time.Duration, error) {
|
||||
text := strings.TrimSpace(raw)
|
||||
if text == "" {
|
||||
return autoLearnDefaultInterval, nil
|
||||
}
|
||||
if d, err := time.ParseDuration(text); err == nil {
|
||||
return d, nil
|
||||
}
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(text, "%d", &n); err == nil && n > 0 {
|
||||
return time.Duration(n) * time.Minute, nil
|
||||
}
|
||||
return 0, fmt.Errorf("invalid interval: %s (examples: 5m, 30s, 2h)", raw)
|
||||
}
|
||||
|
||||
func parseAutonomyIdleInterval(raw string) (time.Duration, error) {
|
||||
text := strings.TrimSpace(raw)
|
||||
if text == "" {
|
||||
return autonomyDefaultIdleInterval, nil
|
||||
}
|
||||
if d, err := time.ParseDuration(text); err == nil {
|
||||
return d, nil
|
||||
}
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(text, "%d", &n); err == nil && n > 0 {
|
||||
return time.Duration(n) * time.Minute, nil
|
||||
}
|
||||
return 0, fmt.Errorf("invalid idle interval: %s (examples: 30m, 1h)", raw)
|
||||
}
|
||||
|
||||
func parseAutonomyIntent(content string) (autonomyIntent, bool) {
|
||||
text := strings.TrimSpace(content)
|
||||
if text == "" {
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
|
||||
if strings.Contains(text, "停止自主模式") ||
|
||||
strings.Contains(text, "关闭自主模式") ||
|
||||
strings.Contains(text, "退出自主模式") ||
|
||||
strings.Contains(text, "别主动找我") ||
|
||||
strings.Contains(text, "不要主动找我") {
|
||||
return autonomyIntent{action: "stop"}, true
|
||||
}
|
||||
|
||||
if strings.Contains(text, "自主模式状态") ||
|
||||
strings.Contains(text, "查看自主模式") ||
|
||||
strings.Contains(text, "你现在是自主模式") {
|
||||
return autonomyIntent{action: "status"}, true
|
||||
}
|
||||
|
||||
hasAutoAction := strings.Contains(text, "自动拆解") ||
|
||||
strings.Contains(text, "自动执行") ||
|
||||
strings.Contains(text, "主动找我") ||
|
||||
strings.Contains(text, "不用我一直问") ||
|
||||
strings.Contains(text, "你自己推进")
|
||||
hasPersistentHint := strings.Contains(text, "从现在开始") ||
|
||||
strings.Contains(text, "以后") ||
|
||||
strings.Contains(text, "长期") ||
|
||||
strings.Contains(text, "持续") ||
|
||||
strings.Contains(text, "一直") ||
|
||||
strings.Contains(text, "我不理你") ||
|
||||
strings.Contains(text, "我不说话") ||
|
||||
strings.Contains(text, "空闲时")
|
||||
|
||||
startHint := strings.Contains(text, "开启自主模式") ||
|
||||
strings.Contains(text, "开始自主模式") ||
|
||||
strings.Contains(text, "进入自主模式") ||
|
||||
strings.Contains(text, "启用自主模式") ||
|
||||
strings.Contains(text, "切到自主模式") ||
|
||||
(hasAutoAction && hasPersistentHint)
|
||||
|
||||
if !startHint {
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
|
||||
if d, ok := extractChineseAutoLearnInterval(text); ok {
|
||||
return autonomyIntent{action: "start", idleInterval: &d}, true
|
||||
}
|
||||
return autonomyIntent{action: "start"}, true
|
||||
}
|
||||
|
||||
func parseAutoLearnIntent(content string) (autoLearnIntent, bool) {
|
||||
text := strings.TrimSpace(content)
|
||||
if text == "" || !strings.Contains(text, "自动学习") {
|
||||
return autoLearnIntent{}, false
|
||||
}
|
||||
|
||||
if strings.Contains(text, "停止自动学习") ||
|
||||
strings.Contains(text, "关闭自动学习") ||
|
||||
strings.Contains(text, "暂停自动学习") {
|
||||
return autoLearnIntent{action: "stop"}, true
|
||||
}
|
||||
|
||||
if strings.Contains(text, "自动学习状态") ||
|
||||
strings.Contains(text, "查看自动学习") ||
|
||||
strings.Contains(text, "自动学习还在") ||
|
||||
strings.Contains(text, "自动学习进度") {
|
||||
return autoLearnIntent{action: "status"}, true
|
||||
}
|
||||
|
||||
if strings.Contains(text, "开始自动学习") ||
|
||||
strings.Contains(text, "开启自动学习") ||
|
||||
strings.Contains(text, "启动自动学习") ||
|
||||
strings.Contains(text, "打开自动学习") {
|
||||
if d, ok := extractChineseAutoLearnInterval(text); ok {
|
||||
return autoLearnIntent{action: "start", interval: &d}, true
|
||||
}
|
||||
return autoLearnIntent{action: "start"}, true
|
||||
}
|
||||
|
||||
return autoLearnIntent{}, false
|
||||
}
|
||||
|
||||
func extractChineseAutoLearnInterval(text string) (time.Duration, bool) {
|
||||
patterns := []struct {
|
||||
re *regexp.Regexp
|
||||
unit time.Duration
|
||||
}{
|
||||
{re: regexp.MustCompile(`每\s*(\d+)\s*秒`), unit: time.Second},
|
||||
{re: regexp.MustCompile(`每\s*(\d+)\s*分钟`), unit: time.Minute},
|
||||
{re: regexp.MustCompile(`每\s*(\d+)\s*小时`), unit: time.Hour},
|
||||
}
|
||||
|
||||
for _, p := range patterns {
|
||||
m := p.re.FindStringSubmatch(text)
|
||||
if len(m) != 2 {
|
||||
continue
|
||||
}
|
||||
n, err := strconv.Atoi(m[1])
|
||||
if err != nil || n <= 0 {
|
||||
continue
|
||||
}
|
||||
return time.Duration(n) * p.unit, true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (al *AgentLoop) handleSlashCommand(msg bus.InboundMessage) (bool, string, error) {
|
||||
text := strings.TrimSpace(msg.Content)
|
||||
if !strings.HasPrefix(text, "/") {
|
||||
return false, "", nil
|
||||
}
|
||||
@@ -1144,7 +1863,7 @@ func (al *AgentLoop) handleSlashCommand(content string) (bool, string, error) {
|
||||
|
||||
switch fields[0] {
|
||||
case "/help":
|
||||
return true, "Slash commands:\n/help\n/status\n/config get <path>\n/config set <path> <value>\n/reload\n/pipeline list\n/pipeline status <pipeline_id>\n/pipeline ready <pipeline_id>", nil
|
||||
return true, "Slash commands:\n/help\n/status\n/run <task> [--stage-report]\n/autonomy start [idle]\n/autonomy stop\n/autonomy status\n/autolearn start [interval]\n/autolearn stop\n/autolearn status\n/config get <path>\n/config set <path> <value>\n/reload\n/pipeline list\n/pipeline status <pipeline_id>\n/pipeline ready <pipeline_id>", nil
|
||||
case "/stop":
|
||||
return true, "Stop command is handled by queue runtime. Send /stop from your channel session to interrupt current response.", nil
|
||||
case "/status":
|
||||
@@ -1158,6 +1877,56 @@ func (al *AgentLoop) handleSlashCommand(content string) (bool, string, error) {
|
||||
cfg.Logging.Enabled,
|
||||
al.getConfigPathForCommands(),
|
||||
), nil
|
||||
case "/autolearn":
|
||||
if len(fields) < 2 {
|
||||
return true, "Usage: /autolearn start [interval] | /autolearn stop | /autolearn status", nil
|
||||
}
|
||||
switch strings.ToLower(fields[1]) {
|
||||
case "start":
|
||||
interval := autoLearnDefaultInterval
|
||||
if len(fields) >= 3 {
|
||||
d, err := parseAutoLearnInterval(fields[2])
|
||||
if err != nil {
|
||||
return true, "", err
|
||||
}
|
||||
interval = d
|
||||
}
|
||||
return true, al.startAutoLearner(msg, interval), nil
|
||||
case "stop":
|
||||
if al.stopAutoLearner(msg.SessionKey) {
|
||||
return true, "自动学习已停止。", nil
|
||||
}
|
||||
return true, "自动学习当前未运行。", nil
|
||||
case "status":
|
||||
return true, al.autoLearnerStatus(msg.SessionKey), nil
|
||||
default:
|
||||
return true, "Usage: /autolearn start [interval] | /autolearn stop | /autolearn status", nil
|
||||
}
|
||||
case "/autonomy":
|
||||
if len(fields) < 2 {
|
||||
return true, "Usage: /autonomy start [idle] | /autonomy stop | /autonomy status", nil
|
||||
}
|
||||
switch strings.ToLower(fields[1]) {
|
||||
case "start":
|
||||
idle := autonomyDefaultIdleInterval
|
||||
if len(fields) >= 3 {
|
||||
d, err := parseAutonomyIdleInterval(fields[2])
|
||||
if err != nil {
|
||||
return true, "", err
|
||||
}
|
||||
idle = d
|
||||
}
|
||||
return true, al.startAutonomy(msg, idle), nil
|
||||
case "stop":
|
||||
if al.stopAutonomy(msg.SessionKey) {
|
||||
return true, "自主模式已关闭。", nil
|
||||
}
|
||||
return true, "自主模式当前未运行。", nil
|
||||
case "status":
|
||||
return true, al.autonomyStatus(msg.SessionKey), nil
|
||||
default:
|
||||
return true, "Usage: /autonomy start [idle] | /autonomy stop | /autonomy status", nil
|
||||
}
|
||||
case "/reload":
|
||||
running, err := al.triggerGatewayReloadFromAgent()
|
||||
if err != nil {
|
||||
|
||||
142
pkg/agent/loop_directive_test.go
Normal file
142
pkg/agent/loop_directive_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseTaskExecutionDirectives_RunCommand(t *testing.T) {
|
||||
d := parseTaskExecutionDirectives("/run 修复构建脚本 --stage-report")
|
||||
if d.task != "修复构建脚本" {
|
||||
t.Fatalf("unexpected task: %q", d.task)
|
||||
}
|
||||
if !d.stageReport {
|
||||
t.Fatalf("expected stage report enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTaskExecutionDirectives_NaturalLanguage(t *testing.T) {
|
||||
d := parseTaskExecutionDirectives("你可以自动运行任务:整理日志,但是每到一个阶段给我报告一下任务完成情况")
|
||||
if d.task != "整理日志" {
|
||||
t.Fatalf("unexpected task: %q", d.task)
|
||||
}
|
||||
if !d.stageReport {
|
||||
t.Fatalf("expected stage report enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTaskExecutionDirectives_Default(t *testing.T) {
|
||||
d := parseTaskExecutionDirectives("帮我看看今天的日志异常")
|
||||
if d.task != "帮我看看今天的日志异常" {
|
||||
t.Fatalf("unexpected task: %q", d.task)
|
||||
}
|
||||
if d.stageReport {
|
||||
t.Fatalf("expected stage report disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutoLearnInterval(t *testing.T) {
|
||||
d, err := parseAutoLearnInterval("5m")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if d != 5*time.Minute {
|
||||
t.Fatalf("unexpected duration: %s", d)
|
||||
}
|
||||
|
||||
d, err = parseAutoLearnInterval("2")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if d != 2*time.Minute {
|
||||
t.Fatalf("unexpected duration: %s", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutoLearnInterval_Invalid(t *testing.T) {
|
||||
if _, err := parseAutoLearnInterval("oops"); err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutoLearnIntent_StartNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutoLearnIntent("请开始自动学习,每5分钟执行一轮")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
if intent.action != "start" {
|
||||
t.Fatalf("unexpected action: %s", intent.action)
|
||||
}
|
||||
if intent.interval == nil || *intent.interval != 5*time.Minute {
|
||||
t.Fatalf("unexpected interval: %v", intent.interval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutoLearnIntent_StopNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutoLearnIntent("先暂停自动学习")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
if intent.action != "stop" {
|
||||
t.Fatalf("unexpected action: %s", intent.action)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutoLearnIntent_StatusNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutoLearnIntent("帮我看下自动学习状态")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
if intent.action != "status" {
|
||||
t.Fatalf("unexpected action: %s", intent.action)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutonomyIntent_StartNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutonomyIntent("以后你自动拆解并自动执行任务,每15分钟主动找我汇报一次")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
if intent.action != "start" {
|
||||
t.Fatalf("unexpected action: %s", intent.action)
|
||||
}
|
||||
if intent.idleInterval == nil || *intent.idleInterval != 15*time.Minute {
|
||||
t.Fatalf("unexpected interval: %v", intent.idleInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutonomyIntent_StopNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutonomyIntent("先不要主动找我,关闭自主模式")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
if intent.action != "stop" {
|
||||
t.Fatalf("unexpected action: %s", intent.action)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutonomyIntent_StatusNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutonomyIntent("帮我看下自主模式状态")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
if intent.action != "status" {
|
||||
t.Fatalf("unexpected action: %s", intent.action)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutonomyIdleInterval(t *testing.T) {
|
||||
d, err := parseAutonomyIdleInterval("45m")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if d != 45*time.Minute {
|
||||
t.Fatalf("unexpected duration: %s", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutonomyIntent_NoFalsePositiveOnSingleTask(t *testing.T) {
|
||||
if intent, ok := parseAutonomyIntent("请自动执行这个任务"); ok {
|
||||
t.Fatalf("expected no intent, got: %+v", intent)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user