Merge pull request #175 from multica-ai/forrestchang/agent-comment-identity
fix(handler): attribute agent CLI actions to agent identity
This commit is contained in:
commit
cbf8f95d05
5 changed files with 52 additions and 10 deletions
|
|
@ -39,7 +39,12 @@ func newAPIClient(cmd *cobra.Command) (*cli.APIClient, error) {
|
|||
return nil, fmt.Errorf("server URL not set: use --server-url flag, MULTICA_SERVER_URL env, or 'multica config set server_url <url>'")
|
||||
}
|
||||
|
||||
return cli.NewAPIClient(serverURL, workspaceID, token), nil
|
||||
client := cli.NewAPIClient(serverURL, workspaceID, token)
|
||||
// When running inside a daemon task, attribute actions to the agent.
|
||||
if agentID := os.Getenv("MULTICA_AGENT_ID"); agentID != "" {
|
||||
client.AgentID = agentID
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func resolveServerURL(cmd *cobra.Command) string {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type APIClient struct {
|
|||
BaseURL string
|
||||
WorkspaceID string
|
||||
Token string
|
||||
AgentID string // When set, requests are attributed to this agent instead of the user.
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +42,9 @@ func (c *APIClient) setHeaders(req *http.Request) {
|
|||
if c.WorkspaceID != "" {
|
||||
req.Header.Set("X-Workspace-ID", c.WorkspaceID)
|
||||
}
|
||||
if c.AgentID != "" {
|
||||
req.Header.Set("X-Agent-ID", c.AgentID)
|
||||
}
|
||||
}
|
||||
|
||||
// GetJSON performs a GET request and decodes the JSON response.
|
||||
|
|
|
|||
|
|
@ -662,6 +662,7 @@ func (d *Daemon) runTask(ctx context.Context, task Task, provider string) (TaskR
|
|||
"MULTICA_DAEMON_PORT": fmt.Sprintf("%d", d.cfg.HealthPort),
|
||||
"MULTICA_WORKSPACE_ID": d.workspaceIDForRuntime(task.RuntimeID),
|
||||
"MULTICA_AGENT_NAME": agentName,
|
||||
"MULTICA_AGENT_ID": task.AgentID,
|
||||
"MULTICA_TASK_ID": task.ID,
|
||||
}
|
||||
// Point Codex to the per-task CODEX_HOME so it discovers skills natively
|
||||
|
|
|
|||
|
|
@ -101,10 +101,22 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// Determine author identity: agent (via X-Agent-ID header) or member.
|
||||
authorType := "member"
|
||||
authorID := userID
|
||||
if agentID := r.Header.Get("X-Agent-ID"); agentID != "" {
|
||||
// Validate the agent exists in this workspace.
|
||||
agent, err := h.Queries.GetAgent(r.Context(), parseUUID(agentID))
|
||||
if err == nil && uuidToString(agent.WorkspaceID) == uuidToString(issue.WorkspaceID) {
|
||||
authorType = "agent"
|
||||
authorID = agentID
|
||||
}
|
||||
}
|
||||
|
||||
comment, err := h.Queries.CreateComment(r.Context(), db.CreateCommentParams{
|
||||
IssueID: issue.ID,
|
||||
AuthorType: "member",
|
||||
AuthorID: parseUUID(userID),
|
||||
AuthorType: authorType,
|
||||
AuthorID: parseUUID(authorID),
|
||||
Content: req.Content,
|
||||
Type: req.Type,
|
||||
ParentID: parentID,
|
||||
|
|
@ -117,7 +129,7 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
resp := commentToResponse(comment)
|
||||
slog.Info("comment created", append(logger.RequestAttrs(r), "comment_id", uuidToString(comment.ID), "issue_id", issueID)...)
|
||||
h.publish(protocol.EventCommentCreated, uuidToString(issue.WorkspaceID), "member", userID, map[string]any{
|
||||
h.publish(protocol.EventCommentCreated, uuidToString(issue.WorkspaceID), authorType, authorID, map[string]any{
|
||||
"comment": resp,
|
||||
"issue_title": issue.Title,
|
||||
"issue_assignee_type": textToPtr(issue.AssigneeType),
|
||||
|
|
@ -126,8 +138,8 @@ 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.
|
||||
// The agent will resume its prior session and see this comment.
|
||||
if h.shouldEnqueueOnComment(r.Context(), issue) {
|
||||
// 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 {
|
||||
slog.Warn("enqueue agent task on comment failed", "issue_id", issueID, "error", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,6 +220,16 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Determine creator identity: agent (via X-Agent-ID header) or member.
|
||||
creatorType := "member"
|
||||
actualCreatorID := creatorID
|
||||
if agentID := r.Header.Get("X-Agent-ID"); agentID != "" {
|
||||
if agent, err := h.Queries.GetAgent(r.Context(), parseUUID(agentID)); err == nil && uuidToString(agent.WorkspaceID) == workspaceID {
|
||||
creatorType = "agent"
|
||||
actualCreatorID = agentID
|
||||
}
|
||||
}
|
||||
|
||||
issue, err := qtx.CreateIssue(r.Context(), db.CreateIssueParams{
|
||||
WorkspaceID: parseUUID(workspaceID),
|
||||
Title: req.Title,
|
||||
|
|
@ -228,8 +238,8 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
|
|||
Priority: priority,
|
||||
AssigneeType: assigneeType,
|
||||
AssigneeID: assigneeID,
|
||||
CreatorType: "member",
|
||||
CreatorID: parseUUID(creatorID),
|
||||
CreatorType: creatorType,
|
||||
CreatorID: parseUUID(actualCreatorID),
|
||||
ParentIssueID: parentIssueID,
|
||||
Position: 0,
|
||||
DueDate: dueDate,
|
||||
|
|
@ -249,7 +259,7 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
|
|||
prefix := h.getIssuePrefix(r.Context(), issue.WorkspaceID)
|
||||
resp := issueToResponse(issue, prefix)
|
||||
slog.Info("issue created", append(logger.RequestAttrs(r), "issue_id", uuidToString(issue.ID), "title", issue.Title, "status", issue.Status, "workspace_id", workspaceID)...)
|
||||
h.publish(protocol.EventIssueCreated, workspaceID, "member", creatorID, map[string]any{"issue": resp})
|
||||
h.publish(protocol.EventIssueCreated, workspaceID, creatorType, actualCreatorID, map[string]any{"issue": resp})
|
||||
|
||||
// Only ready issues in todo are enqueued for agents.
|
||||
if issue.AssigneeType.Valid && issue.AssigneeID.Valid {
|
||||
|
|
@ -371,7 +381,17 @@ func (h *Handler) UpdateIssue(w http.ResponseWriter, r *http.Request) {
|
|||
dueDateChanged := prevDueDate != resp.DueDate && (prevDueDate == nil) != (resp.DueDate == nil) ||
|
||||
(prevDueDate != nil && resp.DueDate != nil && *prevDueDate != *resp.DueDate)
|
||||
|
||||
h.publish(protocol.EventIssueUpdated, workspaceID, "member", userID, map[string]any{
|
||||
// Determine actor identity: agent (via X-Agent-ID header) or member.
|
||||
actorType := "member"
|
||||
actorID := userID
|
||||
if agentID := r.Header.Get("X-Agent-ID"); agentID != "" {
|
||||
if agent, err := h.Queries.GetAgent(r.Context(), parseUUID(agentID)); err == nil && uuidToString(agent.WorkspaceID) == workspaceID {
|
||||
actorType = "agent"
|
||||
actorID = agentID
|
||||
}
|
||||
}
|
||||
|
||||
h.publish(protocol.EventIssueUpdated, workspaceID, actorType, actorID, map[string]any{
|
||||
"issue": resp,
|
||||
"assignee_changed": assigneeChanged,
|
||||
"status_changed": statusChanged,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue