Version: 0.9.75.dev.260505

后端:
1.收口阶段 6 agent 结构迁移,将 newAgent 内核与 agentsvc 编排层迁入 services/agent
- 切换 Agent 启动装配与 HTTP handler 直连 agent sv,移除旧 service agent bridge
- 补齐 Agent 对 memory、task、task-class、schedule 的 RPC 适配与契约字段
- 扩展 schedule、task、task-class RPC/contract 支撑 Agent 查询、写入与 provider 切流
- 更新迁移文档、README 与相关注释,明确 agent 当前切流点和剩余 memory 迁移面
This commit is contained in:
Losita
2026-05-05 16:00:57 +08:00
parent e1819c5653
commit d7184b776b
174 changed files with 2189 additions and 1236 deletions

View File

@@ -0,0 +1,407 @@
package agenttools
import (
"encoding/json"
"strings"
"github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule"
toolcontextresult "github.com/LoveLosita/smartflow/backend/services/agent/tools/tool_context_result"
)
type contextToolsAddResult struct {
Tool string `json:"tool"`
Success bool `json:"success"`
Action string `json:"action"`
Domain string `json:"domain,omitempty"`
Packs []string `json:"packs,omitempty"`
Mode string `json:"mode,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
}
type contextToolsRemoveResult struct {
Tool string `json:"tool"`
Success bool `json:"success"`
Action string `json:"action"`
Domain string `json:"domain,omitempty"`
Packs []string `json:"packs,omitempty"`
All bool `json:"all,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
}
// NewContextToolsAddHandler 创建 context_tools_add 工具。
//
// 职责边界:
// 1. 这里只负责校验 domain / packs / mode并产出结构化结果
// 2. 不直接改 CommonState真正的激活切流仍由 execute 层读取 observation 后更新快照;
// 3. 因为这里拿不到 CommonState所以卡片展示的是“本次工具结果返回的 domain/packs/mode”不是全局最终快照。
func NewContextToolsAddHandler() ToolHandler {
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
_ = state
domain := NormalizeToolDomain(readContextToolString(args["domain"]))
if domain == "" {
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: false,
Action: "reject",
Error: "参数非法domain 仅支持 schedule/taskclass",
ErrorCode: "invalid_domain",
})
}
mode := strings.ToLower(strings.TrimSpace(readContextToolString(args["mode"])))
if mode == "" {
mode = "replace"
}
if mode != "replace" && mode != "merge" {
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: false,
Action: "reject",
Domain: domain,
Error: "参数非法mode 仅支持 replace/merge",
ErrorCode: "invalid_mode",
})
}
packsRaw := readContextToolStringSlice(args["packs"])
packs, errCode, errText := validateContextPacks(domain, packsRaw, false)
if errCode != "" {
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: false,
Action: "reject",
Domain: domain,
Error: errText,
ErrorCode: errCode,
})
}
// 1. schedule 未显式传 packs 时,默认激活最小可用包 mutation+analyze。
// 2. taskclass 当前没有可选包,所以这里会保持空切片,由 execute 层只保留固定 core。
// 3. 这样做可以让 observation 直接表达“本次实际生效的可选包集合”,减少展示层再二次猜测。
if domain == ToolDomainSchedule && len(packsRaw) == 0 {
packs = ResolveEffectiveToolPacks(domain, nil)
}
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: true,
Action: "activate",
Domain: domain,
Packs: packs,
Mode: mode,
Message: "已激活目标工具域,可继续调用对应业务工具。",
})
}
}
// NewContextToolsRemoveHandler 创建 context_tools_remove 工具。
//
// 职责边界:
// 1. 这里只解释 domain / packs / all 的语义,并返回结构化结果;
// 2. all=true 表示清空全部业务工具域domain+packs 表示移除某域下的可选包;
// 3. 实际 CommonState 的域/包更新,仍由 execute 层统一消费 observation 完成。
func NewContextToolsRemoveHandler() ToolHandler {
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
_ = state
all := readContextToolBool(args["all"])
domainRaw := strings.ToLower(strings.TrimSpace(readContextToolString(args["domain"])))
packsRaw := readContextToolStringSlice(args["packs"])
// 兼容旧写法domain=all 也视为清空全部业务工具域。
if domainRaw == "all" {
all = true
}
if all {
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: true,
Action: "clear_all",
All: true,
Message: "已移除全部业务工具域,仅保留 context 管理工具。",
})
}
domain := NormalizeToolDomain(domainRaw)
if domain == "" {
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: false,
Action: "reject",
Error: "参数非法:需要提供 domain=schedule/taskclass 或 all=true",
ErrorCode: "invalid_domain",
})
}
packs, errCode, errText := validateContextPacks(domain, packsRaw, true)
if errCode != "" {
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: false,
Action: "reject",
Domain: domain,
Error: errText,
ErrorCode: errCode,
})
}
if len(packs) > 0 {
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: true,
Action: "deactivate_packs",
Domain: domain,
Packs: packs,
Message: "已移除指定工具包。",
})
}
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: true,
Action: "deactivate",
Domain: domain,
Message: "已移除指定工具域。",
})
}
}
func buildContextToolsAddExecutionResult(args map[string]any, payload contextToolsAddResult) ToolExecutionResult {
observation := marshalContextToolsAddResult(payload)
legacy := LegacyResult(ToolNameContextToolsAdd, args, observation)
view := toolcontextresult.BuildAddView(toContextToolsAddPayload(payload), observation)
return buildContextToolExecutionResult(legacy, args, view)
}
func buildContextToolsRemoveExecutionResult(args map[string]any, payload contextToolsRemoveResult) ToolExecutionResult {
observation := marshalContextToolsRemoveResult(payload)
legacy := LegacyResult(ToolNameContextToolsRemove, args, observation)
view := toolcontextresult.BuildRemoveView(toContextToolsRemovePayload(payload), observation)
return buildContextToolExecutionResult(legacy, args, view)
}
// buildContextToolExecutionResult 负责把子包纯展示结构包回 ToolExecutionResult。
//
// 职责边界:
// 1. 只做 ContextResultView -> ToolDisplayView 的协议桥接;
// 2. 不改写 ObservationText确保模型侧仍消费原始 observation JSON
// 3. 错误码与错误文案继续复用父包现有 JSON/text 解析逻辑,避免多套失败判定分叉。
func buildContextToolExecutionResult(
legacy ToolExecutionResult,
args map[string]any,
view toolcontextresult.ContextResultView,
) ToolExecutionResult {
result := legacy
status := normalizeToolStatus(result.Status)
if status == "" {
status = ToolStatusDone
}
collapsed := cloneAnyMap(view.Collapsed)
if collapsed == nil {
collapsed = make(map[string]any)
}
expanded := cloneAnyMap(view.Expanded)
if expanded == nil {
expanded = make(map[string]any)
}
collapsed["status"] = status
if _, exists := collapsed["status_label"]; !exists {
collapsed["status_label"] = resolveToolStatusLabelCN(status)
}
if _, exists := expanded["raw_text"]; !exists {
expanded["raw_text"] = result.ObservationText
}
result.Status = status
result.Success = status == ToolStatusDone
result.ResultView = &ToolDisplayView{
ViewType: strings.TrimSpace(view.ViewType),
Version: view.Version,
Collapsed: collapsed,
Expanded: expanded,
}
if title, ok := readStringAnyMap(collapsed, "title"); ok {
result.Summary = title
}
if !result.Success {
errorCode, errorMessage := extractToolErrorInfo(result.ObservationText, status)
if strings.TrimSpace(result.ErrorCode) == "" {
result.ErrorCode = strings.TrimSpace(errorCode)
}
if strings.TrimSpace(result.ErrorMessage) == "" {
result.ErrorMessage = strings.TrimSpace(errorMessage)
}
}
return EnsureToolResultDefaults(result, args)
}
func toContextToolsAddPayload(payload contextToolsAddResult) toolcontextresult.ContextToolsAddPayload {
return toolcontextresult.ContextToolsAddPayload{
Tool: payload.Tool,
Success: payload.Success,
Action: payload.Action,
Domain: payload.Domain,
Packs: append([]string(nil), payload.Packs...),
Mode: payload.Mode,
Message: payload.Message,
Error: payload.Error,
ErrorCode: payload.ErrorCode,
}
}
func toContextToolsRemovePayload(payload contextToolsRemoveResult) toolcontextresult.ContextToolsRemovePayload {
return toolcontextresult.ContextToolsRemovePayload{
Tool: payload.Tool,
Success: payload.Success,
Action: payload.Action,
Domain: payload.Domain,
Packs: append([]string(nil), payload.Packs...),
All: payload.All,
Message: payload.Message,
Error: payload.Error,
ErrorCode: payload.ErrorCode,
}
}
func validateContextPacks(domain string, packs []string, forRemove bool) ([]string, string, string) {
normalizedDomain := NormalizeToolDomain(domain)
if normalizedDomain == "" {
return nil, "invalid_domain", "参数非法domain 非法"
}
if len(packs) == 0 {
return nil, "", ""
}
if normalizedDomain == ToolDomainTaskClass {
return nil, "unsupported_packs_for_domain", "参数非法taskclass 暂不支持 packs"
}
normalized := make([]string, 0, len(packs))
seen := make(map[string]struct{}, len(packs))
for _, raw := range packs {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
continue
}
pack := NormalizeToolPack(normalizedDomain, trimmed)
if pack == "" {
return nil, "invalid_pack", "参数非法:存在不支持的 pack"
}
if IsFixedToolPack(normalizedDomain, pack) {
if forRemove {
return nil, "fixed_pack_forbidden", "参数非法core 为固定包,不允许 remove"
}
return nil, "fixed_pack_forbidden", "参数非法core 为固定包,不允许 add"
}
if _, exists := seen[pack]; exists {
continue
}
seen[pack] = struct{}{}
normalized = append(normalized, pack)
}
if len(normalized) == 0 {
return nil, "invalid_pack", "参数非法packs 为空或无效"
}
return normalized, "", ""
}
func readContextToolString(raw any) string {
text, _ := raw.(string)
return strings.TrimSpace(text)
}
func readContextToolStringSlice(raw any) []string {
switch typed := raw.(type) {
case []string:
out := make([]string, 0, len(typed))
for _, item := range typed {
text := strings.TrimSpace(item)
if text == "" {
continue
}
out = append(out, text)
}
return out
case []any:
out := make([]string, 0, len(typed))
for _, item := range typed {
text, ok := item.(string)
if !ok {
continue
}
text = strings.TrimSpace(text)
if text == "" {
continue
}
out = append(out, text)
}
return out
case string:
text := strings.TrimSpace(typed)
if text == "" {
return nil
}
parts := strings.Split(text, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
out = append(out, part)
}
return out
default:
return nil
}
}
func readContextToolBool(raw any) bool {
switch v := raw.(type) {
case bool:
return v
case string:
value := strings.ToLower(strings.TrimSpace(v))
return value == "1" || value == "true" || value == "yes"
case float64:
return v != 0
case float32:
return v != 0
case int:
return v != 0
case int8:
return v != 0
case int16:
return v != 0
case int32:
return v != 0
case int64:
return v != 0
default:
return false
}
}
func marshalContextToolsAddResult(result contextToolsAddResult) string {
raw, err := json.Marshal(result)
if err != nil {
return `{"tool":"context_tools_add","success":false,"action":"reject","error":"result encode failed","error_code":"encode_failed"}`
}
return string(raw)
}
func marshalContextToolsRemoveResult(result contextToolsRemoveResult) string {
raw, err := json.Marshal(result)
if err != nil {
return `{"tool":"context_tools_remove","success":false,"action":"reject","error":"result encode failed","error_code":"encode_failed"}`
}
return string(raw)
}