Merge pull request #314 from multica-ai/agent/lambda/0fa89561
feat(cli): add issue runs and run-messages commands
This commit is contained in:
commit
0659865645
4 changed files with 162 additions and 2 deletions
|
|
@ -289,6 +289,23 @@ multica issue comment add <issue-id> --parent <comment-id> --content "Thanks!"
|
|||
multica issue comment delete <comment-id>
|
||||
```
|
||||
|
||||
### Execution History
|
||||
|
||||
```bash
|
||||
# List all execution runs for an issue
|
||||
multica issue runs <issue-id>
|
||||
multica issue runs <issue-id> --output json
|
||||
|
||||
# View messages for a specific execution run
|
||||
multica issue run-messages <task-id>
|
||||
multica issue run-messages <task-id> --output json
|
||||
|
||||
# Incremental fetch (only messages after a given sequence number)
|
||||
multica issue run-messages <task-id> --since 42 --output json
|
||||
```
|
||||
|
||||
The `runs` command shows all past and current executions for an issue, including running tasks. The `run-messages` command shows the detailed message log (tool calls, thinking, text, errors) for a single run. Use `--since` for efficient polling of in-progress runs.
|
||||
|
||||
## Configuration
|
||||
|
||||
### View Config
|
||||
|
|
|
|||
|
|
@ -87,6 +87,22 @@ var issueCommentDeleteCmd = &cobra.Command{
|
|||
RunE: runIssueCommentDelete,
|
||||
}
|
||||
|
||||
// Execution history subcommands.
|
||||
|
||||
var issueRunsCmd = &cobra.Command{
|
||||
Use: "runs <issue-id>",
|
||||
Short: "List execution history for an issue",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runIssueRuns,
|
||||
}
|
||||
|
||||
var issueRunMessagesCmd = &cobra.Command{
|
||||
Use: "run-messages <task-id>",
|
||||
Short: "List messages for an execution",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runIssueRunMessages,
|
||||
}
|
||||
|
||||
var validIssueStatuses = []string{
|
||||
"backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled",
|
||||
}
|
||||
|
|
@ -99,6 +115,8 @@ func init() {
|
|||
issueCmd.AddCommand(issueAssignCmd)
|
||||
issueCmd.AddCommand(issueStatusCmd)
|
||||
issueCmd.AddCommand(issueCommentCmd)
|
||||
issueCmd.AddCommand(issueRunsCmd)
|
||||
issueCmd.AddCommand(issueRunMessagesCmd)
|
||||
|
||||
issueCommentCmd.AddCommand(issueCommentListCmd)
|
||||
issueCommentCmd.AddCommand(issueCommentAddCmd)
|
||||
|
|
@ -145,6 +163,13 @@ func init() {
|
|||
// issue comment list
|
||||
issueCommentListCmd.Flags().String("output", "table", "Output format: table or json")
|
||||
|
||||
// issue runs
|
||||
issueRunsCmd.Flags().String("output", "table", "Output format: table or json")
|
||||
|
||||
// issue run-messages
|
||||
issueRunMessagesCmd.Flags().String("output", "json", "Output format: table or json")
|
||||
issueRunMessagesCmd.Flags().Int("since", 0, "Only return messages after this sequence number")
|
||||
|
||||
// issue comment add
|
||||
issueCommentAddCmd.Flags().String("content", "", "Comment content (required)")
|
||||
issueCommentAddCmd.Flags().String("parent", "", "Parent comment ID (reply to a specific comment)")
|
||||
|
|
@ -625,6 +650,108 @@ func runIssueCommentDelete(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Execution history commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func runIssueRuns(cmd *cobra.Command, args []string) error {
|
||||
client, err := newAPIClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var runs []map[string]any
|
||||
if err := client.GetJSON(ctx, "/api/issues/"+args[0]+"/task-runs", &runs); err != nil {
|
||||
return fmt.Errorf("list runs: %w", err)
|
||||
}
|
||||
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
if output == "json" {
|
||||
return cli.PrintJSON(os.Stdout, runs)
|
||||
}
|
||||
|
||||
headers := []string{"ID", "AGENT", "STATUS", "STARTED", "COMPLETED", "ERROR"}
|
||||
rows := make([][]string, 0, len(runs))
|
||||
for _, r := range runs {
|
||||
started := strVal(r, "started_at")
|
||||
if len(started) >= 16 {
|
||||
started = started[:16]
|
||||
}
|
||||
completed := strVal(r, "completed_at")
|
||||
if len(completed) >= 16 {
|
||||
completed = completed[:16]
|
||||
}
|
||||
errMsg := strVal(r, "error")
|
||||
if utf8.RuneCountInString(errMsg) > 50 {
|
||||
runes := []rune(errMsg)
|
||||
errMsg = string(runes[:47]) + "..."
|
||||
}
|
||||
rows = append(rows, []string{
|
||||
truncateID(strVal(r, "id")),
|
||||
truncateID(strVal(r, "agent_id")),
|
||||
strVal(r, "status"),
|
||||
started,
|
||||
completed,
|
||||
errMsg,
|
||||
})
|
||||
}
|
||||
cli.PrintTable(os.Stdout, headers, rows)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runIssueRunMessages(cmd *cobra.Command, args []string) error {
|
||||
client, err := newAPIClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
path := "/api/daemon/tasks/" + args[0] + "/messages"
|
||||
if since, _ := cmd.Flags().GetInt("since"); since > 0 {
|
||||
path += fmt.Sprintf("?since=%d", since)
|
||||
}
|
||||
|
||||
var messages []map[string]any
|
||||
if err := client.GetJSON(ctx, path, &messages); err != nil {
|
||||
return fmt.Errorf("list run messages: %w", err)
|
||||
}
|
||||
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
if output == "json" {
|
||||
return cli.PrintJSON(os.Stdout, messages)
|
||||
}
|
||||
|
||||
headers := []string{"SEQ", "TYPE", "TOOL", "CONTENT"}
|
||||
rows := make([][]string, 0, len(messages))
|
||||
for _, m := range messages {
|
||||
content := strVal(m, "content")
|
||||
if content == "" {
|
||||
content = strVal(m, "output")
|
||||
}
|
||||
if utf8.RuneCountInString(content) > 80 {
|
||||
runes := []rune(content)
|
||||
content = string(runes[:77]) + "..."
|
||||
}
|
||||
seq := ""
|
||||
if v, ok := m["seq"]; ok {
|
||||
seq = fmt.Sprintf("%v", v)
|
||||
}
|
||||
rows = append(rows, []string{
|
||||
seq,
|
||||
strVal(m, "type"),
|
||||
strVal(m, "tool"),
|
||||
content,
|
||||
})
|
||||
}
|
||||
cli.PrintTable(os.Stdout, headers, rows)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
|||
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("- `multica agent list --output json` — List agents in workspace\n")
|
||||
b.WriteString("- `multica issue runs <issue-id> --output json` — List all execution runs for an issue (status, timestamps, errors)\n")
|
||||
b.WriteString("- `multica issue run-messages <task-id> [--since <seq>] --output json` — List messages for a specific execution run (supports incremental fetch)\n\n")
|
||||
|
||||
b.WriteString("### Write\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")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
|
@ -483,7 +484,20 @@ func (h *Handler) ListTaskMessages(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
messages, err := h.Queries.ListTaskMessages(r.Context(), parseUUID(taskID))
|
||||
var messages []db.TaskMessage
|
||||
if sinceStr := r.URL.Query().Get("since"); sinceStr != "" {
|
||||
sinceSeq, parseErr := strconv.Atoi(sinceStr)
|
||||
if parseErr != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid since parameter")
|
||||
return
|
||||
}
|
||||
messages, err = h.Queries.ListTaskMessagesSince(r.Context(), db.ListTaskMessagesSinceParams{
|
||||
TaskID: parseUUID(taskID),
|
||||
Seq: int32(sinceSeq),
|
||||
})
|
||||
} else {
|
||||
messages, err = h.Queries.ListTaskMessages(r.Context(), parseUUID(taskID))
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to list task messages")
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue