feat(trigger): 新增"任务创建"触发机制(on_task_create)

自定义 agent 触发条件从 5 种扩展到 6 种,新增任务被创建时触发。
用 tasks.id 单调水位线 + scheduler_state 防重复,重启不补触发;
触发消息完整分行给出任务ID/描述/目标。DB 加列并带 ALTER 迁移。

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Autumn-27andClaude Opus 4.8 committed 2026-08-09 02:19:29 -04:00
1 parent 5962826f2d
commit 4cf622593a
6 files changed
+128 -27

No files matched your search

+7 -3
View File
@@ -457,11 +457,13 @@ CREATE TABLE IF NOT EXISTS agent_triggers (
on_goal_met BOOLEAN NOT NULL DEFAULT false,
on_task_timeout BOOLEAN NOT NULL DEFAULT false,
on_tool_call BOOLEAN NOT NULL DEFAULT false,
on_task_create BOOLEAN NOT NULL DEFAULT false,
interval_message TEXT NOT NULL DEFAULT '',
finding_message TEXT NOT NULL DEFAULT '',
goal_message TEXT NOT NULL DEFAULT '',
task_timeout_message TEXT NOT NULL DEFAULT '',
tool_call_message TEXT NOT NULL DEFAULT '',
task_create_message TEXT NOT NULL DEFAULT '',
tool_names TEXT NOT NULL DEFAULT '',
last_fire TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
@@ -469,9 +471,11 @@ CREATE TABLE IF NOT EXISTS agent_triggers (
);
CREATE INDEX IF NOT EXISTS idx_agent_triggers_agent ON agent_triggers(agent_key);
-- 加列迁移(已发版,旧库升级补列;新库 CREATE 已含这些列,ALTER 为 no-op)。幂等,每次启动可重复执行。
ALTER TABLE agent_triggers ADD COLUMN IF NOT EXISTS on_tool_call BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE agent_triggers ADD COLUMN IF NOT EXISTS tool_call_message TEXT NOT NULL DEFAULT '';
ALTER TABLE agent_triggers ADD COLUMN IF NOT EXISTS tool_names TEXT NOT NULL DEFAULT '';
ALTER TABLE agent_triggers ADD COLUMN IF NOT EXISTS on_tool_call BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE agent_triggers ADD COLUMN IF NOT EXISTS tool_call_message TEXT NOT NULL DEFAULT '';
ALTER TABLE agent_triggers ADD COLUMN IF NOT EXISTS tool_names TEXT NOT NULL DEFAULT '';
ALTER TABLE agent_triggers ADD COLUMN IF NOT EXISTS on_task_create BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE agent_triggers ADD COLUMN IF NOT EXISTS task_create_message TEXT NOT NULL DEFAULT '';
DROP TRIGGER IF EXISTS trg_agent_triggers_upd ON agent_triggers;
CREATE TRIGGER trg_agent_triggers_upd BEFORE UPDATE ON agent_triggers
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
+41 -13
View File
@@ -6,10 +6,11 @@ import (
"time"
)
// AgentTrigger is one P3 trigger attached to a custom agent. Five trigger
// AgentTrigger is one P3 trigger attached to a custom agent. Six trigger
// conditions can be on at once: interval / on_finding / on_goal_met /
// on_task_timeout / on_tool_call. ToolNames scopes the tool-call trigger to a
// non-empty set of tool keys (empty is rejected at the API layer for on_tool_call).
// on_task_timeout / on_tool_call / on_task_create. ToolNames scopes the tool-call
// trigger to a non-empty set of tool keys (empty is rejected at the API layer for
// on_tool_call).
type AgentTrigger struct {
ID int64 `json:"id"`
AgentKey string `json:"agent_key"`
@@ -19,16 +20,18 @@ type AgentTrigger struct {
OnGoalMet bool `json:"on_goal_met"`
OnTaskTimeout bool `json:"on_task_timeout"`
OnToolCall bool `json:"on_tool_call"`
OnTaskCreate bool `json:"on_task_create"`
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"`
TaskCreateMessage string `json:"task_create_message"`
ToolNames []string `json:"tool_names"` // 选中的工具 key 列表(DB 存 JSON 文本)
LastFire *time.Time `json:"last_fire,omitempty"`
}
const triggerCols = `id, agent_key, enabled, interval_sec, on_finding, on_goal_met, on_task_timeout, on_tool_call, interval_message, finding_message, goal_message, task_timeout_message, tool_call_message, tool_names, last_fire`
const triggerCols = `id, agent_key, enabled, interval_sec, on_finding, on_goal_met, on_task_timeout, on_tool_call, on_task_create, interval_message, finding_message, goal_message, task_timeout_message, tool_call_message, task_create_message, tool_names, last_fire`
// marshalToolNames encodes the tool-key list as JSON text for the tool_names column.
// A nil/empty list stores "" (not "null"/"[]") so the column default stays clean.
@@ -47,8 +50,8 @@ func scanTrigger(sc interface{ Scan(...any) error }) (*AgentTrigger, error) {
var t AgentTrigger
var lf sql.NullTime
var toolNames string
if err := sc.Scan(&t.ID, &t.AgentKey, &t.Enabled, &t.IntervalSec, &t.OnFinding, &t.OnGoalMet, &t.OnTaskTimeout, &t.OnToolCall,
&t.IntervalMessage, &t.FindingMessage, &t.GoalMessage, &t.TaskTimeoutMessage, &t.ToolCallMessage, &toolNames, &lf); err != nil {
if err := sc.Scan(&t.ID, &t.AgentKey, &t.Enabled, &t.IntervalSec, &t.OnFinding, &t.OnGoalMet, &t.OnTaskTimeout, &t.OnToolCall, &t.OnTaskCreate,
&t.IntervalMessage, &t.FindingMessage, &t.GoalMessage, &t.TaskTimeoutMessage, &t.ToolCallMessage, &t.TaskCreateMessage, &toolNames, &lf); err != nil {
return nil, err
}
t.ToolNames = []string{}
@@ -64,18 +67,18 @@ func scanTrigger(sc interface{ Scan(...any) error }) (*AgentTrigger, error) {
// CreateTrigger inserts a trigger for agentKey and returns it.
func (d *DB) CreateTrigger(t *AgentTrigger) (*AgentTrigger, error) {
row := d.QueryRow(`
INSERT INTO agent_triggers(agent_key, enabled, interval_sec, on_finding, on_goal_met, on_task_timeout, on_tool_call, interval_message, finding_message, goal_message, task_timeout_message, tool_call_message, tool_names)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING `+triggerCols,
t.AgentKey, t.Enabled, t.IntervalSec, t.OnFinding, t.OnGoalMet, t.OnTaskTimeout, t.OnToolCall,
t.IntervalMessage, t.FindingMessage, t.GoalMessage, t.TaskTimeoutMessage, t.ToolCallMessage, marshalToolNames(t.ToolNames))
INSERT INTO agent_triggers(agent_key, enabled, interval_sec, on_finding, on_goal_met, on_task_timeout, on_tool_call, on_task_create, interval_message, finding_message, goal_message, task_timeout_message, tool_call_message, task_create_message, tool_names)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING `+triggerCols,
t.AgentKey, t.Enabled, t.IntervalSec, t.OnFinding, t.OnGoalMet, t.OnTaskTimeout, t.OnToolCall, t.OnTaskCreate,
t.IntervalMessage, t.FindingMessage, t.GoalMessage, t.TaskTimeoutMessage, t.ToolCallMessage, t.TaskCreateMessage, marshalToolNames(t.ToolNames))
return scanTrigger(row)
}
// UpdateTrigger updates a trigger's fields (not last_fire).
func (d *DB) UpdateTrigger(t *AgentTrigger) error {
_, err := d.Exec(`UPDATE agent_triggers SET enabled=$2, interval_sec=$3, on_finding=$4, on_goal_met=$5, on_task_timeout=$6, on_tool_call=$7, interval_message=$8, finding_message=$9, goal_message=$10, task_timeout_message=$11, tool_call_message=$12, tool_names=$13 WHERE id=$1`,
t.ID, t.Enabled, t.IntervalSec, t.OnFinding, t.OnGoalMet, t.OnTaskTimeout, t.OnToolCall,
t.IntervalMessage, t.FindingMessage, t.GoalMessage, t.TaskTimeoutMessage, t.ToolCallMessage, marshalToolNames(t.ToolNames))
_, err := d.Exec(`UPDATE agent_triggers SET enabled=$2, interval_sec=$3, on_finding=$4, on_goal_met=$5, on_task_timeout=$6, on_tool_call=$7, on_task_create=$8, interval_message=$9, finding_message=$10, goal_message=$11, task_timeout_message=$12, tool_call_message=$13, task_create_message=$14, tool_names=$15 WHERE id=$1`,
t.ID, t.Enabled, t.IntervalSec, t.OnFinding, t.OnGoalMet, t.OnTaskTimeout, t.OnToolCall, t.OnTaskCreate,
t.IntervalMessage, t.FindingMessage, t.GoalMessage, t.TaskTimeoutMessage, t.ToolCallMessage, t.TaskCreateMessage, marshalToolNames(t.ToolNames))
return err
}
@@ -210,6 +213,31 @@ ORDER BY id`, lastID)
return out, rows.Err()
}
// NewTasksSince returns tasks created with id > lastID (excluding deleted),
// ordered by id (monotonic watermark → no double-fire across restarts). Triggered
// agent runs are conversations, not tasks, so this never fires on its own output.
func (d *DB) NewTasksSince(lastID int64) ([]TaskEvent, error) {
rows, err := d.Query(`
SELECT id, description, goal FROM tasks
WHERE deleted_at IS NULL AND id > $1
ORDER BY id`, lastID)
if err != nil {
return nil, err
}
defer rows.Close()
out := []TaskEvent{}
for rows.Next() {
var e TaskEvent
if err := rows.Scan(&e.NodeID, &e.TaskDesc, &e.TaskGoal); err != nil {
return nil, err
}
e.TaskID = e.NodeID // task id doubles as NodeID for the watermark
e.Summary = e.TaskGoal
out = append(out, e)
}
return out, rows.Err()
}
// NewToolCallsSince returns completed tool calls (a tool_result row) with activity
// id > lastID across all live tasks, ordered by id (monotonic watermark → no
// double-fire). It is driven by tool_result rows (the tool finished, so both input