feat(agent): 触发器新增「工具调用」条件 + 定时触发改为复选框展开
- 定时触发统一为前置复选框,勾选后展开「每 N 秒」+ 消息(与其它条件一致) - 新增工具调用触发(P3 第 5 种条件): - 选中系统工具列表(至少一个),任务执行中这些工具每次调用完成即触发 agent - 触发消息带上任务 id/描述/目标 + 工具名 + 入参 + 返回内容(各截断 1500 字) - 只读任务执行的 activity,触发跑的会话写 conversation_activities,天然无自触发循环 - DB: agent_triggers 加 on_tool_call/tool_call_message/tool_names 三列; 已发版,附幂等 ALTER TABLE ADD COLUMN IF NOT EXISTS 供旧库升级补列 - scheduler: 新增 last_toolcall_id 水位线(首启 seed) + fireToolCalls,以 tool_result 为水位、回连 tool_use 取入参,按选中工具集合匹配后触发 Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
82bd19a1cd
commit
f9716269ea
6 files changed
+275
-51
No files matched your search
+75
-3
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
@@ -25,9 +26,10 @@ type Scheduler struct {
|
||||
}
|
||||
|
||||
const (
|
||||
schedKeyLastFinding = "last_finding_id" // watermark: max finding node id fired for
|
||||
schedKeyFiredGoals = "fired_goals" // JSON array of goal node ids already fired
|
||||
schedKeyLastTimeout = "last_timeout_id" // watermark: max task id fired for task timeout
|
||||
schedKeyLastFinding = "last_finding_id" // watermark: max finding node id fired for
|
||||
schedKeyFiredGoals = "fired_goals" // JSON array of goal node ids already fired
|
||||
schedKeyLastTimeout = "last_timeout_id" // watermark: max task id fired for task timeout
|
||||
schedKeyLastToolCall = "last_toolcall_id" // watermark: max activity id fired for tool call
|
||||
)
|
||||
|
||||
func newScheduler(s *Server) *Scheduler {
|
||||
@@ -86,6 +88,17 @@ func (sc *Scheduler) init() {
|
||||
}
|
||||
_ = sc.pg.SetSchedState(schedKeyLastTimeout, strconv.FormatInt(maxID, 10))
|
||||
}
|
||||
if sc.mustState(schedKeyLastToolCall) == "" {
|
||||
var maxID int64
|
||||
if evs, err := sc.pg.NewToolCallsSince(0); err == nil {
|
||||
for _, e := range evs {
|
||||
if e.NodeID > maxID {
|
||||
maxID = e.NodeID
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = sc.pg.SetSchedState(schedKeyLastToolCall, strconv.FormatInt(maxID, 10))
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *Scheduler) step() {
|
||||
@@ -100,6 +113,7 @@ func (sc *Scheduler) step() {
|
||||
sc.fireFindings(triggers)
|
||||
sc.fireGoals(triggers)
|
||||
sc.fireTaskTimeouts(triggers)
|
||||
sc.fireToolCalls(triggers)
|
||||
}
|
||||
|
||||
// fireIntervals fires triggers whose interval has elapsed since last_fire.
|
||||
@@ -215,6 +229,64 @@ func (sc *Scheduler) fireTaskTimeouts(triggers []*db.AgentTrigger) {
|
||||
_ = sc.pg.SetSchedState(schedKeyLastTimeout, strconv.FormatInt(maxID, 10))
|
||||
}
|
||||
|
||||
// fireToolCalls fires on_tool_call triggers for tool calls (tool_result rows) above
|
||||
// the persisted watermark whose tool name is in the trigger's selected set. The fire
|
||||
// message carries the task id/desc/goal + tool name + (truncated) input & output.
|
||||
func (sc *Scheduler) fireToolCalls(triggers []*db.AgentTrigger) {
|
||||
var want []*db.AgentTrigger
|
||||
for _, tr := range triggers {
|
||||
if tr.OnToolCall && len(tr.ToolNames) > 0 {
|
||||
want = append(want, tr)
|
||||
}
|
||||
}
|
||||
if len(want) == 0 {
|
||||
return
|
||||
}
|
||||
last, _ := strconv.ParseInt(sc.mustState(schedKeyLastToolCall), 10, 64)
|
||||
events, err := sc.pg.NewToolCallsSince(last)
|
||||
if err != nil || len(events) == 0 {
|
||||
return
|
||||
}
|
||||
maxID := last
|
||||
for _, e := range events {
|
||||
if e.NodeID > maxID {
|
||||
maxID = e.NodeID
|
||||
}
|
||||
errTag := ""
|
||||
if e.ToolIsErr {
|
||||
errTag = "[error] "
|
||||
}
|
||||
msgCtx := fmt.Sprintf("\n\n【本次由工具调用触发】\n任务: #%d %s(目标:%s)\n工具: %s\n入参: %s\n返回: %s%s",
|
||||
e.TaskID, e.TaskDesc, e.TaskGoal, e.Tool, trunc(e.ToolInput, 1500), errTag, trunc(e.ToolOutput, 1500))
|
||||
for _, tr := range want {
|
||||
if !containsFold(tr.ToolNames, e.Tool) {
|
||||
continue
|
||||
}
|
||||
sc.s.StartTriggeredRun(tr.AgentKey, fmt.Sprintf("工具触发 · %s · task#%d", e.Tool, e.TaskID), tr.ToolCallMessage+msgCtx, e.TaskID, true)
|
||||
}
|
||||
}
|
||||
_ = sc.pg.SetSchedState(schedKeyLastToolCall, strconv.FormatInt(maxID, 10))
|
||||
}
|
||||
|
||||
// containsFold reports whether name is in set (case-insensitive).
|
||||
func containsFold(set []string, name string) bool {
|
||||
for _, s := range set {
|
||||
if strings.EqualFold(s, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// trunc caps s to max runes, appending an ellipsis + original length when cut.
|
||||
func trunc(s string, max int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= max {
|
||||
return s
|
||||
}
|
||||
return string(r[:max]) + fmt.Sprintf("…(已截断,共 %d 字)", len(r))
|
||||
}
|
||||
|
||||
func (sc *Scheduler) mustState(key string) string {
|
||||
v, _ := sc.pg.GetSchedState(key)
|
||||
return v
|
||||
|
||||
+32
-15
@@ -22,15 +22,30 @@ func (s *Server) pgListTriggers(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type triggerReq struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
IntervalSec int `json:"interval_sec"`
|
||||
OnFinding bool `json:"on_finding"`
|
||||
OnGoalMet bool `json:"on_goal_met"`
|
||||
OnTaskTimeout bool `json:"on_task_timeout"`
|
||||
IntervalMessage string `json:"interval_message"`
|
||||
FindingMessage string `json:"finding_message"`
|
||||
GoalMessage string `json:"goal_message"`
|
||||
TaskTimeoutMessage string `json:"task_timeout_message"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IntervalSec int `json:"interval_sec"`
|
||||
OnFinding bool `json:"on_finding"`
|
||||
OnGoalMet bool `json:"on_goal_met"`
|
||||
OnTaskTimeout bool `json:"on_task_timeout"`
|
||||
OnToolCall bool `json:"on_tool_call"`
|
||||
IntervalMessage string `json:"interval_message"`
|
||||
FindingMessage string `json:"finding_message"`
|
||||
GoalMessage string `json:"goal_message"`
|
||||
TaskTimeoutMessage string `json:"task_timeout_message"`
|
||||
ToolCallMessage string `json:"tool_call_message"`
|
||||
ToolNames []string `json:"tool_names"`
|
||||
}
|
||||
|
||||
// validateTrigger enforces the shared trigger rules for create/update:
|
||||
// at least one condition, and on_tool_call requires a non-empty tool set.
|
||||
func validateTrigger(req *triggerReq) string {
|
||||
if req.IntervalSec == 0 && !req.OnFinding && !req.OnGoalMet && !req.OnTaskTimeout && !req.OnToolCall {
|
||||
return "至少选择一种触发条件(定时/发现finding/目标达成/任务超时/工具调用)"
|
||||
}
|
||||
if req.OnToolCall && len(req.ToolNames) == 0 {
|
||||
return "工具调用触发至少选择一个工具"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Server) pgCreateTrigger(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -50,15 +65,16 @@ func (s *Server) pgCreateTrigger(w http.ResponseWriter, r *http.Request) {
|
||||
if req.IntervalSec < 0 {
|
||||
req.IntervalSec = 0
|
||||
}
|
||||
if req.IntervalSec == 0 && !req.OnFinding && !req.OnGoalMet && !req.OnTaskTimeout {
|
||||
writeErr(w, 400, "至少选择一种触发条件(定时/发现finding/目标达成/任务超时)")
|
||||
if msg := validateTrigger(&req); msg != "" {
|
||||
writeErr(w, 400, msg)
|
||||
return
|
||||
}
|
||||
tr, err := pg.CreateTrigger(&db.AgentTrigger{
|
||||
AgentKey: a.Key, Enabled: req.Enabled, IntervalSec: req.IntervalSec,
|
||||
OnFinding: req.OnFinding, OnGoalMet: req.OnGoalMet, OnTaskTimeout: req.OnTaskTimeout,
|
||||
OnFinding: req.OnFinding, OnGoalMet: req.OnGoalMet, OnTaskTimeout: req.OnTaskTimeout, OnToolCall: req.OnToolCall,
|
||||
IntervalMessage: req.IntervalMessage, FindingMessage: req.FindingMessage,
|
||||
GoalMessage: req.GoalMessage, TaskTimeoutMessage: req.TaskTimeoutMessage,
|
||||
ToolCallMessage: req.ToolCallMessage, ToolNames: req.ToolNames,
|
||||
})
|
||||
if err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
@@ -85,15 +101,16 @@ func (s *Server) pgUpdateTrigger(w http.ResponseWriter, r *http.Request) {
|
||||
if req.IntervalSec < 0 {
|
||||
req.IntervalSec = 0
|
||||
}
|
||||
if req.IntervalSec == 0 && !req.OnFinding && !req.OnGoalMet && !req.OnTaskTimeout {
|
||||
writeErr(w, 400, "至少选择一种触发条件(定时/发现finding/目标达成/任务超时)")
|
||||
if msg := validateTrigger(&req); msg != "" {
|
||||
writeErr(w, 400, msg)
|
||||
return
|
||||
}
|
||||
if err := pg.UpdateTrigger(&db.AgentTrigger{
|
||||
ID: id, Enabled: req.Enabled, IntervalSec: req.IntervalSec,
|
||||
OnFinding: req.OnFinding, OnGoalMet: req.OnGoalMet, OnTaskTimeout: req.OnTaskTimeout,
|
||||
OnFinding: req.OnFinding, OnGoalMet: req.OnGoalMet, OnTaskTimeout: req.OnTaskTimeout, OnToolCall: req.OnToolCall,
|
||||
IntervalMessage: req.IntervalMessage, FindingMessage: req.FindingMessage,
|
||||
GoalMessage: req.GoalMessage, TaskTimeoutMessage: req.TaskTimeoutMessage,
|
||||
ToolCallMessage: req.ToolCallMessage, ToolNames: req.ToolNames,
|
||||
}); err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user