mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 22:17:48 +02:00
* feat(server): add quick-create issue async task path Adds POST /api/issues/quick-create which validates the picked agent's reachability up front (not archived, has runtime, runtime online) then queues an issue-less agent task whose context JSONB carries the user's natural-language prompt + requester + workspace. Daemon claim resolves the workspace from the context, and the prompt builder switches to a quick-create template instructing the agent to translate the prompt into a single multica issue create call. Task completion writes a success inbox item to the requester pointing at the newly-created issue (located by querying the agent's most recent issue in the workspace since task start, so we don't depend on agent stdout shape). Failures write an action_required inbox item carrying the original prompt + agent id so the frontend can offer "Edit as advanced form" without losing input. * feat(views): quick-create issue modal + inbox failure CTA Adds a streamlined create-issue UI bound to the c shortcut: pick an agent, type one line, submit. The modal closes immediately and the agent translates the prompt into a multica issue create call in the background. Shift+c keeps the legacy advanced form for users who want every field. The "Advanced" button inside the new modal seeds the shared issue-draft store with the prompt + picked agent so switching mid-flow doesn't lose input. Last-used agent persists per (user, workspace) via a workspace-aware zustand store so frequent users skip the picker on every open. Inbox renders quick_create_done items with a status pin to the new issue and quick_create_failed items with an "Edit as advanced form" CTA that re-seeds the legacy modal with the original prompt. ApiError now carries the parsed JSON body so the modal can branch on the structured agent_unavailable code without parsing the error message. * fix(quick-create): execenv injection, claim race, private-agent permission Addresses GPT-Boy review on #1786: 1. execenv was rendering the assignment-task issue_context.md / runtime workflow even for quick-create, telling the agent to call `multica issue get/status/comment add` against an empty IssueID. Adds QuickCreatePrompt to TaskContextForEnv, plus a quick-create branch in renderIssueContext + the runtime_config workflow that instructs the agent to run a single `multica issue create` and exit, with explicit "do NOT call issue get/status/comment add" guards. 2. ClaimAgentTask serialized only on issue_id / chat_session_id, so concurrent quick-creates on the same agent (both NULL on those columns) ran in parallel — making the success-inbox lookup race over "most recent issue by this agent". Adds a third OR clause that treats "all four FKs NULL" as a serialization key for the same agent, so quick-create tasks on a given agent run one at a time. 3. QuickCreateIssue handler bypassed the private-agent ownership rule that validateAssigneePair enforces elsewhere — a user could POST a private agent_id they didn't own and trigger it. Now routes the picked agent through validateAssigneePair before the runtime liveness check. 4. Clarifies the quick-create-store namespacing comment to match the actual workspace-aware StateStorage convention used by the other issue stores (per-user is browser-profile-local). * fix(quick-create): branch Output section + deterministic origin lookup Addresses GPT-Boy's second-pass review on #1786: 1. The runtime_config.go Output section forced "Final results MUST be delivered via multica issue comment add" for every non-autopilot task — quick-create still got this conflicting instruction even though there's no issue to comment on. Switched the Output block to a three-way switch so quick-create gets a tailored "stdout is captured automatically; do NOT call comment add" branch matching the autopilot variant. 2. Completion lookup was "most recent issue created by this agent since task.started_at", which races against concurrent issue creates by the same agent (assignment task running alongside quick-create when max_concurrent_tasks > 1). Replaced with a deterministic origin link: - Migration 060 extends issue.origin_type CHECK to allow 'quick_create'. - Daemon sets MULTICA_QUICK_CREATE_TASK_ID env var when running a quick-create task. - multica issue create CLI reads the env var and stamps the new issue with origin_type=quick_create + origin_id=<task_id>. - Server CreateIssue handler accepts (origin_type, origin_id) from trusted callers (only "quick_create" is allowed; the pair is rejected unless both fields are provided together). - notifyQuickCreateCompleted now calls GetIssueByOrigin keyed on (workspace_id, "quick_create", task.ID) — no more time-window racing against parallel agent activity. The old GetRecentIssueByCreatorSince query is removed.
133 lines
7.5 KiB
Go
133 lines
7.5 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: when the user says \"分给 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")
|
|
b.WriteString("- project: omit. The platform will route the issue to the workspace default.\n")
|
|
b.WriteString("- status: omit (defaults to `todo`).\n\n")
|
|
b.WriteString("Output format:\n")
|
|
b.WriteString("- Run exactly one `multica issue create` invocation.\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()
|
|
}
|