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})
|
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
|
toolsEnabled := req.ToolsEnabled
|
||||||
maxIters := uint32(1)
|
maxIters := uint32(1)
|
||||||
maxCalls := uint32(0)
|
maxCalls := uint32(0)
|
||||||
@ -72,7 +77,9 @@ func (r *CoreRunner) RunAgent(ctx context.Context, req RunRequest) (RunResult, e
|
|||||||
TotalTimeoutSeconds: req.TimeoutSeconds,
|
TotalTimeoutSeconds: req.TimeoutSeconds,
|
||||||
},
|
},
|
||||||
ResponseContract: &corepb.AgentResponseContract{
|
ResponseContract: &corepb.AgentResponseContract{
|
||||||
Format: corepb.AgentOutputFormat_AGENT_OUTPUT_FORMAT_TEXT,
|
Format: format,
|
||||||
|
JsonSchema: req.JSONSchema,
|
||||||
|
RepairInvalidOutput: req.Repair,
|
||||||
},
|
},
|
||||||
RequestId: req.RequestID,
|
RequestId: req.RequestID,
|
||||||
IdempotencyKey: req.IdempotencyKey,
|
IdempotencyKey: req.IdempotencyKey,
|
||||||
|
|||||||
@ -13,6 +13,8 @@ type RunRequest struct {
|
|||||||
AgentID string
|
AgentID string
|
||||||
RequestID string
|
RequestID string
|
||||||
IdempotencyKey string
|
IdempotencyKey string
|
||||||
|
JSONSchema string
|
||||||
|
Repair bool
|
||||||
TimeoutSeconds uint32
|
TimeoutSeconds uint32
|
||||||
Metadata map[string]string
|
Metadata map[string]string
|
||||||
ToolsEnabled bool
|
ToolsEnabled bool
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package tasksboard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
@ -13,7 +14,13 @@ import (
|
|||||||
"l-ai-agent-kaya/internal/tasksws"
|
"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.
|
// Loop listens to Tasks board WS and replies to @kaya mentions.
|
||||||
type Loop struct {
|
type Loop struct {
|
||||||
@ -82,7 +89,7 @@ func (l *Loop) handle(ctx context.Context, ev tasksws.Event) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
taskID := ev.Task.ID
|
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)
|
replyCtx, cancel := context.WithTimeout(ctx, l.ResponseTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -96,6 +103,42 @@ func (l *Loop) handle(ctx context.Context, ev tasksws.Event) error {
|
|||||||
return fmt.Errorf("list comments: %w", err)
|
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)
|
userPrompt := buildUserPrompt(task, comments, commentID)
|
||||||
timeoutSec := uint32(l.ResponseTimeout.Seconds())
|
timeoutSec := uint32(l.ResponseTimeout.Seconds())
|
||||||
if timeoutSec < 30 {
|
if timeoutSec < 30 {
|
||||||
@ -109,9 +152,15 @@ func (l *Loop) handle(ctx context.Context, ev tasksws.Event) error {
|
|||||||
model = "deepseek-v4-flash"
|
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{
|
res, err := l.Core.RunAgent(replyCtx, inference.RunRequest{
|
||||||
Messages: []inference.ChatMessage{
|
Messages: []inference.ChatMessage{
|
||||||
{Role: "system", Content: l.Profile.SystemPrompt()},
|
{Role: "system", Content: sys},
|
||||||
{Role: "user", Content: userPrompt},
|
{Role: "user", Content: userPrompt},
|
||||||
},
|
},
|
||||||
Model: model,
|
Model: model,
|
||||||
@ -138,6 +187,54 @@ func (l *Loop) handle(ctx context.Context, ev tasksws.Event) error {
|
|||||||
return nil
|
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 {
|
func (l *Loop) markSeen(id string) bool {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
defer l.mu.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user