diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 53a77ac..5c322f1 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -855,7 +855,9 @@ func (al *AgentLoop) clearWorkerCancel(worker *sessionWorker) { } func (al *AgentLoop) formatProcessingErrorMessage(ctx context.Context, msg bus.InboundMessage, err error) string { - return al.localizeUserFacingText(ctx, msg.SessionKey, msg.Content, fmt.Sprintf("Error processing message: %v", err)) + return al.renderControlReply(withUserLanguageHint(ctx, msg.SessionKey, msg.Content), "message_processing_error", map[string]interface{}{ + "error": strings.TrimSpace(err.Error()), + }, "消息处理失败。") } func (al *AgentLoop) preferChineseUserFacingText(sessionKey, currentContent string) bool { @@ -1000,9 +1002,16 @@ func (al *AgentLoop) startAutonomy(ctx context.Context, msg bus.InboundMessage, }) } if s.focus != "" { - return al.naturalizeUserFacingText(ctx, fmt.Sprintf("Autonomy mode is enabled. Current focus: %s. The system will continue in the background and report progress or results every %s.", s.focus, idleInterval.Truncate(time.Second))) + return al.renderControlReply(ctx, "autonomy_started", map[string]interface{}{ + "focus": s.focus, + "report_interval": idleInterval.Truncate(time.Second).String(), + "mode": "background", + }, "自主模式已开启。") } - return al.naturalizeUserFacingText(ctx, fmt.Sprintf("Autonomy mode is enabled: automatic decomposition + background execution; reports progress or results every %s.", idleInterval.Truncate(time.Second))) + return al.renderControlReply(ctx, "autonomy_started", map[string]interface{}{ + "report_interval": idleInterval.Truncate(time.Second).String(), + "mode": "background", + }, "自主模式已开启。") } func (al *AgentLoop) stopAutonomy(sessionKey string) bool { @@ -1062,19 +1071,15 @@ func (al *AgentLoop) autonomyStatus(ctx context.Context, sessionKey string) stri return al.naturalizeUserFacingText(ctx, "Autonomy mode is not enabled.") } - uptime := time.Since(s.started).Truncate(time.Second) - idle := time.Since(s.lastUserAt).Truncate(time.Second) - focus := strings.TrimSpace(s.focus) - if focus == "" { - focus = "not set" - } - fallback := fmt.Sprintf("Autonomy mode is running: report interval %s, uptime %s, time since last user activity %s, automatic rounds %d.", - s.idleInterval.Truncate(time.Second), - uptime, - idle, - s.rounds, - ) + fmt.Sprintf(" Current focus: %s.", focus) - return al.naturalizeUserFacingText(ctx, fallback) + return al.renderControlReply(ctx, "autonomy_status", map[string]interface{}{ + "report_interval": s.idleInterval.Truncate(time.Second).String(), + "uptime": time.Since(s.started).Truncate(time.Second).String(), + "idle_since_user": time.Since(s.lastUserAt).Truncate(time.Second).String(), + "automatic_rounds": s.rounds, + "current_focus": strings.TrimSpace(s.focus), + "autonomy_running": true, + "session_has_focus": strings.TrimSpace(s.focus) != "", + }, "自主模式运行中。") } func (al *AgentLoop) runAutonomyLoop(ctx context.Context, msg bus.InboundMessage) { @@ -1245,7 +1250,9 @@ func (al *AgentLoop) startAutoLearner(ctx context.Context, msg bus.InboundMessag go al.runAutoLearnerLoop(learnerCtx, msg) - return al.naturalizeUserFacingText(ctx, fmt.Sprintf("Auto-learn is enabled: one round every %s. Tell me in natural language whenever you want to stop it.", interval.Truncate(time.Second))) + return al.renderControlReply(ctx, "autolearn_started", map[string]interface{}{ + "interval": interval.Truncate(time.Second).String(), + }, "自动学习已开启。") } func (al *AgentLoop) runAutoLearnerLoop(ctx context.Context, msg bus.InboundMessage) { @@ -1269,7 +1276,9 @@ func (al *AgentLoop) runAutoLearnerLoop(ctx context.Context, msg bus.InboundMess al.bus.PublishOutbound(bus.OutboundMessage{ Channel: msg.Channel, ChatID: msg.ChatID, - Content: al.naturalizeUserFacingText(ctx, fmt.Sprintf("Auto-learn round %d started.", round)), + Content: al.renderControlReply(ctx, "autolearn_round_started", map[string]interface{}{ + "round": round, + }, "自动学习新一轮已开始。"), }) al.bus.PublishInbound(bus.InboundMessage{ @@ -1352,13 +1361,12 @@ func (al *AgentLoop) autoLearnerStatus(ctx context.Context, sessionKey string) s return al.naturalizeUserFacingText(ctx, "Auto-learn is not enabled.") } - uptime := time.Since(learner.started).Truncate(time.Second) - fallback := fmt.Sprintf("Auto-learn is running: one round every %s, uptime %s, total rounds %d.", - learner.interval.Truncate(time.Second), - uptime, - learner.rounds, - ) - return al.naturalizeUserFacingText(ctx, fallback) + return al.renderControlReply(ctx, "autolearn_status", map[string]interface{}{ + "interval": learner.interval.Truncate(time.Second).String(), + "uptime": time.Since(learner.started).Truncate(time.Second).String(), + "total_rounds": learner.rounds, + "running_state": "running", + }, "自动学习运行中。") } func buildAutoLearnPrompt(round int) string { @@ -1735,30 +1743,6 @@ func shouldRetryAfterDeferralNoTools(content string, userTask string, iteration return false } -func formatRunStateReport(rs runState) string { - lines := []string{ - fmt.Sprintf("Run ID: %s", rs.runID), - fmt.Sprintf("Status: %s", rs.status), - fmt.Sprintf("Session: %s", rs.sessionKey), - fmt.Sprintf("Accepted At: %s", rs.acceptedAt.Format(time.RFC3339)), - } - if !rs.startedAt.IsZero() { - lines = append(lines, fmt.Sprintf("Started At: %s", rs.startedAt.Format(time.RFC3339))) - } - if !rs.endedAt.IsZero() { - lines = append(lines, fmt.Sprintf("Ended At: %s", rs.endedAt.Format(time.RFC3339))) - lines = append(lines, fmt.Sprintf("Duration: %s", rs.endedAt.Sub(rs.startedAt).Truncate(time.Millisecond))) - } else if !rs.startedAt.IsZero() { - lines = append(lines, fmt.Sprintf("Elapsed: %s", time.Since(rs.startedAt).Truncate(time.Second))) - } - lines = append(lines, fmt.Sprintf("Control Handled: %v", rs.controlHandled)) - lines = append(lines, fmt.Sprintf("Response Length: %d", rs.responseLen)) - if strings.TrimSpace(rs.errMessage) != "" { - lines = append(lines, fmt.Sprintf("Error: %s", rs.errMessage)) - } - return strings.Join(lines, "\n") -} - func (al *AgentLoop) executeRunControlIntent(ctx context.Context, sessionKey string, intent runControlIntent) string { var ( rs runState @@ -1770,7 +1754,9 @@ func (al *AgentLoop) executeRunControlIntent(ctx context.Context, sessionKey str rs, found = al.getRunState(intent.runID) } if !found { - return al.naturalizeUserFacingText(ctx, "No matching run state found. Try specifying a run ID or asking for the latest run status.") + return al.renderControlReply(ctx, "run_state_not_found", map[string]interface{}{ + "session_key": sessionKey, + }, "未找到匹配的运行记录。") } if intent.wait && (rs.status == runStatusAccepted || rs.status == runStatusRunning) { @@ -1782,11 +1768,15 @@ func (al *AgentLoop) executeRunControlIntent(ctx context.Context, sessionKey str if latest, ok := al.getRunState(rs.runID); ok { rs = latest } - fallback := fmt.Sprintf("Run %s is still %s after waiting %s.\n%s", rs.runID, rs.status, intent.timeout.Truncate(time.Second), formatRunStateReport(rs)) - return al.naturalizeUserFacingText(ctx, fallback) + return al.renderControlReply(ctx, "run_state_wait_timeout", map[string]interface{}{ + "wait_timeout": intent.timeout.Truncate(time.Second).String(), + "run_state": runStateToReplyPayload(rs), + }, "等待超时,任务仍在运行。") } } - return al.naturalizeUserFacingText(ctx, formatRunStateReport(rs)) + return al.renderControlReply(ctx, "run_state_report", map[string]interface{}{ + "run_state": runStateToReplyPayload(rs), + }, "运行状态已更新。") } func (al *AgentLoop) inferRunControlIntent(ctx context.Context, content string) (runControlIntent, float64, bool) { @@ -2037,13 +2027,17 @@ func (al *AgentLoop) handlePendingControlConfirmation(ctx context.Context, msg b } al.setPendingControlConfirmation(msg.SessionKey, pending) al.controlMetricAdd(&al.controlStats.confirmPrompts, 1) - return true, al.naturalizeUserFacingText(ctx, "I am checking a control action confirmation. Please reply with yes or no.") + return true, al.renderControlReply(ctx, "control_confirmation_needed", map[string]interface{}{ + "expected_reply": "yes_or_no", + }, "请回复“是”或“否”以确认操作。") } al.clearPendingControlConfirmation(msg.SessionKey) if !decision { al.controlMetricAdd(&al.controlStats.confirmRejected, 1) - return true, al.naturalizeUserFacingText(ctx, "Understood. I will not change autonomous control mode now.") + return true, al.renderControlReply(ctx, "control_confirmation_rejected", map[string]interface{}{ + "action": pending.action, + }, "已取消本次控制操作。") } al.controlMetricAdd(&al.controlStats.confirmAccepted, 1) @@ -2226,45 +2220,6 @@ func (al *AgentLoop) getPendingControlConfirmation(sessionKey string) (pendingCo return pending, ok } -func (al *AgentLoop) formatAutonomyConfirmationPrompt(intent autonomyIntent) string { - switch intent.action { - case "start": - idleText := autonomyDefaultIdleInterval.Truncate(time.Second).String() - if intent.idleInterval != nil { - idleText = intent.idleInterval.Truncate(time.Second).String() - } - if strings.TrimSpace(intent.focus) != "" { - return fmt.Sprintf("I inferred that you want to enable autonomy mode (idle interval %s, focus: %s). Reply \"yes\" to confirm or \"no\" to cancel.", idleText, strings.TrimSpace(intent.focus)) - } - return fmt.Sprintf("I inferred that you want to enable autonomy mode (idle interval %s). Reply \"yes\" to confirm or \"no\" to cancel.", idleText) - case "stop": - return "I inferred that you want to stop autonomy mode. Reply \"yes\" to confirm or \"no\" to cancel." - case "status": - return "I inferred that you want autonomy status. Reply \"yes\" to confirm or \"no\" to cancel." - case "clear_focus": - return "I inferred that you want to clear the current autonomy focus. Reply \"yes\" to confirm or \"no\" to cancel." - default: - return "I inferred an autonomy control operation. Reply \"yes\" to confirm or \"no\" to cancel." - } -} - -func (al *AgentLoop) formatAutoLearnConfirmationPrompt(intent autoLearnIntent) string { - switch intent.action { - case "start": - intervalText := autoLearnDefaultInterval.Truncate(time.Second).String() - if intent.interval != nil { - intervalText = intent.interval.Truncate(time.Second).String() - } - return fmt.Sprintf("I inferred that you want to start auto-learn (interval %s). Reply \"yes\" to confirm or \"no\" to cancel.", intervalText) - case "stop": - return "I inferred that you want to stop auto-learn. Reply \"yes\" to confirm or \"no\" to cancel." - case "status": - return "I inferred that you want auto-learn status. Reply \"yes\" to confirm or \"no\" to cancel." - default: - return "I inferred an auto-learn control operation. Reply \"yes\" to confirm or \"no\" to cancel." - } -} - func withTokenUsageTotals(ctx context.Context) (context.Context, *tokenUsageTotals) { if ctx == nil { ctx = context.Background() @@ -2390,6 +2345,83 @@ Rules: return out } +func (al *AgentLoop) renderControlReply(ctx context.Context, event string, fields map[string]interface{}, fallback string) string { + base := strings.TrimSpace(fallback) + if base == "" { + base = "操作已完成。" + } + if ctx == nil || al == nil || (al.provider == nil && len(al.providersByProxy) == 0) { + return base + } + + targetLanguage := "the same language as the latest user message" + if hint, ok := ctx.Value(userLanguageHintKey{}).(userLanguageHint); ok { + if al.preferChineseUserFacingText(hint.sessionKey, hint.content) { + targetLanguage = "Simplified Chinese" + } + } + + payload := map[string]interface{}{ + "event": strings.TrimSpace(event), + "fields": fields, + } + raw, err := json.Marshal(payload) + if err != nil { + return al.naturalizeUserFacingText(ctx, base) + } + + llmCtx, cancel := context.WithTimeout(ctx, 4*time.Second) + defer cancel() + + systemPrompt := al.withBootstrapPolicy(fmt.Sprintf(`You generate user-facing control/status replies from structured event data. +Output language: %s. +Rules: +- Use concise natural wording. +- Preserve factual values from fields. +- No markdown, no code block. +- Return plain text only.`, targetLanguage)) + + resp, err := al.callLLMWithModelFallback(llmCtx, []providers.Message{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: string(raw)}, + }, nil, map[string]interface{}{ + "max_tokens": 200, + "temperature": 0.4, + }) + if err != nil || resp == nil { + return al.naturalizeUserFacingText(ctx, base) + } + out := strings.TrimSpace(resp.Content) + if out == "" || shouldRejectNaturalizedOutput(out, base) { + return al.naturalizeUserFacingText(ctx, base) + } + return out +} + +func runStateToReplyPayload(rs runState) map[string]interface{} { + payload := map[string]interface{}{ + "run_id": rs.runID, + "status": rs.status, + "session": rs.sessionKey, + "accepted_at": rs.acceptedAt.Format(time.RFC3339), + "control_handled": rs.controlHandled, + "response_length": rs.responseLen, + } + if !rs.startedAt.IsZero() { + payload["started_at"] = rs.startedAt.Format(time.RFC3339) + } + if !rs.endedAt.IsZero() { + payload["ended_at"] = rs.endedAt.Format(time.RFC3339) + payload["duration"] = rs.endedAt.Sub(rs.startedAt).Truncate(time.Millisecond).String() + } else if !rs.startedAt.IsZero() { + payload["elapsed"] = time.Since(rs.startedAt).Truncate(time.Second).String() + } + if strings.TrimSpace(rs.errMessage) != "" { + payload["error"] = rs.errMessage + } + return payload +} + func shouldRejectNaturalizedOutput(out string, fallback string) bool { candidate := strings.ToLower(strings.TrimSpace(out)) origin := strings.TrimSpace(fallback) @@ -2588,7 +2620,9 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) if progress != nil { progress.Publish(4, 5, "finalization", "Final response is ready.") - progress.Publish(5, 5, "done", fmt.Sprintf("Completed after %d iterations.", iteration)) + progress.Publish(5, 5, "done", al.renderControlReply(withUserLanguageHint(ctx, msg.SessionKey, msg.Content), "task_completed", map[string]interface{}{ + "iterations": iteration, + }, "任务已完成。")) } return userContent, nil @@ -2695,7 +2729,9 @@ func (al *AgentLoop) runLLMToolLoop( state.iteration++ iteration := state.iteration if progress != nil { - progress.Publish(3, 5, "execution", fmt.Sprintf("Running iteration %d.", iteration)) + progress.Publish(3, 5, "execution", al.renderControlReply(ctx, "iteration_running", map[string]interface{}{ + "iteration": iteration, + }, "正在执行下一轮。")) } providerToolDefs, err := buildProviderToolDefs(al.tools.GetDefinitions()) @@ -2842,7 +2878,7 @@ func (al *AgentLoop) runLLMToolLoop( case "done": finalResp, ferr := al.finalizeToolLoop(ctx, append(messages, providers.Message{ Role: "user", - Content: fmt.Sprintf("Reflection indicates completion (confidence %.2f). Provide the final user-facing answer now without tools. Reason: %s", confidence, reason), + Content: "Reflection indicates completion. Provide the final user-facing answer now without tools.", })) if ferr == nil && finalResp != nil && strings.TrimSpace(finalResp.Content) != "" { state.finalContent = finalResp.Content @@ -2850,13 +2886,13 @@ func (al *AgentLoop) runLLMToolLoop( } messages = append(messages, providers.Message{ Role: "user", - Content: fmt.Sprintf("Reflection indicates completion. Provide final answer now without tools. Reason: %s", reason), + Content: "Reflection indicates completion. Provide final answer now without tools.", }) break case "blocked": finalResp, ferr := al.finalizeToolLoop(ctx, append(messages, providers.Message{ Role: "user", - Content: fmt.Sprintf("Reflection indicates blocked progress (confidence %.2f). Stop calling tools and provide diagnosis, blockers, and minimum user input needed. Reason: %s", confidence, reason), + Content: "Reflection indicates blocked progress. Stop calling tools and provide diagnosis, blockers, and minimum user input needed.", })) if ferr == nil && finalResp != nil && strings.TrimSpace(finalResp.Content) != "" { state.finalContent = finalResp.Content @@ -2864,13 +2900,13 @@ func (al *AgentLoop) runLLMToolLoop( } messages = append(messages, providers.Message{ Role: "user", - Content: fmt.Sprintf("Blocked progress detected. Stop calling tools and provide diagnosis plus minimum needed user action. Reason: %s", reason), + Content: "Blocked progress detected. Stop calling tools and provide diagnosis plus minimum needed user action.", }) break default: messages = append(messages, providers.Message{ Role: "user", - Content: fmt.Sprintf("Continue execution with minimal next-step tools only. Avoid repetition. Reflection reason: %s", reason), + Content: "Continue execution with minimal next-step tools only. Avoid repetition.", }) } if state.finalContent != "" || decision == "done" || decision == "blocked" { @@ -3131,9 +3167,14 @@ func (al *AgentLoop) actToolCalls( } if progress != nil { if err != nil { - progress.Publish(3, 5, "execution", fmt.Sprintf("Tool %s failed: %v", tc.Name, err)) + progress.Publish(3, 5, "execution", al.renderControlReply(ctx, "tool_failed", map[string]interface{}{ + "tool": tc.Name, + "error": strings.TrimSpace(err.Error()), + }, "工具执行失败。")) } else { - progress.Publish(3, 5, "execution", fmt.Sprintf("Tool %s completed.", tc.Name)) + progress.Publish(3, 5, "execution", al.renderControlReply(ctx, "tool_completed", map[string]interface{}{ + "tool": tc.Name, + }, "工具执行完成。")) } } outcome.lastToolResult = result @@ -3372,9 +3413,14 @@ func (al *AgentLoop) executeSingleToolCall( } if progress != nil { if err != nil { - progress.Publish(3, 5, "execution", fmt.Sprintf("Tool %s failed: %v", tc.Name, err)) + progress.Publish(3, 5, "execution", al.renderControlReply(ctx, "tool_failed", map[string]interface{}{ + "tool": tc.Name, + "error": strings.TrimSpace(err.Error()), + }, "工具执行失败。")) } else { - progress.Publish(3, 5, "execution", fmt.Sprintf("Tool %s completed.", tc.Name)) + progress.Publish(3, 5, "execution", al.renderControlReply(ctx, "tool_completed", map[string]interface{}{ + "tool": tc.Name, + }, "工具执行完成。")) } } return toolCallExecResult{ @@ -3810,7 +3856,10 @@ func (al *AgentLoop) runSelfRepairIfNeeded( mem.promptsUsed[normalizedPrompt] = struct{}{} repairPasses++ if progress != nil { - progress.Publish(4, 5, "self-repair", fmt.Sprintf("Running self-repair pass %d (confidence %.2f).", repairPasses, confidence)) + progress.Publish(4, 5, "self-repair", al.renderControlReply(ctx, "self_repair_started", map[string]interface{}{ + "pass": repairPasses, + "confidence": confidence, + }, "正在执行自动修复。")) } repairMessages := append([]providers.Message{}, baseMessages...) repairMessages = append(repairMessages, providers.Message{ @@ -5580,24 +5629,26 @@ func (al *AgentLoop) handleSlashCommand(ctx context.Context, msg bus.InboundMess activeBase = p.APIBase } } - statusText := fmt.Sprintf("Model: %s\nProxy: %s\nAPI Base: %s\nResponses Compact: %v\nLogging: %v\nConfig: %s", - al.model, - activeProxy, - activeBase, - providers.ProviderSupportsResponsesCompact(cfg, activeProxy), - cfg.Logging.Enabled, - al.getConfigPathForCommands(), - ) - return true, statusText, nil + return true, al.renderControlReply(ctx, "runtime_status", map[string]interface{}{ + "model": al.model, + "proxy": activeProxy, + "api_base": activeBase, + "responses_compact": providers.ProviderSupportsResponsesCompact(cfg, activeProxy), + "logging_enabled": cfg.Logging.Enabled, + "config_path": al.getConfigPathForCommands(), + "runtime_visibility": "agent", + }, "当前运行状态已更新。"), nil case "/reload": running, err := al.triggerGatewayReloadFromAgent() if err != nil { if running { return true, "", err } - return true, fmt.Sprintf("Hot reload not applied: %v", err), nil + return true, al.renderControlReply(ctx, "hot_reload_skipped", map[string]interface{}{ + "error": strings.TrimSpace(err.Error()), + }, "热重载未生效。"), nil } - return true, "Gateway hot reload signal sent", nil + return true, al.renderControlReply(ctx, "hot_reload_triggered", map[string]interface{}{}, "已发送网关热重载信号。"), nil case "/config": if len(fields) < 2 { return true, "Usage: /config get | /config set ", nil @@ -5615,11 +5666,16 @@ func (al *AgentLoop) handleSlashCommand(ctx context.Context, msg bus.InboundMess path := al.normalizeConfigPathForAgent(fields[2]) value, ok := al.getMapValueByPathForAgent(cfgMap, path) if !ok { - return true, fmt.Sprintf("Path not found: %s", path), nil + return true, al.renderControlReply(ctx, "config_path_not_found", map[string]interface{}{ + "path": path, + }, "未找到对应配置路径。"), nil } data, err := json.Marshal(value) if err != nil { - return true, fmt.Sprintf("%v", value), nil + return true, al.renderControlReply(ctx, "config_value_read", map[string]interface{}{ + "path": path, + "value": fmt.Sprint(value), + }, "已读取配置值。"), nil } return true, string(data), nil case "set": @@ -5657,9 +5713,16 @@ func (al *AgentLoop) handleSlashCommand(ctx context.Context, msg bus.InboundMess } return true, "", fmt.Errorf("hot reload failed, config rolled back: %w", err) } - return true, fmt.Sprintf("Updated %s = %v\nHot reload not applied: %v", path, value, err), nil + return true, al.renderControlReply(ctx, "config_updated_reload_skipped", map[string]interface{}{ + "path": path, + "value": fmt.Sprint(value), + "error": strings.TrimSpace(err.Error()), + }, "配置已更新,但热重载未生效。"), nil } - return true, fmt.Sprintf("Updated %s = %v\nGateway hot reload signal sent", path, value), nil + return true, al.renderControlReply(ctx, "config_updated_reload_triggered", map[string]interface{}{ + "path": path, + "value": fmt.Sprint(value), + }, "配置已更新并触发热重载。"), nil default: return true, "Usage: /config get | /config set ", nil } @@ -5676,12 +5739,18 @@ func (al *AgentLoop) handleSlashCommand(ctx context.Context, msg bus.InboundMess if len(items) == 0 { return true, "No pipelines found.", nil } - var sb strings.Builder - sb.WriteString("Pipelines:\n") + entries := make([]map[string]interface{}, 0, len(items)) for _, p := range items { - sb.WriteString(fmt.Sprintf("- %s [%s] %s\n", p.ID, p.Status, p.Label)) + entries = append(entries, map[string]interface{}{ + "id": p.ID, + "status": p.Status, + "label": p.Label, + }) } - return true, sb.String(), nil + return true, al.renderControlReply(ctx, "pipeline_list", map[string]interface{}{ + "pipelines": entries, + "count": len(entries), + }, "已获取流水线列表。"), nil case "status": if len(fields) < 3 { return true, "Usage: /pipeline status ", nil @@ -5699,12 +5768,18 @@ func (al *AgentLoop) handleSlashCommand(ctx context.Context, msg bus.InboundMess if len(ready) == 0 { return true, "No ready tasks.", nil } - var sb strings.Builder - sb.WriteString("Ready tasks:\n") + tasks := make([]map[string]interface{}, 0, len(ready)) for _, task := range ready { - sb.WriteString(fmt.Sprintf("- %s (%s) %s\n", task.ID, task.Role, task.Goal)) + tasks = append(tasks, map[string]interface{}{ + "id": task.ID, + "role": task.Role, + "goal": task.Goal, + }) } - return true, sb.String(), nil + return true, al.renderControlReply(ctx, "pipeline_ready_tasks", map[string]interface{}{ + "tasks": tasks, + "count": len(tasks), + }, "已获取可执行任务列表。"), nil default: return true, "Usage: /pipeline list | /pipeline status | /pipeline ready ", nil }