Cap mention auto-replies at 15 comments / 30m; escalate floods to Mira.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Human mentions always reply; agent→agent uses a decide gate; over the limit Kaya asks Mira to bring a human.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
alex 2026-09-21 23:55:35 +03:00
parent adb6c5ab8a
commit 8e57341938
3 changed files with 113 additions and 7 deletions

View File

@ -53,6 +53,11 @@ func (r *CoreRunner) RunAgent(ctx context.Context, req RunRequest) (RunResult, e
msgs = append(msgs, &corepb.StatelessMessage{Role: m.Role, Content: m.Content})
}
format := corepb.AgentOutputFormat_AGENT_OUTPUT_FORMAT_TEXT
if req.JSONSchema != "" {
format = corepb.AgentOutputFormat_AGENT_OUTPUT_FORMAT_JSON_SCHEMA
}
toolsEnabled := req.ToolsEnabled
maxIters := uint32(1)
maxCalls := uint32(0)
@ -72,7 +77,9 @@ func (r *CoreRunner) RunAgent(ctx context.Context, req RunRequest) (RunResult, e
TotalTimeoutSeconds: req.TimeoutSeconds,
},
ResponseContract: &corepb.AgentResponseContract{
Format: corepb.AgentOutputFormat_AGENT_OUTPUT_FORMAT_TEXT,
Format: format,
JsonSchema: req.JSONSchema,
RepairInvalidOutput: req.Repair,
},
RequestId: req.RequestID,
IdempotencyKey: req.IdempotencyKey,

View File

@ -13,6 +13,8 @@ type RunRequest struct {
AgentID string
RequestID string
IdempotencyKey string
JSONSchema string
Repair bool
TimeoutSeconds uint32
Metadata map[string]string
ToolsEnabled bool

View File

@ -2,6 +2,7 @@ package tasksboard
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strings"
@ -13,7 +14,13 @@ import (
"l-ai-agent-kaya/internal/tasksws"
)
const agentID = "kaya"
const (
agentID = "kaya"
floodMaxComments = 15
floodWindow = 30 * time.Minute
floodMarker = "[tasks-flood]"
)
// Loop listens to Tasks board WS and replies to @kaya mentions.
type Loop struct {
@ -26,9 +33,9 @@ type Loop struct {
ResponseTimeout time.Duration
Log *slog.Logger
mu sync.Mutex
seen map[string]struct{}
seenQ []string
mu sync.Mutex
seen map[string]struct{}
seenQ []string
maxSeen int
}
@ -82,7 +89,7 @@ func (l *Loop) handle(ctx context.Context, ev tasksws.Event) error {
}
taskID := ev.Task.ID
l.log().Info("mention reply", "comment_id", commentID, "task_id", taskID, "author", ev.Comment.AuthorID)
l.log().Info("mention reply", "comment_id", commentID, "task_id", taskID, "author", ev.Comment.AuthorID, "author_type", ev.Comment.AuthorType)
replyCtx, cancel := context.WithTimeout(ctx, l.ResponseTimeout)
defer cancel()
@ -96,6 +103,42 @@ func (l *Loop) handle(ctx context.Context, ev tasksws.Event) error {
return fmt.Errorf("list comments: %w", err)
}
mentionBody := ev.Comment.Body
if mentionBody == "" {
for _, c := range comments {
if c.ID == commentID {
mentionBody = c.Body
break
}
}
}
if strings.Contains(mentionBody, floodMarker) {
return nil
}
if recentCommentCount(comments, floodWindow) >= floodMaxComments {
l.log().Warn("tasks board flood limit", "task_id", taskID, "window", floodWindow, "max", floodMaxComments)
body := fmt.Sprintf(
"@mira %s flood: ≥%d comments in %s on this task — please tag the human to intervene. Agents are pausing auto-replies.",
floodMarker, floodMaxComments, floodWindow,
)
_, err := l.REST.PostComment(replyCtx, taskID, body, "flood-escalate:"+taskID)
return err
}
authorType := strings.ToLower(ev.Comment.AuthorType)
if authorType == "agent" {
ok, err := l.shouldReplyToAgentMention(replyCtx, task, commentID, mentionBody)
if err != nil {
return err
}
if !ok {
l.log().Info("tasks board skip agent→agent mention", "task_id", taskID, "comment_id", commentID)
return nil
}
}
userPrompt := buildUserPrompt(task, comments, commentID)
timeoutSec := uint32(l.ResponseTimeout.Seconds())
if timeoutSec < 30 {
@ -109,9 +152,15 @@ func (l *Loop) handle(ctx context.Context, ev tasksws.Event) error {
model = "deepseek-v4-flash"
}
sys := l.Profile.SystemPrompt()
sys += " Avoid @mentioning other agents unless you need a concrete action — never @mention just to acknowledge."
if authorType == "human" {
sys += " A human mentioned you — always give a real reply."
}
res, err := l.Core.RunAgent(replyCtx, inference.RunRequest{
Messages: []inference.ChatMessage{
{Role: "system", Content: l.Profile.SystemPrompt()},
{Role: "system", Content: sys},
{Role: "user", Content: userPrompt},
},
Model: model,
@ -138,6 +187,54 @@ func (l *Loop) handle(ctx context.Context, ev tasksws.Event) error {
return nil
}
func recentCommentCount(comments []Comment, window time.Duration) int {
cutoff := time.Now().UTC().Add(-window)
n := 0
for _, c := range comments {
if c.CreatedAt.IsZero() || !c.CreatedAt.Before(cutoff) {
n++
}
}
return n
}
const decideSchema = `{"type":"object","required":["reply"],"properties":{"reply":{"type":"boolean"},"reason":{"type":"string"}}}`
func (l *Loop) shouldReplyToAgentMention(ctx context.Context, task Task, mentionID, mentionBody string) (bool, error) {
model := l.Model
if model == "" {
model = "deepseek-v4-flash"
}
user := "Task: " + task.Title + "\nTrigger comment from another agent:\n" + trimRunes(mentionBody, 800) +
"\n\nReply true only if you must add new substance. Reply false for acknowledgements/thanks/repeats."
res, err := l.Core.RunAgent(ctx, inference.RunRequest{
Messages: []inference.ChatMessage{
{Role: "system", Content: "You gate Kaya's board replies to agent @mentions. Output JSON only."},
{Role: "user", Content: user},
},
Model: model,
AgentID: agentID,
RequestID: "mention-decide:" + mentionID,
IdempotencyKey: "mention-decide:" + mentionID,
JSONSchema: decideSchema,
Repair: true,
TimeoutSeconds: 45,
ToolsEnabled: false,
Metadata: map[string]string{"trace_reason": "kaya_tasks_mention_decide"},
})
if err != nil {
return false, err
}
var out struct {
Reply bool `json:"reply"`
}
if err := json.Unmarshal([]byte(strings.TrimSpace(res.Content)), &out); err != nil {
l.log().Warn("tasks board decide parse failed; skipping", "error", err)
return false, nil
}
return out.Reply, nil
}
func (l *Loop) markSeen(id string) bool {
l.mu.Lock()
defer l.mu.Unlock()