mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 13:06:20 +02:00
Two coordinated fixes for a quick-create case where the agent ended up
creating duplicate issues. Repro: user pasted an image into the
quick-create prompt; the front-end uploaded it and embedded the URL as
markdown in the user input; the agent saw the URL, assumed it was an
attachment, and ran `multica issue create … --attachment "https://…"`.
The CLI POSTed the issue first, then failed to read the URL as a file
(`os.ReadFile("https://…")`) and exited 1. The agent treated exit 1 as
"create failed" and retried — but the first issue already existed, so
the workspace ended up with two of them.
CLI (`server/cmd/multica/cmd_issue.go`):
- `runIssueCreate` pre-validates `--attachment` BEFORE POSTing. URLs are
warned about and skipped (they are never local files); local-path
read errors fail before the issue is created so no half-baked issue
lands. Once the POST succeeds, post-create upload failures only
print a stderr warning and the issue metadata is still emitted —
never a non-zero exit, so callers cannot mistake "attachment upload
hiccup" for "create failed" and retry.
- `runIssueCommentAdd` already uploads attachments BEFORE the comment
is created, so its failure mode is fine; it just gets the same
URL-skip behaviour for consistency.
Quick-create prompt (`buildQuickCreatePrompt`):
- Tells the agent NOT to pass `--attachment` for prompt-embedded image
URLs (they are already part of the description as markdown).
- Hardens the "no retry" rule: even on a non-zero exit, do not retry
`issue create` — the issue may already exist.
144 lines
8.6 KiB
Go
144 lines
8.6 KiB
Go
package daemon
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/multica-ai/multica/server/internal/daemon/execenv"
|
|
)
|
|
|
|
// BuildPrompt constructs the task prompt for an agent CLI.
|
|
// Keep this minimal — detailed instructions live in CLAUDE.md / AGENTS.md
|
|
// injected by execenv.InjectRuntimeConfig.
|
|
func BuildPrompt(task Task) string {
|
|
if task.ChatSessionID != "" {
|
|
return buildChatPrompt(task)
|
|
}
|
|
if task.TriggerCommentID != "" {
|
|
return buildCommentPrompt(task)
|
|
}
|
|
if task.AutopilotRunID != "" {
|
|
return buildAutopilotPrompt(task)
|
|
}
|
|
if task.QuickCreatePrompt != "" {
|
|
return buildQuickCreatePrompt(task)
|
|
}
|
|
var b strings.Builder
|
|
b.WriteString("You are running as a local coding agent for a Multica workspace.\n\n")
|
|
fmt.Fprintf(&b, "Your assigned issue ID is: %s\n\n", task.IssueID)
|
|
fmt.Fprintf(&b, "Start by running `multica issue get %s --output json` to understand your task, then complete it.\n", task.IssueID)
|
|
return b.String()
|
|
}
|
|
|
|
// buildQuickCreatePrompt constructs a prompt for quick-create tasks. The
|
|
// user typed a single natural-language sentence in the create-issue modal;
|
|
// the agent's only job is to translate it into one `multica issue create`
|
|
// CLI invocation. No issue exists yet, so the agent must NOT call
|
|
// `multica issue get` or attempt to comment — there's nothing to read or
|
|
// reply to.
|
|
func buildQuickCreatePrompt(task Task) string {
|
|
var b strings.Builder
|
|
b.WriteString("You are running as a quick-create assistant for a Multica workspace.\n\n")
|
|
b.WriteString("A user pressed the quick-create shortcut and typed a one-line description. There is NO existing issue. Your only job is to translate the description into a single `multica issue create` command and run it.\n\n")
|
|
fmt.Fprintf(&b, "User input:\n> %s\n\n", task.QuickCreatePrompt)
|
|
b.WriteString("Field rules:\n")
|
|
b.WriteString("- title: required. A short, imperative summary extracted from the user input (e.g. \"fix inbox loading\"). Strip filler words.\n")
|
|
b.WriteString("- description: optional. Include only if the user supplied detail beyond the title; otherwise omit. Never echo the title here.\n")
|
|
b.WriteString("- priority: one of `urgent`, `high`, `medium`, `low`, or omit. Map P0/P1 → urgent/high; \"asap\"/\"紧急\" → urgent; \"低优先级\" → low. If unspecified, omit.\n")
|
|
b.WriteString("- assignee:\n")
|
|
b.WriteString(" - When the user names someone (\"分给 X\" / \"assign to X\" / \"@X\"), call `multica workspace members --output json` and find the matching member by display name (case-insensitive substring match is fine). On a clean match, pass `--assignee <name>`. On no match or ambiguous match, do NOT pass `--assignee` — instead append a final line to the description: `未识别 assignee: X`.\n")
|
|
agentName := ""
|
|
if task.Agent != nil {
|
|
agentName = task.Agent.Name
|
|
}
|
|
if agentName != "" {
|
|
fmt.Fprintf(&b, " - When the user did NOT name an assignee, default to YOURSELF: pass `--assignee %q`. The picker agent is the expected owner because the user opened quick-create with you selected — never leave the issue unassigned.\n", agentName)
|
|
} else {
|
|
b.WriteString(" - When the user did NOT name an assignee, default to YOURSELF (the picker agent): pass `--assignee <your agent name>`. Never leave the issue unassigned.\n")
|
|
}
|
|
b.WriteString("- project: omit. The platform will route the issue to the workspace default.\n")
|
|
b.WriteString("- status: omit (defaults to `todo`).\n")
|
|
b.WriteString("- attachments: do NOT pass `--attachment`. The flag only accepts LOCAL file paths, and any image URL embedded in the user input is already part of the description as markdown — keep it inline in `--description` instead of trying to re-attach it. (Trying to pass `https://…` to `--attachment` will fail and look like a create error to you, but the issue may already exist; never retry `issue create` on that signal.)\n\n")
|
|
b.WriteString("Output format:\n")
|
|
b.WriteString("- Run exactly one `multica issue create` invocation. Do not retry it for any reason — even on a non-zero exit. The issue may already exist; another attempt would create a duplicate.\n")
|
|
b.WriteString("- After it succeeds, print exactly one line: `Created MUL-<n>: <title>` and exit. No commentary, no follow-up tool calls.\n")
|
|
b.WriteString("- Do NOT call `multica issue get` or `multica issue comment add` for this task — there is no issue to query or comment on prior to creation.\n")
|
|
b.WriteString("- If the CLI returns an error, exit with that error as the only output. The platform writes a failure notification automatically; do not retry.\n")
|
|
return b.String()
|
|
}
|
|
|
|
// buildCommentPrompt constructs a prompt for comment-triggered tasks.
|
|
// The triggering comment content is embedded directly so the agent cannot
|
|
// miss it, even when stale output files exist in a reused workdir.
|
|
// The reply instructions (including the current TriggerCommentID as --parent)
|
|
// are re-emitted on every turn so resumed sessions cannot carry forward a
|
|
// previous turn's --parent UUID.
|
|
func buildCommentPrompt(task Task) string {
|
|
var b strings.Builder
|
|
b.WriteString("You are running as a local coding agent for a Multica workspace.\n\n")
|
|
fmt.Fprintf(&b, "Your assigned issue ID is: %s\n\n", task.IssueID)
|
|
if task.TriggerCommentContent != "" {
|
|
authorLabel := "A user"
|
|
if task.TriggerAuthorType == "agent" {
|
|
name := task.TriggerAuthorName
|
|
if name == "" {
|
|
name = "another agent"
|
|
}
|
|
authorLabel = fmt.Sprintf("Another agent (%s)", name)
|
|
}
|
|
fmt.Fprintf(&b, "[NEW COMMENT] %s just left a new comment. Focus on THIS comment — do not confuse it with previous ones:\n\n", authorLabel)
|
|
fmt.Fprintf(&b, "> %s\n\n", task.TriggerCommentContent)
|
|
if task.TriggerAuthorType == "agent" {
|
|
b.WriteString("⚠️ The triggering comment was posted by another agent. Decide whether a reply is warranted. If you produced actual work this turn (investigated, fixed something, answered a real question), post the result as a normal reply — that is NOT a noise comment, and the standard rule that final results must be delivered via comment still applies. If the triggering comment was a pure acknowledgment, thanks, or sign-off AND you produced no work this turn, do NOT reply — and do NOT post a comment saying 'No reply needed' or similar. Simply exit with no output. Silence is the preferred way to end agent-to-agent threads. If you do reply, do not @mention the other agent as a sign-off (that re-triggers them and starts a loop).\n\n")
|
|
}
|
|
}
|
|
fmt.Fprintf(&b, "Start by running `multica issue get %s --output json` to understand your task, then decide how to proceed.\n\n", task.IssueID)
|
|
b.WriteString(execenv.BuildCommentReplyInstructions(task.IssueID, task.TriggerCommentID))
|
|
return b.String()
|
|
}
|
|
|
|
// buildChatPrompt constructs a prompt for interactive chat tasks.
|
|
func buildChatPrompt(task Task) string {
|
|
var b strings.Builder
|
|
b.WriteString("You are running as a chat assistant for a Multica workspace.\n")
|
|
b.WriteString("A user is chatting with you directly. Respond to their message.\n\n")
|
|
fmt.Fprintf(&b, "User message:\n%s\n", task.ChatMessage)
|
|
return b.String()
|
|
}
|
|
|
|
// buildAutopilotPrompt constructs a prompt for run_only autopilot tasks.
|
|
func buildAutopilotPrompt(task Task) string {
|
|
var b strings.Builder
|
|
b.WriteString("You are running as a local coding agent for a Multica workspace.\n\n")
|
|
b.WriteString("This task was triggered by an Autopilot in run-only mode. There is no assigned Multica issue for this run.\n\n")
|
|
fmt.Fprintf(&b, "Autopilot run ID: %s\n", task.AutopilotRunID)
|
|
if task.AutopilotID != "" {
|
|
fmt.Fprintf(&b, "Autopilot ID: %s\n", task.AutopilotID)
|
|
}
|
|
if task.AutopilotTitle != "" {
|
|
fmt.Fprintf(&b, "Autopilot title: %s\n", task.AutopilotTitle)
|
|
}
|
|
if task.AutopilotSource != "" {
|
|
fmt.Fprintf(&b, "Trigger source: %s\n", task.AutopilotSource)
|
|
}
|
|
if strings.TrimSpace(string(task.AutopilotTriggerPayload)) != "" {
|
|
fmt.Fprintf(&b, "Trigger payload:\n%s\n", strings.TrimSpace(string(task.AutopilotTriggerPayload)))
|
|
}
|
|
b.WriteString("\nAutopilot instructions:\n")
|
|
if strings.TrimSpace(task.AutopilotDescription) != "" {
|
|
b.WriteString(task.AutopilotDescription)
|
|
b.WriteString("\n\n")
|
|
} else if task.AutopilotTitle != "" {
|
|
fmt.Fprintf(&b, "%s\n\n", task.AutopilotTitle)
|
|
} else {
|
|
b.WriteString("No additional autopilot instructions were provided. Inspect the autopilot configuration before proceeding.\n\n")
|
|
}
|
|
if task.AutopilotID != "" {
|
|
fmt.Fprintf(&b, "Start by running `multica autopilot get %s --output json` if you need the full autopilot configuration, then complete the instructions above.\n", task.AutopilotID)
|
|
} else {
|
|
b.WriteString("Complete the instructions above.\n")
|
|
}
|
|
b.WriteString("Do not run `multica issue get`; this run does not have an issue ID.\n")
|
|
return b.String()
|
|
}
|