Files
smartmate/backend/service/agentsvc/agent_newagent.go
Losita cdedd3c968 Version: 0.9.5.dev.260407
后端:
1.粗排链路收口(按 task_class_ids 精确加载 ScheduleState + 规划窗口抗脏数据)
  - 更新conv/schedule_provider.go:新增 LoadScheduleStateForTaskClasses;优先按本轮任务类加载窗口;buildWindowFromTaskClasses 改为逐条过滤坏日期,避免 DayMapping 被全量任务类污染
  - 更新model/state_store.go:新增 ScopedScheduleStateProvider 可选接口
  - 更新model/graph_run_state.go:EnsureScheduleState 首次加载时优先走 scoped provider,再做 scope 裁剪
2.粗排建议态语义统一(pending/existing → pending/suggested/existing)
  - 新建tools/status.go:统一 IsPendingTask / IsSuggestedTask / IsExistingTask / scope 过滤逻辑
  - 更新node/rough_build.go:粗排回写后任务显式转 suggested;pending 统计仅看“真实 pending”
  - 更新tools/state.go:ScheduleTask.Status/Slots/Duration 注释补齐 suggested 语义
  - 更新tools/read_helpers.go + read_tools.go:overview/list_tasks/task_info 支持 suggested 展示;占用计算按“已落位任务”统一处理
  - 更新tools/write_helpers.go + write_tools.go:place/move/swap/unplace 全量切到 suggested/existing/pending 新语义
  - 更新tools/registry.go + SCHEDULE_TOOLS.md:工具描述、参数枚举、文档口径同步到 suggested 语义
  - 更新conv/schedule_preview.go:预览层统一通过 IsSuggestedTask 输出 suggested,兼容旧快照
  - 更新service/agentsvc/agent_newagent.go:预览 debug 摘要改为 pending/suggested/existing 三态统计
3.粗排调试增强
  - 更新node/rough_build.go:新增 applied/day_mapping_miss/task_item_match_miss 统计及样本日志,便于排查 placement 未落回 state 的根因
前端:无 仓库:无
2026-04-07 23:58:00 +08:00

585 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agentsvc
import (
"context"
"fmt"
"log"
"strings"
"time"
newagentgraph "github.com/LoveLosita/smartflow/backend/newAgent/graph"
newagentllm "github.com/LoveLosita/smartflow/backend/newAgent/llm"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
"github.com/cloudwego/eino/schema"
agentchat "github.com/LoveLosita/smartflow/backend/agent/chat"
"github.com/LoveLosita/smartflow/backend/conv"
"github.com/LoveLosita/smartflow/backend/model"
"github.com/LoveLosita/smartflow/backend/pkg"
"github.com/LoveLosita/smartflow/backend/respond"
eventsvc "github.com/LoveLosita/smartflow/backend/service/events"
)
// runNewAgentGraph 运行 newAgent 通用 graph直接替换旧 agent 路由逻辑。
//
// 职责边界:
// 1. 负责构造 AgentGraphRunInputRuntimeState、ConversationContext、Request、Deps
// 2. 负责将 outChan 适配为 ChunkEmitter
// 3. 负责调用 graph.RunAgentGraph
// 4. 负责持久化聊天历史(复用现有逻辑)。
//
// 设计原则:
// 1. 直接走 newAgent graph不再经过旧的 agentrouter 路由决策;
// 2. 所有任务类型chat、task、quick_note都由 graph 内部 LLM 决策;
// 3. 状态恢复、工具执行、确认流程全部由 graph 节点处理。
func (s *AgentService) runNewAgentGraph(
ctx context.Context,
userMessage string,
ifThinking bool,
modelName string,
userID int,
chatID string,
extra map[string]any,
traceID string,
requestStart time.Time,
outChan chan<- string,
errChan chan error,
) {
requestCtx, _ := withRequestTokenMeter(ctx)
// 1. 规范会话 ID 和模型选择。
chatID = normalizeConversationID(chatID)
_, resolvedModelName := s.pickChatModel(modelName)
// 2. 确保会话存在(优先缓存,必要时回源 DB
result, err := s.agentCache.GetConversationStatus(requestCtx, chatID)
if err != nil {
pushErrNonBlocking(errChan, err)
return
}
if !result {
innerResult, ifErr := s.repo.IfChatExists(requestCtx, userID, chatID)
if ifErr != nil {
pushErrNonBlocking(errChan, ifErr)
return
}
if !innerResult {
if _, err = s.repo.CreateNewChat(userID, chatID); err != nil {
pushErrNonBlocking(errChan, err)
return
}
}
if err = s.agentCache.SetConversationStatus(requestCtx, chatID); err != nil {
log.Printf("设置会话状态缓存失败 chat=%s: %v", chatID, err)
}
}
// 3. 构建重试元数据。
retryMeta, err := s.buildChatRetryMeta(requestCtx, userID, chatID, extra)
if err != nil {
pushErrNonBlocking(errChan, err)
return
}
// 4. 从 StateStore 加载或创建 RuntimeState。
// 恢复场景confirm/ask_user同时拿到快照中保存的 ConversationContext
// 其中包含工具调用/结果等中间消息,保证后续 LLM 调用的消息链完整。
runtimeState, savedConversationContext, savedScheduleState, savedOriginalScheduleState := s.loadOrCreateRuntimeState(requestCtx, chatID, userID)
// 5. 构造 ConversationContext。
// 优先使用快照中恢复的 ConversationContext含工具调用/结果),
// 无快照时从 Redis LLM 历史缓存加载。
var conversationContext *newagentmodel.ConversationContext
if savedConversationContext != nil {
conversationContext = savedConversationContext
// 把用户本轮输入追加到恢复的上下文中(与 loadConversationContext 行为一致)。
if strings.TrimSpace(userMessage) != "" {
conversationContext.AppendHistory(schema.UserMessage(userMessage))
}
} else {
conversationContext = s.loadConversationContext(requestCtx, chatID, userMessage)
}
// 5.5 若 extra 携带 task_class_ids校验后写入 CommonState仅首轮/尚未设置时生效,跨轮持久化)。
// 校验:通过 LoadTaskClassMetas → GetCompleteTaskClassesByIDs 检查所有 ID 是否存在且属于当前用户;
// 校验失败时向 errChan 推送 WrongTaskClassIDcode=40040前端收到 SSE 错误事件。
if taskClassIDs := readAgentExtraIntSlice(extra, "task_class_ids"); len(taskClassIDs) > 0 {
cs := runtimeState.EnsureCommonState()
if len(cs.TaskClassIDs) == 0 {
if s.scheduleProvider == nil {
pushErrNonBlocking(errChan, respond.WrongTaskClassID)
return
}
metas, metaErr := s.scheduleProvider.LoadTaskClassMetas(requestCtx, userID, taskClassIDs)
if metaErr != nil {
pushErrNonBlocking(errChan, respond.WrongTaskClassID)
return
}
cs.TaskClassIDs = taskClassIDs
cs.TaskClasses = metas
}
}
// 6. 构造 AgentGraphRequest。
var confirmAction string
if len(extra) > 0 {
confirmAction = readAgentExtraString(extra, "confirm_action")
}
graphRequest := newagentmodel.AgentGraphRequest{
UserInput: userMessage,
ConfirmAction: confirmAction,
AlwaysExecute: readAgentExtraBool(extra, "always_execute"),
}
graphRequest.Normalize()
// 7. 适配 LLM clients从 AIHub 的 ark.ChatModel 转换为 newAgent LLM Client
chatClient := newagentllm.WrapArkClient(s.AIHub.Worker)
planClient := newagentllm.WrapArkClient(s.AIHub.Worker)
executeClient := newagentllm.WrapArkClient(s.AIHub.Worker)
deliverClient := newagentllm.WrapArkClient(s.AIHub.Worker)
// 8. 适配 SSE emitter。
sseEmitter := newagentstream.NewSSEPayloadEmitter(outChan)
chunkEmitter := newagentstream.NewChunkEmitter(sseEmitter, traceID, resolvedModelName, requestStart.Unix())
// 9. 构造 AgentGraphDeps由 cmd/start.go 注入的依赖)。
deps := newagentmodel.AgentGraphDeps{
ChatClient: chatClient,
PlanClient: planClient,
ExecuteClient: executeClient,
DeliverClient: deliverClient,
ChunkEmitter: chunkEmitter,
StateStore: s.agentStateStore,
ToolRegistry: s.toolRegistry,
ScheduleProvider: s.scheduleProvider,
SchedulePersistor: s.schedulePersistor,
RoughBuildFunc: s.makeRoughBuildFunc(),
WriteSchedulePreview: s.makeWriteSchedulePreviewFunc(),
}
// 10. 构造 AgentGraphRunInput 并运行 graph。
runInput := newagentmodel.AgentGraphRunInput{
RuntimeState: runtimeState,
ConversationContext: conversationContext,
ScheduleState: savedScheduleState,
OriginalScheduleState: savedOriginalScheduleState,
Request: graphRequest,
Deps: deps,
}
finalState, graphErr := newagentgraph.RunAgentGraph(requestCtx, runInput)
if graphErr != nil {
log.Printf("[ERROR] newAgent graph 执行失败 trace=%s chat=%s: %v", traceID, chatID, graphErr)
pushErrNonBlocking(errChan, fmt.Errorf("graph 执行失败: %w", graphErr))
// Graph 出错时回退普通聊天,保证可用性。
s.runNormalChatFlow(requestCtx, s.AIHub.Worker, resolvedModelName, userMessage, "", nil, retryMeta, ifThinking, userID, chatID, traceID, requestStart, outChan, errChan)
return
}
// 11. 持久化聊天历史(用户消息 + 助手回复)。
s.persistChatAfterGraph(requestCtx, userID, chatID, userMessage, finalState, retryMeta, requestStart, outChan, errChan)
// 11.5. 将最终状态快照异步写入 MySQL通过 outbox
// Deliver 节点已将快照保存到 Redis2h TTL此处通过 outbox 异步写入 MySQL 做永久存储。
if finalState != nil {
snapshot := &newagentmodel.AgentStateSnapshot{
RuntimeState: finalState.EnsureRuntimeState(),
ConversationContext: finalState.EnsureConversationContext(),
}
eventsvc.PublishAgentStateSnapshot(requestCtx, s.eventPublisher, snapshot, chatID, userID)
}
// 排程预览缓存由 Deliver 节点负责写入(通过注入的 WriteSchedulePreview func
// 保证只有任务真正完成时才写,中断路径不写中间态。
// 12. 发送 OpenAI 兼容的流式结束标记,告知客户端 stream 已完成。
_ = chunkEmitter.EmitDone()
// 13. 异步生成会话标题。
s.ensureConversationTitleAsync(userID, chatID)
}
// loadOrCreateRuntimeState 从 StateStore 加载或创建新的 RuntimeState。
//
// 返回值:
// - RuntimeState可持久化流程状态
// - ConversationContext快照中保存的完整对话上下文含工具调用/结果),
// 仅在恢复已有快照时非 nil新建会话时为 nil。
//
// 设计说明:
// 1. 快照中的 ConversationContext 包含 graph 执行期间的完整中间消息(工具调用、工具结果等),
// 这些消息不会出现在 Redis LLM 历史缓存中;
// 2. 恢复场景confirm/ask_user必须使用快照中的 ConversationContext否则工具结果丢失
// 导致后续 LLM 调用收到非法的裸 Tool 消息API 拒绝请求、连接断开。
func (s *AgentService) loadOrCreateRuntimeState(ctx context.Context, chatID string, userID int) (*newagentmodel.AgentRuntimeState, *newagentmodel.ConversationContext, *newagenttools.ScheduleState, *newagenttools.ScheduleState) {
newRT := func() (*newagentmodel.AgentRuntimeState, *newagentmodel.ConversationContext, *newagenttools.ScheduleState, *newagenttools.ScheduleState) {
rt := newagentmodel.NewAgentRuntimeState(nil)
cs := rt.EnsureCommonState()
cs.UserID = userID
cs.ConversationID = chatID // saveAgentState 依赖此字段决定是否持久化
return rt, nil, nil, nil
}
if s.agentStateStore == nil {
return newRT()
}
snapshot, ok, err := s.agentStateStore.Load(ctx, chatID)
log.Printf("[DEBUG] loadOrCreateRuntimeState chatID=%s ok=%v err=%v hasRuntime=%v hasPending=%v hasCtx=%v hasSchedule=%v hasOriginal=%v",
chatID, ok, err,
snapshot != nil && snapshot.RuntimeState != nil,
snapshot != nil && snapshot.RuntimeState != nil && snapshot.RuntimeState.HasPendingInteraction(),
snapshot != nil && snapshot.ConversationContext != nil,
snapshot != nil && snapshot.ScheduleState != nil,
snapshot != nil && snapshot.OriginalScheduleState != nil,
)
if err != nil {
log.Printf("加载 agent 状态失败 chat=%s: %v", chatID, err)
return newRT()
}
if ok && snapshot != nil && snapshot.RuntimeState != nil {
// 恢复运行态,确保身份信息与当前请求一致。
cs := snapshot.RuntimeState.EnsureCommonState()
cs.UserID = userID
cs.ConversationID = chatID
// 不需要手动重置 Phase所有请求统一先过 Chat 节点Chat 会根据路由决策覆盖 Phase。
// 保留完整的 RuntimeStatePlanSteps、CurrentStep 等),支持连续对话调整日程。
originalScheduleState := snapshot.OriginalScheduleState
if snapshot.ScheduleState != nil && originalScheduleState == nil {
// 1. 兼容老快照:历史会话可能只存了 ScheduleState没有 original 副本。
// 2. 这里补一份克隆,保证后续节点拿到的仍是“恢复态 + 原始态”成对数据。
// 3. 即便当前阶段不落库,这里也保留一致性,避免下一轮再出现语义漂移。
originalScheduleState = snapshot.ScheduleState.Clone()
}
return snapshot.RuntimeState, snapshot.ConversationContext, snapshot.ScheduleState, originalScheduleState
}
return newRT()
}
// loadConversationContext 加载对话历史,构造 ConversationContext。
func (s *AgentService) loadConversationContext(ctx context.Context, chatID, userMessage string) *newagentmodel.ConversationContext {
// 从 Redis 加载历史。
history, err := s.agentCache.GetHistory(ctx, chatID)
if err != nil {
log.Printf("加载历史失败 chat=%s: %v", chatID, err)
history = nil
}
// 缓存未命中时回源 DB。
if history == nil {
histories, hisErr := s.repo.GetUserChatHistories(ctx, 0, pkg.HistoryFetchLimitByModel("worker"), chatID)
if hisErr != nil {
log.Printf("从 DB 加载历史失败 chat=%s: %v", chatID, hisErr)
} else {
history = conv.ToEinoMessages(histories)
// 回填到 Redis。
if backfillErr := s.agentCache.BackfillHistory(ctx, chatID, history); backfillErr != nil {
log.Printf("回填历史到 Redis 失败 chat=%s: %v", chatID, backfillErr)
}
}
}
// 构造 ConversationContext。
conversationContext := newagentmodel.NewConversationContext(agentchat.SystemPrompt)
if history != nil {
conversationContext.ReplaceHistory(history)
}
// 把用户本轮输入追加到历史(供 graph 使用)。
if strings.TrimSpace(userMessage) != "" {
conversationContext.AppendHistory(schema.UserMessage(userMessage))
}
return conversationContext
}
// persistChatAfterGraph graph 执行完成后持久化聊天历史。
func (s *AgentService) persistChatAfterGraph(
ctx context.Context,
userID int,
chatID string,
userMessage string,
finalState *newagentmodel.AgentGraphState,
retryMeta *chatRetryMeta,
requestStart time.Time,
outChan chan<- string,
errChan chan error,
) {
if finalState == nil {
return
}
// 1. 持久化用户消息:先写 LLM 上下文 Redis再落 DB最后更新 UI 历史缓存。
userMsg := &schema.Message{Role: schema.User, Content: userMessage}
if retryExtra := retryMeta.CacheExtra(); len(retryExtra) > 0 {
userMsg.Extra = retryExtra
}
if err := s.agentCache.PushMessage(ctx, chatID, userMsg); err != nil {
log.Printf("写入用户消息到 LLM 上下文 Redis 失败 chat=%s: %v", chatID, err)
}
userPayload := model.ChatHistoryPersistPayload{
UserID: userID,
ConversationID: chatID,
Role: "user",
Message: userMessage,
ReasoningContent: "",
ReasoningDurationSeconds: 0,
RetryGroupID: retryMeta.GroupIDPtr(),
RetryIndex: retryMeta.IndexPtr(),
RetryFromUserMessageID: retryMeta.FromUserMessageIDPtr(),
RetryFromAssistantMessageID: retryMeta.FromAssistantMessageIDPtr(),
TokensConsumed: 0,
}
if err := s.PersistChatHistory(ctx, userPayload); err != nil {
pushErrNonBlocking(errChan, err)
}
userCreatedAt := time.Now()
s.appendConversationHistoryCacheOptimistically(
context.Background(),
userID,
chatID,
buildOptimisticConversationHistoryItem("user", userMessage, "", 0, retryMeta, userCreatedAt),
)
// 2. 从 ConversationContext 提取助手回复(最后一条 assistant 消息)。
conversationContext := finalState.ConversationContext
if conversationContext == nil || len(conversationContext.History) == 0 {
return
}
var lastAssistantMsg *schema.Message
for i := len(conversationContext.History) - 1; i >= 0; i-- {
msg := conversationContext.History[i]
if msg.Role == schema.Assistant {
lastAssistantMsg = msg
break
}
}
if lastAssistantMsg == nil {
return
}
assistantReply := lastAssistantMsg.Content
reasoningContent := lastAssistantMsg.ReasoningContent
var reasoningDurationSeconds int
if lastAssistantMsg.Extra != nil {
if dur, ok := lastAssistantMsg.Extra["reasoning_duration_seconds"].(float64); ok {
reasoningDurationSeconds = int(dur)
}
}
// 3. 持久化助手消息:先写 LLM 上下文 Redis再落 DB最后更新 UI 历史缓存。
assistantMsg := &schema.Message{
Role: schema.Assistant,
Content: assistantReply,
ReasoningContent: reasoningContent,
}
if reasoningDurationSeconds > 0 {
assistantMsg.Extra = map[string]any{"reasoning_duration_seconds": reasoningDurationSeconds}
}
if retryExtra := retryMeta.CacheExtra(); len(retryExtra) > 0 {
if assistantMsg.Extra == nil {
assistantMsg.Extra = make(map[string]any)
}
for k, v := range retryExtra {
assistantMsg.Extra[k] = v
}
}
if err := s.agentCache.PushMessage(context.Background(), chatID, assistantMsg); err != nil {
log.Printf("写入助手消息到 LLM 上下文 Redis 失败 chat=%s: %v", chatID, err)
}
requestTotalTokens := snapshotRequestTokenMeter(ctx).TotalTokens
assistantPayload := model.ChatHistoryPersistPayload{
UserID: userID,
ConversationID: chatID,
Role: "assistant",
Message: assistantReply,
ReasoningContent: reasoningContent,
ReasoningDurationSeconds: reasoningDurationSeconds,
RetryGroupID: retryMeta.GroupIDPtr(),
RetryIndex: retryMeta.IndexPtr(),
RetryFromUserMessageID: retryMeta.FromUserMessageIDPtr(),
RetryFromAssistantMessageID: retryMeta.FromAssistantMessageIDPtr(),
TokensConsumed: requestTotalTokens,
}
if err := s.PersistChatHistory(ctx, assistantPayload); err != nil {
pushErrNonBlocking(errChan, err)
} else {
s.appendConversationHistoryCacheOptimistically(
context.Background(),
userID,
chatID,
buildOptimisticConversationHistoryItem(
"assistant",
assistantReply,
reasoningContent,
reasoningDurationSeconds,
retryMeta,
time.Now(),
),
)
}
}
// makeRoughBuildFunc 把 AgentService 上的 HybridScheduleWithPlanMultiFunc 封装成
// newAgent 层的 RoughBuildFunc将 HybridScheduleWithPlanMultiFunc 的结果转换为 RoughBuildPlacement。
// HybridScheduleWithPlanMultiFunc 未注入时返回 nilRoughBuild 节点会静默跳过粗排。
//
// 修复说明:
// 旧实现使用第二个返回值 []TaskClassItem只有 EmbeddedTime != nil 的条目(嵌入水课)才生成
// placement普通时段放置的任务全部被丢弃。
// 正确做法:使用第一个返回值 []HybridScheduleEntry过滤 Status="suggested" 且 TaskItemID>0 的条目,
// 这样嵌入和非嵌入的粗排结果都能正确写入 ScheduleState。
func (s *AgentService) makeRoughBuildFunc() newagentmodel.RoughBuildFunc {
if s.HybridScheduleWithPlanMultiFunc == nil {
return nil
}
return func(ctx context.Context, userID int, taskClassIDs []int) ([]newagentmodel.RoughBuildPlacement, error) {
entries, _, err := s.HybridScheduleWithPlanMultiFunc(ctx, userID, taskClassIDs)
if err != nil {
return nil, err
}
placements := make([]newagentmodel.RoughBuildPlacement, 0, len(entries))
for _, entry := range entries {
if entry.Status != "suggested" || entry.TaskItemID == 0 {
continue
}
placements = append(placements, newagentmodel.RoughBuildPlacement{
TaskItemID: entry.TaskItemID,
Week: entry.Week,
DayOfWeek: entry.DayOfWeek,
SectionFrom: entry.SectionFrom,
SectionTo: entry.SectionTo,
})
}
return placements, nil
}
}
// makeWriteSchedulePreviewFunc 封装 cacheDAO 写排程预览缓存的操作,供 Deliver 节点注入。
func (s *AgentService) makeWriteSchedulePreviewFunc() newagentmodel.WriteSchedulePreviewFunc {
if s.cacheDAO == nil {
return nil
}
return func(ctx context.Context, state *newagenttools.ScheduleState, userID int, conversationID string, taskClassIDs []int) error {
stateDigest := summarizeScheduleStateForPreviewDebug(state)
preview := conv.ScheduleStateToPreview(state, userID, conversationID, taskClassIDs, "")
if preview == nil {
log.Printf("[WARN] deliver preview skipped chat=%s user=%d state=%s", conversationID, userID, stateDigest)
return nil
}
previewDigest := summarizeHybridEntriesForPreviewDebug(preview.HybridEntries)
log.Printf(
"[DEBUG] deliver preview write chat=%s user=%d state=%s preview=%s generated_at=%s",
conversationID,
userID,
stateDigest,
previewDigest,
preview.GeneratedAt.Format(time.RFC3339),
)
return s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, conversationID, preview)
}
}
// summarizeScheduleStateForPreviewDebug 统计 Deliver 写预览前的内存日程摘要。
func summarizeScheduleStateForPreviewDebug(state *newagenttools.ScheduleState) string {
if state == nil {
return "state=nil"
}
total := len(state.Tasks)
pendingTotal := 0
suggestedTotal := 0
existingTotal := 0
taskItemWithSlot := 0
eventWithSlot := 0
for i := range state.Tasks {
t := &state.Tasks[i]
hasSlot := len(t.Slots) > 0
switch {
case newagenttools.IsPendingTask(*t):
pendingTotal++
case newagenttools.IsSuggestedTask(*t):
suggestedTotal++
case newagenttools.IsExistingTask(*t):
existingTotal++
}
if hasSlot {
if t.Source == "task_item" {
taskItemWithSlot++
}
if t.Source == "event" {
eventWithSlot++
}
}
}
return fmt.Sprintf(
"tasks=%d pending=%d suggested=%d existing=%d task_item_with_slot=%d event_with_slot=%d",
total,
pendingTotal,
suggestedTotal,
existingTotal,
taskItemWithSlot,
eventWithSlot,
)
}
// summarizeHybridEntriesForPreviewDebug 统计预览转换后的 HybridEntries 摘要。
func summarizeHybridEntriesForPreviewDebug(entries []model.HybridScheduleEntry) string {
existing := 0
suggested := 0
taskType := 0
courseType := 0
for _, e := range entries {
if e.Status == "suggested" {
suggested++
} else {
existing++
}
if e.Type == "task" {
taskType++
}
if e.Type == "course" {
courseType++
}
}
return fmt.Sprintf(
"entries=%d existing=%d suggested=%d task_type=%d course_type=%d",
len(entries),
existing,
suggested,
taskType,
courseType,
)
}
// --- 依赖注入字段 ---
// toolRegistry 由 cmd/start.go 注入
func (s *AgentService) SetToolRegistry(registry *newagenttools.ToolRegistry) {
s.toolRegistry = registry
}
// scheduleProvider 由 cmd/start.go 注入
func (s *AgentService) SetScheduleProvider(provider newagentmodel.ScheduleStateProvider) {
s.scheduleProvider = provider
}
// schedulePersistor 由 cmd/start.go 注入
func (s *AgentService) SetSchedulePersistor(persistor newagentmodel.SchedulePersistor) {
s.schedulePersistor = persistor
}
// agentStateStore 由 cmd/start.go 注入
func (s *AgentService) SetAgentStateStore(store newagentmodel.AgentStateStore) {
s.agentStateStore = store
}