Cap mention auto-replies at 15 comments / 30m; escalate floods to Mira.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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:
parent
adb6c5ab8a
commit
8e57341938
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user