feat(agents): reply as thread instead of top-level comment (#205)
* feat(agents): reply as thread instead of top-level comment When an agent responds to a user comment, the reply is now nested under the triggering comment (parent_id) instead of appearing as a separate top-level comment. Also enables on_comment trigger by default for newly created agents. - Add trigger_comment_id column to agent_task_queue (migration 028) - Pass triggering comment ID through EnqueueTaskForIssue → task → createAgentComment - Include parent_id in WebSocket broadcast for agent comments - Default agent creation includes both on_assign and on_comment triggers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(cli): add --parent flag to comment add for threaded replies The agent posts comments via the CLI, so the correct fix is giving it a --parent flag rather than wiring trigger_comment_id through the task infrastructure. The agent reads the comment list, decides which comment to reply to, and passes --parent <comment-id>. - Add --parent flag to `multica issue comment add` - Update agent runtime instructions to explain --parent usage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(daemon): pass trigger_comment_id to agent execution context The agent now knows which comment triggered its task and gets an explicit instruction to reply to it using --parent. The trigger_comment_id flows from the DB through the claim response, daemon Task struct, and into issue_context.md where the agent sees it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(comments): agent replies to thread root, matching frontend behavior When the triggering comment is itself a reply (has parent_id), resolve to the thread root so the agent's reply stays in the same flat thread. This matches the frontend where all replies share the top-level parent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(cli): show parent_id and full IDs in comment list The table output now includes a PARENT column and shows full comment IDs (not truncated) so agents can see thread structure and use --parent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(daemon): instruct agents to always use --output json Agents now see explicit guidance to use --output json for all read commands, ensuring they get structured data with full IDs and parent_id for proper threading. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(daemon): differentiate comment-trigger vs assign-trigger context When triggered by a comment, the agent now gets clear instructions: - Primary goal is to read and respond to the comment - Do NOT change issue status just because you replied - Only change status if explicitly requested This prevents the agent from seeing "In Review" and stopping, since it now understands the task is to reply, not to re-evaluate the issue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(daemon): split workflow by trigger type in CLAUDE.md/AGENTS.md The Workflow section in the agent's runtime config now shows a comment-reply workflow when triggered by a comment (read comments, find trigger, reply, don't change status) vs the full assignment workflow (set in_progress, do work, set in_review). Previously the agent always saw the assignment workflow, causing it to check the issue status, see "In Review", and stop without reading or replying to the triggering comment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(daemon): remove duplicate workflow from issue_context.md Workflow instructions now live only in CLAUDE.md/AGENTS.md (runtime_config.go). issue_context.md keeps just the task data: issue ID, trigger type, and triggering comment ID. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(task): skip duplicate comment on completion for comment-triggered tasks When triggered by a comment, the agent posts its own reply via CLI with --parent. The task completion path was also creating a comment from the agent's stdout output, resulting in duplicates. Now only assignment-triggered tasks auto-post output as a comment. Error messages from FailTask are still posted regardless of trigger type. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5517136d73
commit
961de18c97
15 changed files with 150 additions and 80 deletions
|
|
@ -150,7 +150,10 @@ function CreateAgentDialog({
|
|||
description: description.trim(),
|
||||
runtime_id: selectedRuntime.id,
|
||||
visibility,
|
||||
triggers: [{ id: generateId(), type: "on_assign", enabled: true, config: {} }],
|
||||
triggers: [
|
||||
{ id: generateId(), type: "on_assign", enabled: true, config: {} },
|
||||
{ id: generateId(), type: "on_comment", enabled: true, config: {} },
|
||||
],
|
||||
});
|
||||
onClose();
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ func init() {
|
|||
|
||||
// issue comment add
|
||||
issueCommentAddCmd.Flags().String("content", "", "Comment content (required)")
|
||||
issueCommentAddCmd.Flags().String("parent", "", "Parent comment ID (reply to a specific comment)")
|
||||
issueCommentAddCmd.Flags().String("output", "json", "Output format: table or json")
|
||||
}
|
||||
|
||||
|
|
@ -499,7 +500,7 @@ func runIssueCommentList(cmd *cobra.Command, args []string) error {
|
|||
return cli.PrintJSON(os.Stdout, comments)
|
||||
}
|
||||
|
||||
headers := []string{"ID", "AUTHOR", "TYPE", "CONTENT", "CREATED"}
|
||||
headers := []string{"ID", "PARENT", "AUTHOR", "TYPE", "CONTENT", "CREATED"}
|
||||
rows := make([][]string, 0, len(comments))
|
||||
for _, c := range comments {
|
||||
content := strVal(c, "content")
|
||||
|
|
@ -511,8 +512,13 @@ func runIssueCommentList(cmd *cobra.Command, args []string) error {
|
|||
if len(created) >= 16 {
|
||||
created = created[:16]
|
||||
}
|
||||
parentID := strVal(c, "parent_id")
|
||||
if parentID == "" {
|
||||
parentID = "—"
|
||||
}
|
||||
rows = append(rows, []string{
|
||||
truncateID(strVal(c, "id")),
|
||||
strVal(c, "id"),
|
||||
parentID,
|
||||
strVal(c, "author_type") + ":" + truncateID(strVal(c, "author_id")),
|
||||
strVal(c, "type"),
|
||||
content,
|
||||
|
|
@ -538,6 +544,9 @@ func runIssueCommentAdd(cmd *cobra.Command, args []string) error {
|
|||
defer cancel()
|
||||
|
||||
body := map[string]any{"content": content}
|
||||
if parentID, _ := cmd.Flags().GetString("parent"); parentID != "" {
|
||||
body["parent_id"] = parentID
|
||||
}
|
||||
var result map[string]any
|
||||
if err := client.PostJSON(ctx, "/api/issues/"+args[0]+"/comments", body, &result); err != nil {
|
||||
return fmt.Errorf("add comment: %w", err)
|
||||
|
|
|
|||
|
|
@ -737,6 +737,7 @@ func (d *Daemon) runTask(ctx context.Context, task Task, provider string, taskLo
|
|||
// via `multica repo checkout <url>`.
|
||||
taskCtx := execenv.TaskContextForEnv{
|
||||
IssueID: task.IssueID,
|
||||
TriggerCommentID: task.TriggerCommentID,
|
||||
AgentName: agentName,
|
||||
AgentInstructions: instructions,
|
||||
AgentSkills: convertSkillsForEnv(skills),
|
||||
|
|
|
|||
|
|
@ -113,8 +113,12 @@ func renderIssueContext(provider string, ctx TaskContextForEnv) string {
|
|||
b.WriteString("# Task Assignment\n\n")
|
||||
fmt.Fprintf(&b, "**Issue ID:** %s\n\n", ctx.IssueID)
|
||||
|
||||
b.WriteString("Run `multica issue get " + ctx.IssueID + " --output json` for full issue details and description.\n")
|
||||
b.WriteString("Run `multica issue comment list " + ctx.IssueID + "` for discussion history.\n\n")
|
||||
if ctx.TriggerCommentID != "" {
|
||||
b.WriteString("**Trigger:** Comment Reply\n")
|
||||
b.WriteString("**Triggering comment ID:** `" + ctx.TriggerCommentID + "`\n\n")
|
||||
} else {
|
||||
b.WriteString("**Trigger:** New Assignment\n\n")
|
||||
}
|
||||
|
||||
if len(ctx.AgentSkills) > 0 {
|
||||
b.WriteString("## Agent Skills\n\n")
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type PrepareParams struct {
|
|||
// TaskContextForEnv is the subset of task context used for writing context files.
|
||||
type TaskContextForEnv struct {
|
||||
IssueID string
|
||||
TriggerCommentID string // comment that triggered this task (empty for on_assign)
|
||||
AgentName string
|
||||
AgentInstructions string // agent identity/persona instructions, injected into CLAUDE.md
|
||||
AgentSkills []SkillContextForEnv
|
||||
|
|
|
|||
|
|
@ -42,15 +42,16 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
|||
}
|
||||
|
||||
b.WriteString("## Available Commands\n\n")
|
||||
b.WriteString("**Always use `--output json` for all read commands** to get structured data with full IDs.\n\n")
|
||||
b.WriteString("### Read\n")
|
||||
b.WriteString("- `multica issue get <id>` — Get full issue details (title, description, status, priority, assignee)\n")
|
||||
b.WriteString("- `multica issue list [--status X] [--priority X] [--assignee X]` — List issues in workspace\n")
|
||||
b.WriteString("- `multica issue comment list <issue-id>` — List all comments on an issue\n")
|
||||
b.WriteString("- `multica workspace get` — Get workspace details and context\n")
|
||||
b.WriteString("- `multica agent list` — List agents in workspace\n\n")
|
||||
b.WriteString("- `multica issue get <id> --output json` — Get full issue details (title, description, status, priority, assignee)\n")
|
||||
b.WriteString("- `multica issue list [--status X] [--priority X] [--assignee X] --output json` — List issues in workspace\n")
|
||||
b.WriteString("- `multica issue comment list <issue-id> --output json` — List all comments on an issue (includes id, parent_id for threading)\n")
|
||||
b.WriteString("- `multica workspace get --output json` — Get workspace details and context\n")
|
||||
b.WriteString("- `multica agent list --output json` — List agents in workspace\n\n")
|
||||
|
||||
b.WriteString("### Write\n")
|
||||
b.WriteString("- `multica issue comment add <issue-id> --content \"...\"` — Post a comment to an issue\n")
|
||||
b.WriteString("- `multica issue comment add <issue-id> --content \"...\" [--parent <comment-id>]` — Post a comment (use --parent to reply to a specific comment)\n")
|
||||
b.WriteString("- `multica issue status <id> <status>` — Update issue status (todo, in_progress, in_review, done, blocked)\n")
|
||||
b.WriteString("- `multica issue update <id> [--title X] [--description X] [--priority X]` — Update issue fields\n\n")
|
||||
|
||||
|
|
@ -71,26 +72,39 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
|||
b.WriteString("\nThe checkout command creates a git worktree with a dedicated branch. You can check out one or more repos as needed.\n\n")
|
||||
}
|
||||
|
||||
b.WriteString("### Workflow\n")
|
||||
b.WriteString("You are responsible for managing the issue status throughout your work.\n\n")
|
||||
fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand your task\n", ctx.IssueID)
|
||||
fmt.Fprintf(&b, "2. Run `multica issue status %s in_progress`\n", ctx.IssueID)
|
||||
b.WriteString("3. Read comments for additional context or human instructions\n")
|
||||
b.WriteString("4. If the task requires code changes:\n")
|
||||
if len(ctx.Repos) > 0 {
|
||||
b.WriteString(" a. Run `multica repo checkout <url>` to check out the appropriate repository\n")
|
||||
b.WriteString(" b. `cd` into the checked-out directory\n")
|
||||
b.WriteString(" c. Implement the changes and commit\n")
|
||||
b.WriteString("### Workflow\n\n")
|
||||
|
||||
if ctx.TriggerCommentID != "" {
|
||||
// Comment-triggered: focus on reading and replying
|
||||
b.WriteString("**This task was triggered by a comment.** Your primary job is to respond.\n\n")
|
||||
fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand the issue context\n", ctx.IssueID)
|
||||
fmt.Fprintf(&b, "2. Run `multica issue comment list %s --output json` to read the conversation\n", ctx.IssueID)
|
||||
fmt.Fprintf(&b, "3. Find the triggering comment (ID: `%s`) and understand what is being asked\n", ctx.TriggerCommentID)
|
||||
fmt.Fprintf(&b, "4. Reply: `multica issue comment add %s --parent %s --content \"...\"`\n", ctx.IssueID, ctx.TriggerCommentID)
|
||||
b.WriteString("5. If the comment requests code changes or further work, do the work first, then reply with your results\n")
|
||||
b.WriteString("6. Do NOT change the issue status unless the comment explicitly asks for it\n\n")
|
||||
} else {
|
||||
b.WriteString(" a. Create a new branch\n")
|
||||
b.WriteString(" b. Implement the changes and commit\n")
|
||||
// Assignment-triggered: full workflow
|
||||
b.WriteString("You are responsible for managing the issue status throughout your work.\n\n")
|
||||
fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand your task\n", ctx.IssueID)
|
||||
fmt.Fprintf(&b, "2. Run `multica issue status %s in_progress`\n", ctx.IssueID)
|
||||
b.WriteString("3. Read comments for additional context or human instructions\n")
|
||||
b.WriteString("4. If the task requires code changes:\n")
|
||||
if len(ctx.Repos) > 0 {
|
||||
b.WriteString(" a. Run `multica repo checkout <url>` to check out the appropriate repository\n")
|
||||
b.WriteString(" b. `cd` into the checked-out directory\n")
|
||||
b.WriteString(" c. Implement the changes and commit\n")
|
||||
} else {
|
||||
b.WriteString(" a. Create a new branch\n")
|
||||
b.WriteString(" b. Implement the changes and commit\n")
|
||||
}
|
||||
b.WriteString(" c. Push the branch to the remote\n")
|
||||
b.WriteString(" d. Create a pull request (decide the target branch based on the repo's conventions)\n")
|
||||
fmt.Fprintf(&b, " e. Post the PR link as a comment: `multica issue comment add %s --content \"PR: <url>\"`\n", ctx.IssueID)
|
||||
b.WriteString("5. If the task does not require code (e.g. research, documentation), post your findings as a comment\n")
|
||||
fmt.Fprintf(&b, "6. Run `multica issue status %s in_review`\n", ctx.IssueID)
|
||||
fmt.Fprintf(&b, "7. If blocked, run `multica issue status %s blocked` and post a comment explaining why\n\n", ctx.IssueID)
|
||||
}
|
||||
b.WriteString(" c. Push the branch to the remote\n")
|
||||
b.WriteString(" d. Create a pull request (decide the target branch based on the repo's conventions)\n")
|
||||
fmt.Fprintf(&b, " e. Post the PR link as a comment: `multica issue comment add %s --content \"PR: <url>\"`\n", ctx.IssueID)
|
||||
b.WriteString("5. If the task does not require code (e.g. research, documentation), post your findings as a comment\n")
|
||||
fmt.Fprintf(&b, "6. Run `multica issue status %s in_review`\n", ctx.IssueID)
|
||||
fmt.Fprintf(&b, "7. If blocked, run `multica issue status %s blocked` and post a comment explaining why\n\n", ctx.IssueID)
|
||||
|
||||
if len(ctx.AgentSkills) > 0 {
|
||||
b.WriteString("## Skills\n\n")
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ type Task struct {
|
|||
WorkspaceID string `json:"workspace_id"`
|
||||
Agent *AgentData `json:"agent,omitempty"`
|
||||
Repos []RepoData `json:"repos,omitempty"`
|
||||
PriorSessionID string `json:"prior_session_id,omitempty"` // Claude session ID from a previous task on this issue
|
||||
PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on this issue
|
||||
PriorSessionID string `json:"prior_session_id,omitempty"` // Claude session ID from a previous task on this issue
|
||||
PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on this issue
|
||||
TriggerCommentID string `json:"trigger_comment_id,omitempty"` // comment that triggered this task
|
||||
}
|
||||
|
||||
// AgentData holds agent details returned by the claim endpoint.
|
||||
|
|
|
|||
|
|
@ -104,8 +104,9 @@ type AgentTaskResponse struct {
|
|||
Agent *TaskAgentData `json:"agent,omitempty"`
|
||||
Repos []RepoData `json:"repos,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
PriorSessionID string `json:"prior_session_id,omitempty"` // session ID from a previous task on same issue
|
||||
PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on same issue
|
||||
PriorSessionID string `json:"prior_session_id,omitempty"` // session ID from a previous task on same issue
|
||||
PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on same issue
|
||||
TriggerCommentID *string `json:"trigger_comment_id,omitempty"` // comment that triggered this task
|
||||
}
|
||||
|
||||
// TaskAgentData holds agent info included in claim responses so the daemon
|
||||
|
|
@ -133,8 +134,9 @@ func taskToResponse(t db.AgentTaskQueue) AgentTaskResponse {
|
|||
StartedAt: timestampToPtr(t.StartedAt),
|
||||
CompletedAt: timestampToPtr(t.CompletedAt),
|
||||
Result: result,
|
||||
Error: textToPtr(t.Error),
|
||||
CreatedAt: timestampToString(t.CreatedAt),
|
||||
Error: textToPtr(t.Error),
|
||||
CreatedAt: timestampToString(t.CreatedAt),
|
||||
TriggerCommentID: uuidToPtr(t.TriggerCommentID),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,14 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) {
|
|||
// If the issue is assigned to an agent with on_comment trigger, enqueue a new task.
|
||||
// Skip when the comment comes from the assigned agent itself to avoid loops.
|
||||
if authorType == "member" && h.shouldEnqueueOnComment(r.Context(), issue) {
|
||||
if _, err := h.TaskService.EnqueueTaskForIssue(r.Context(), issue); err != nil {
|
||||
// Resolve thread root: if the comment is a reply, agent should reply
|
||||
// to the thread root (matching frontend behavior where all replies
|
||||
// in a thread share the same top-level parent).
|
||||
replyTo := comment.ID
|
||||
if comment.ParentID.Valid {
|
||||
replyTo = comment.ParentID
|
||||
}
|
||||
if _, err := h.TaskService.EnqueueTaskForIssue(r.Context(), issue, replyTo); err != nil {
|
||||
slog.Warn("enqueue agent task on comment failed", "issue_id", issueID, "error", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func NewTaskService(q *db.Queries, hub *realtime.Hub, bus *events.Bus) *TaskServ
|
|||
// EnqueueTaskForIssue creates a queued task for an agent-assigned issue.
|
||||
// No context snapshot is stored — the agent fetches all data it needs at
|
||||
// runtime via the multica CLI.
|
||||
func (s *TaskService) EnqueueTaskForIssue(ctx context.Context, issue db.Issue) (db.AgentTaskQueue, error) {
|
||||
func (s *TaskService) EnqueueTaskForIssue(ctx context.Context, issue db.Issue, triggerCommentID ...pgtype.UUID) (db.AgentTaskQueue, error) {
|
||||
if !issue.AssigneeID.Valid {
|
||||
slog.Error("task enqueue failed", "issue_id", util.UUIDToString(issue.ID), "error", "issue has no assignee")
|
||||
return db.AgentTaskQueue{}, fmt.Errorf("issue has no assignee")
|
||||
|
|
@ -48,11 +48,17 @@ func (s *TaskService) EnqueueTaskForIssue(ctx context.Context, issue db.Issue) (
|
|||
return db.AgentTaskQueue{}, fmt.Errorf("agent has no runtime")
|
||||
}
|
||||
|
||||
var commentID pgtype.UUID
|
||||
if len(triggerCommentID) > 0 {
|
||||
commentID = triggerCommentID[0]
|
||||
}
|
||||
|
||||
task, err := s.Queries.CreateAgentTask(ctx, db.CreateAgentTaskParams{
|
||||
AgentID: issue.AssigneeID,
|
||||
RuntimeID: agent.RuntimeID,
|
||||
IssueID: issue.ID,
|
||||
Priority: priorityToInt(issue.Priority),
|
||||
AgentID: issue.AssigneeID,
|
||||
RuntimeID: agent.RuntimeID,
|
||||
IssueID: issue.ID,
|
||||
Priority: priorityToInt(issue.Priority),
|
||||
TriggerCommentID: commentID,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("task enqueue failed", "issue_id", util.UUIDToString(issue.ID), "error", err)
|
||||
|
|
@ -185,10 +191,15 @@ func (s *TaskService) CompleteTask(ctx context.Context, taskID pgtype.UUID, resu
|
|||
|
||||
slog.Info("task completed", "task_id", util.UUIDToString(task.ID), "issue_id", util.UUIDToString(task.IssueID))
|
||||
|
||||
var payload protocol.TaskCompletedPayload
|
||||
if err := json.Unmarshal(result, &payload); err == nil {
|
||||
if payload.Output != "" {
|
||||
s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(payload.Output), "comment")
|
||||
// Post agent output as a comment, but only for assignment-triggered tasks.
|
||||
// Comment-triggered tasks: the agent replies via CLI with --parent, so
|
||||
// posting here would create a duplicate.
|
||||
if !task.TriggerCommentID.Valid {
|
||||
var payload protocol.TaskCompletedPayload
|
||||
if err := json.Unmarshal(result, &payload); err == nil {
|
||||
if payload.Output != "" {
|
||||
s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(payload.Output), "comment", task.TriggerCommentID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,7 +239,7 @@ func (s *TaskService) FailTask(ctx context.Context, taskID pgtype.UUID, errMsg s
|
|||
slog.Warn("task failed", "task_id", util.UUIDToString(task.ID), "issue_id", util.UUIDToString(task.IssueID), "error", errMsg)
|
||||
|
||||
if errMsg != "" {
|
||||
s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(errMsg), "system")
|
||||
s.createAgentComment(ctx, task.IssueID, task.AgentID, redact.Text(errMsg), "system", task.TriggerCommentID)
|
||||
}
|
||||
// Reconcile agent status
|
||||
s.ReconcileAgentStatus(ctx, task.AgentID)
|
||||
|
|
@ -401,7 +412,7 @@ func (s *TaskService) getIssuePrefix(workspaceID pgtype.UUID) string {
|
|||
return ws.IssuePrefix
|
||||
}
|
||||
|
||||
func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID pgtype.UUID, content, commentType string) {
|
||||
func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID pgtype.UUID, content, commentType string, parentID pgtype.UUID) {
|
||||
if content == "" {
|
||||
return
|
||||
}
|
||||
|
|
@ -411,6 +422,7 @@ func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID p
|
|||
AuthorID: agentID,
|
||||
Content: content,
|
||||
Type: commentType,
|
||||
ParentID: parentID,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
|
|
@ -433,6 +445,7 @@ func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID p
|
|||
"author_id": util.UUIDToString(comment.AuthorID),
|
||||
"content": comment.Content,
|
||||
"type": comment.Type,
|
||||
"parent_id": util.UUIDToPtr(comment.ParentID),
|
||||
"created_at": comment.CreatedAt.Time.Format("2006-01-02T15:04:05Z"),
|
||||
},
|
||||
"issue_title": issue.Title,
|
||||
|
|
|
|||
1
server/migrations/028_task_trigger_comment.down.sql
Normal file
1
server/migrations/028_task_trigger_comment.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE agent_task_queue DROP COLUMN trigger_comment_id;
|
||||
1
server/migrations/028_task_trigger_comment.up.sql
Normal file
1
server/migrations/028_task_trigger_comment.up.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE agent_task_queue ADD COLUMN trigger_comment_id UUID REFERENCES comment(id) ON DELETE SET NULL;
|
||||
|
|
@ -32,7 +32,7 @@ WHERE id = (
|
|||
LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED
|
||||
)
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id
|
||||
`
|
||||
|
||||
func (q *Queries) ClaimAgentTask(ctx context.Context, agentID pgtype.UUID) (AgentTaskQueue, error) {
|
||||
|
|
@ -54,6 +54,7 @@ func (q *Queries) ClaimAgentTask(ctx context.Context, agentID pgtype.UUID) (Agen
|
|||
&i.RuntimeID,
|
||||
&i.SessionID,
|
||||
&i.WorkDir,
|
||||
&i.TriggerCommentID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -62,7 +63,7 @@ const completeAgentTask = `-- name: CompleteAgentTask :one
|
|||
UPDATE agent_task_queue
|
||||
SET status = 'completed', completed_at = now(), result = $2, session_id = $3, work_dir = $4
|
||||
WHERE id = $1 AND status = 'running'
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id
|
||||
`
|
||||
|
||||
type CompleteAgentTaskParams struct {
|
||||
|
|
@ -96,6 +97,7 @@ func (q *Queries) CompleteAgentTask(ctx context.Context, arg CompleteAgentTaskPa
|
|||
&i.RuntimeID,
|
||||
&i.SessionID,
|
||||
&i.WorkDir,
|
||||
&i.TriggerCommentID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -177,16 +179,17 @@ func (q *Queries) CreateAgent(ctx context.Context, arg CreateAgentParams) (Agent
|
|||
}
|
||||
|
||||
const createAgentTask = `-- name: CreateAgentTask :one
|
||||
INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority)
|
||||
VALUES ($1, $2, $3, 'queued', $4)
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir
|
||||
INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority, trigger_comment_id)
|
||||
VALUES ($1, $2, $3, 'queued', $4, $5)
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id
|
||||
`
|
||||
|
||||
type CreateAgentTaskParams struct {
|
||||
AgentID pgtype.UUID `json:"agent_id"`
|
||||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||||
IssueID pgtype.UUID `json:"issue_id"`
|
||||
Priority int32 `json:"priority"`
|
||||
AgentID pgtype.UUID `json:"agent_id"`
|
||||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||||
IssueID pgtype.UUID `json:"issue_id"`
|
||||
Priority int32 `json:"priority"`
|
||||
TriggerCommentID pgtype.UUID `json:"trigger_comment_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateAgentTask(ctx context.Context, arg CreateAgentTaskParams) (AgentTaskQueue, error) {
|
||||
|
|
@ -195,6 +198,7 @@ func (q *Queries) CreateAgentTask(ctx context.Context, arg CreateAgentTaskParams
|
|||
arg.RuntimeID,
|
||||
arg.IssueID,
|
||||
arg.Priority,
|
||||
arg.TriggerCommentID,
|
||||
)
|
||||
var i AgentTaskQueue
|
||||
err := row.Scan(
|
||||
|
|
@ -213,6 +217,7 @@ func (q *Queries) CreateAgentTask(ctx context.Context, arg CreateAgentTaskParams
|
|||
&i.RuntimeID,
|
||||
&i.SessionID,
|
||||
&i.WorkDir,
|
||||
&i.TriggerCommentID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -230,7 +235,7 @@ const failAgentTask = `-- name: FailAgentTask :one
|
|||
UPDATE agent_task_queue
|
||||
SET status = 'failed', completed_at = now(), error = $2
|
||||
WHERE id = $1 AND status IN ('dispatched', 'running')
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id
|
||||
`
|
||||
|
||||
type FailAgentTaskParams struct {
|
||||
|
|
@ -257,6 +262,7 @@ func (q *Queries) FailAgentTask(ctx context.Context, arg FailAgentTaskParams) (A
|
|||
&i.RuntimeID,
|
||||
&i.SessionID,
|
||||
&i.WorkDir,
|
||||
&i.TriggerCommentID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -369,7 +375,7 @@ func (q *Queries) GetAgentInWorkspace(ctx context.Context, arg GetAgentInWorkspa
|
|||
}
|
||||
|
||||
const getAgentTask = `-- name: GetAgentTask :one
|
||||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir FROM agent_task_queue
|
||||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id FROM agent_task_queue
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
|
|
@ -392,6 +398,7 @@ func (q *Queries) GetAgentTask(ctx context.Context, id pgtype.UUID) (AgentTaskQu
|
|||
&i.RuntimeID,
|
||||
&i.SessionID,
|
||||
&i.WorkDir,
|
||||
&i.TriggerCommentID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -452,7 +459,7 @@ func (q *Queries) HasPendingTaskForIssue(ctx context.Context, issueID pgtype.UUI
|
|||
}
|
||||
|
||||
const listActiveTasksByIssue = `-- name: ListActiveTasksByIssue :many
|
||||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir FROM agent_task_queue
|
||||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id FROM agent_task_queue
|
||||
WHERE issue_id = $1 AND status IN ('dispatched', 'running')
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
|
@ -482,6 +489,7 @@ func (q *Queries) ListActiveTasksByIssue(ctx context.Context, issueID pgtype.UUI
|
|||
&i.RuntimeID,
|
||||
&i.SessionID,
|
||||
&i.WorkDir,
|
||||
&i.TriggerCommentID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -494,7 +502,7 @@ func (q *Queries) ListActiveTasksByIssue(ctx context.Context, issueID pgtype.UUI
|
|||
}
|
||||
|
||||
const listAgentTasks = `-- name: ListAgentTasks :many
|
||||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir FROM agent_task_queue
|
||||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id FROM agent_task_queue
|
||||
WHERE agent_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
|
@ -524,6 +532,7 @@ func (q *Queries) ListAgentTasks(ctx context.Context, agentID pgtype.UUID) ([]Ag
|
|||
&i.RuntimeID,
|
||||
&i.SessionID,
|
||||
&i.WorkDir,
|
||||
&i.TriggerCommentID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -580,7 +589,7 @@ func (q *Queries) ListAgents(ctx context.Context, workspaceID pgtype.UUID) ([]Ag
|
|||
}
|
||||
|
||||
const listPendingTasksByRuntime = `-- name: ListPendingTasksByRuntime :many
|
||||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir FROM agent_task_queue
|
||||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id FROM agent_task_queue
|
||||
WHERE runtime_id = $1 AND status IN ('queued', 'dispatched')
|
||||
ORDER BY priority DESC, created_at ASC
|
||||
`
|
||||
|
|
@ -610,6 +619,7 @@ func (q *Queries) ListPendingTasksByRuntime(ctx context.Context, runtimeID pgtyp
|
|||
&i.RuntimeID,
|
||||
&i.SessionID,
|
||||
&i.WorkDir,
|
||||
&i.TriggerCommentID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -622,7 +632,7 @@ func (q *Queries) ListPendingTasksByRuntime(ctx context.Context, runtimeID pgtyp
|
|||
}
|
||||
|
||||
const listTasksByIssue = `-- name: ListTasksByIssue :many
|
||||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir FROM agent_task_queue
|
||||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id FROM agent_task_queue
|
||||
WHERE issue_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
|
@ -652,6 +662,7 @@ func (q *Queries) ListTasksByIssue(ctx context.Context, issueID pgtype.UUID) ([]
|
|||
&i.RuntimeID,
|
||||
&i.SessionID,
|
||||
&i.WorkDir,
|
||||
&i.TriggerCommentID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -667,7 +678,7 @@ const startAgentTask = `-- name: StartAgentTask :one
|
|||
UPDATE agent_task_queue
|
||||
SET status = 'running', started_at = now()
|
||||
WHERE id = $1 AND status = 'dispatched'
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id
|
||||
`
|
||||
|
||||
func (q *Queries) StartAgentTask(ctx context.Context, id pgtype.UUID) (AgentTaskQueue, error) {
|
||||
|
|
@ -689,6 +700,7 @@ func (q *Queries) StartAgentTask(ctx context.Context, id pgtype.UUID) (AgentTask
|
|||
&i.RuntimeID,
|
||||
&i.SessionID,
|
||||
&i.WorkDir,
|
||||
&i.TriggerCommentID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,21 +61,22 @@ type AgentSkill struct {
|
|||
}
|
||||
|
||||
type AgentTaskQueue struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
AgentID pgtype.UUID `json:"agent_id"`
|
||||
IssueID pgtype.UUID `json:"issue_id"`
|
||||
Status string `json:"status"`
|
||||
Priority int32 `json:"priority"`
|
||||
DispatchedAt pgtype.Timestamptz `json:"dispatched_at"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||
Result []byte `json:"result"`
|
||||
Error pgtype.Text `json:"error"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
Context []byte `json:"context"`
|
||||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||||
SessionID pgtype.Text `json:"session_id"`
|
||||
WorkDir pgtype.Text `json:"work_dir"`
|
||||
ID pgtype.UUID `json:"id"`
|
||||
AgentID pgtype.UUID `json:"agent_id"`
|
||||
IssueID pgtype.UUID `json:"issue_id"`
|
||||
Status string `json:"status"`
|
||||
Priority int32 `json:"priority"`
|
||||
DispatchedAt pgtype.Timestamptz `json:"dispatched_at"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||
Result []byte `json:"result"`
|
||||
Error pgtype.Text `json:"error"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
Context []byte `json:"context"`
|
||||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||||
SessionID pgtype.Text `json:"session_id"`
|
||||
WorkDir pgtype.Text `json:"work_dir"`
|
||||
TriggerCommentID pgtype.UUID `json:"trigger_comment_id"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ WHERE agent_id = $1
|
|||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: CreateAgentTask :one
|
||||
INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority)
|
||||
VALUES ($1, $2, $3, 'queued', $4)
|
||||
INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority, trigger_comment_id)
|
||||
VALUES ($1, $2, $3, 'queued', $4, sqlc.narg(trigger_comment_id))
|
||||
RETURNING *;
|
||||
|
||||
-- name: CancelAgentTasksByIssue :exec
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue