package mcp import ( "context" "time" "github.com/google/uuid" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" ) // ===== ACP 桥接抽象 ===== // // acp 包 import 了 mcp(supervisor 签发 system token),mcp 不能反向 import acp。 // 这里定义 acp-free 的桥接接口与 DTO,由 internal/acp/mcp_bridge.go 实现适配 // (acp → mcp 的既有依赖边,不新增循环)。 // // 范围边界(spec 决策):agent kind 的增删改(含加密 env)与权限 approve/deny // 不经 MCP 暴露——前者是秘密载体,后者的设计初衷是人工把关。 // ACPAgentKind 是 agent kind 的 MCP 投影(不含 binary/args/env 等部署细节)。 type ACPAgentKind struct { ID uuid.UUID Name string DisplayName string Description string Enabled bool ToolAllowlist []string } // ACPSession 是 ACP 会话的 MCP 投影。 type ACPSession struct { ID uuid.UUID WorkspaceID uuid.UUID ProjectID uuid.UUID AgentKindID uuid.UUID UserID uuid.UUID IssueID *uuid.UUID RequirementID *uuid.UUID Branch string IsMainWorktree bool Status string LastError *string LastStopReason *string StartedAt time.Time EndedAt *time.Time // 成本核算(只读):运行时累计 token/cost + 有效预算上限。 PromptTokens int64 CompletionTokens int64 ThinkingTokens int64 TotalCostUSD float64 BudgetMaxCostUSD *float64 BudgetMaxTokens *int64 } // ACPTurn 是 ACP 回合记录的 MCP 投影(只读)。 type ACPTurn struct { TurnIndex int Status string StopReason *string UpdateCount int StartedAt time.Time CompletedAt *time.Time } // ACPPermission 是待审权限请求的 MCP 投影(只读,不含原始 toolCall/options JSON)。 type ACPPermission struct { ID uuid.UUID SessionID uuid.UUID AgentRequestID string ToolName string Status string CreatedAt time.Time } // ACPCreateSessionInput 与 acp.CreateSessionInput 字段一一对应。 type ACPCreateSessionInput struct { WorkspaceID uuid.UUID AgentKindID uuid.UUID Branch *string IssueNumber *int RequirementNumber *int InitialPrompt *string } // ACPSessionFilter 与 acp.SessionListFilter 字段一一对应。 type ACPSessionFilter struct { All bool RequirementID *uuid.UUID } // ACPRunDashboardInput 是 run-history 汇总入参(service 在 caller scope 内归一化)。 type ACPRunDashboardInput struct { ProjectID *uuid.UUID RequirementID *uuid.UUID From time.Time To time.Time } // ACPRunDashboard 是 run-history 汇总(计数 + 成功率 + 成本 + 时长 + 按天序列)。 type ACPRunDashboard struct { From time.Time To time.Time Total int64 Succeeded int64 Crashed int64 Active int64 SuccessRate float64 TotalCostUSD float64 TotalTokens int64 AvgDurationSeconds float64 ByDay []ACPRunDayRow } // ACPRunDayRow 是 run-history 按天的一行。 type ACPRunDayRow struct { Day time.Time Total int64 Succeeded int64 Crashed int64 TotalCostUSD float64 } // ACPBridge 是 mcp 调用 acp 服务的桥接接口。 type ACPBridge interface { ListAgentKinds(ctx context.Context, userID uuid.UUID, isAdmin bool) ([]ACPAgentKind, error) GetAgentKind(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) (*ACPAgentKind, error) CreateSession(ctx context.Context, userID uuid.UUID, isAdmin bool, in ACPCreateSessionInput) (*ACPSession, error) GetSession(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) (*ACPSession, error) ListSessions(ctx context.Context, userID uuid.UUID, isAdmin bool, f ACPSessionFilter) ([]ACPSession, error) TerminateSession(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) error ListPendingPermissions(ctx context.Context, userID uuid.UUID, isAdmin bool, sessionID uuid.UUID) ([]ACPPermission, error) // ListPendingApprovals 返回调用者跨所有会话的待审权限请求(HITL inbox,read-only)。 ListPendingApprovals(ctx context.Context, userID uuid.UUID, isAdmin bool) ([]ACPPermission, error) ListSessionTurns(ctx context.Context, userID uuid.UUID, isAdmin bool, sessionID uuid.UUID) ([]ACPTurn, error) // GetRunDashboard 返回 run-history 汇总(owner scope;admin 全量),read-only。 GetRunDashboard(ctx context.Context, userID uuid.UUID, isAdmin bool, in ACPRunDashboardInput) (*ACPRunDashboard, error) } // registerACPTools 注册 ACP 相关 MCP 工具。 func registerACPTools(srv *mcpsdk.Server, deps ServerDeps) { registerListAgentKinds(srv, deps) registerGetAgentKind(srv, deps) registerCreateACPSession(srv, deps) registerGetACPSession(srv, deps) registerListACPSessions(srv, deps) registerTerminateACPSession(srv, deps) registerListPendingPermissions(srv, deps) registerListPendingApprovals(srv, deps) registerGetACPSessionTurns(srv, deps) registerGetRunDashboard(srv, deps) } // ===== DTO ===== type agentKindDetail struct { ID string `json:"id"` Name string `json:"name"` DisplayName string `json:"display_name"` Description string `json:"description,omitempty"` Enabled bool `json:"enabled"` ToolAllowlist []string `json:"tool_allowlist,omitempty"` } func agentKindDetailFrom(k ACPAgentKind) agentKindDetail { return agentKindDetail{ ID: k.ID.String(), Name: k.Name, DisplayName: k.DisplayName, Description: k.Description, Enabled: k.Enabled, ToolAllowlist: k.ToolAllowlist, } } type acpSessionDetail struct { ID string `json:"id"` WorkspaceID string `json:"workspace_id"` ProjectID string `json:"project_id"` AgentKindID string `json:"agent_kind_id"` UserID string `json:"user_id"` IssueID string `json:"issue_id,omitempty"` RequirementID string `json:"requirement_id,omitempty"` Branch string `json:"branch,omitempty"` IsMainWorktree bool `json:"is_main_worktree"` Status string `json:"status"` LastError string `json:"last_error,omitempty"` LastStopReason string `json:"last_stop_reason,omitempty"` StartedAt string `json:"started_at"` EndedAt string `json:"ended_at,omitempty"` // 成本核算(只读)。 PromptTokens int64 `json:"prompt_tokens"` CompletionTokens int64 `json:"completion_tokens"` ThinkingTokens int64 `json:"thinking_tokens"` TotalCostUSD float64 `json:"total_cost_usd"` BudgetMaxCostUSD *float64 `json:"budget_max_cost_usd,omitempty"` BudgetMaxTokens *int64 `json:"budget_max_tokens,omitempty"` } func acpSessionDetailFrom(s *ACPSession) acpSessionDetail { d := acpSessionDetail{ ID: s.ID.String(), WorkspaceID: s.WorkspaceID.String(), ProjectID: s.ProjectID.String(), AgentKindID: s.AgentKindID.String(), UserID: s.UserID.String(), Branch: s.Branch, IsMainWorktree: s.IsMainWorktree, Status: s.Status, StartedAt: s.StartedAt.Format("2006-01-02T15:04:05Z07:00"), PromptTokens: s.PromptTokens, CompletionTokens: s.CompletionTokens, ThinkingTokens: s.ThinkingTokens, TotalCostUSD: s.TotalCostUSD, BudgetMaxCostUSD: s.BudgetMaxCostUSD, BudgetMaxTokens: s.BudgetMaxTokens, } if s.IssueID != nil { d.IssueID = s.IssueID.String() } if s.RequirementID != nil { d.RequirementID = s.RequirementID.String() } if s.LastError != nil { d.LastError = *s.LastError } if s.LastStopReason != nil { d.LastStopReason = *s.LastStopReason } if s.EndedAt != nil { d.EndedAt = s.EndedAt.Format("2006-01-02T15:04:05Z07:00") } return d } // checkNotSystemToken 拒绝 system token(agent 会话签发)调用:用于 // create_acp_session,防止 agent 在会话内套娃创建嵌套会话。 func checkNotSystemToken(ctx context.Context) error { sess, ok := FromContext(ctx) if !ok { return errs.New(errs.CodeInternal, "mcp: AuthSession missing") } if sess.Issuer == IssuerSystem { return errs.New(errs.CodeForbidden, "system tokens (agent sessions) cannot create nested ACP sessions") } return nil } // scopedACPSession 取 session 并做项目 scope 校验。 func scopedACPSession(ctx context.Context, deps ServerDeps, userID uuid.UUID, isAdmin bool, sessionID string) (*ACPSession, error) { sid, err := uuid.Parse(sessionID) if err != nil { return nil, errs.Wrap(err, errs.CodeInvalidInput, "session_id parse") } s, err := deps.ACP.GetSession(ctx, userID, isAdmin, sid) if err != nil { return nil, err } if err := CheckProjectFromCtx(ctx, s.ProjectID); err != nil { return nil, err } return s, nil } // ===== list_agent_kinds / get_agent_kind ===== type listAgentKindsOut struct { AgentKinds []agentKindDetail `json:"agent_kinds"` } func registerListAgentKinds(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_agent_kinds", Description: "List ACP agent kinds available to the caller."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, listAgentKindsOut, error) { if err := CheckToolFromCtx(ctx, "list_agent_kinds"); err != nil { return nil, listAgentKindsOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listAgentKindsOut{}, err } ks, err := deps.ACP.ListAgentKinds(ctx, c.UserID, c.IsAdmin) if err != nil { return nil, listAgentKindsOut{}, err } out := listAgentKindsOut{AgentKinds: make([]agentKindDetail, 0, len(ks))} for _, k := range ks { out.AgentKinds = append(out.AgentKinds, agentKindDetailFrom(k)) } return nil, out, nil }) } type getAgentKindIn struct { AgentKindID string `json:"agent_kind_id" jsonschema:"agent kind UUID"` } func registerGetAgentKind(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "get_agent_kind", Description: "Get a single ACP agent kind by ID."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getAgentKindIn) (*mcpsdk.CallToolResult, agentKindDetail, error) { if err := CheckToolFromCtx(ctx, "get_agent_kind"); err != nil { return nil, agentKindDetail{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, agentKindDetail{}, err } kid, err := uuid.Parse(in.AgentKindID) if err != nil { return nil, agentKindDetail{}, errs.Wrap(err, errs.CodeInvalidInput, "agent_kind_id parse") } k, err := deps.ACP.GetAgentKind(ctx, c.UserID, c.IsAdmin, kid) if err != nil { return nil, agentKindDetail{}, err } return nil, agentKindDetailFrom(*k), nil }) } // ===== create_acp_session ===== type createACPSessionIn struct { WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"` AgentKindID string `json:"agent_kind_id" jsonschema:"agent kind UUID"` Branch string `json:"branch,omitempty" jsonschema:"work on this branch (mutually exclusive with issue/requirement number)"` IssueNumber int `json:"issue_number,omitempty" jsonschema:"bind to an issue"` RequirementNumber int `json:"requirement_number,omitempty" jsonschema:"bind to a requirement"` InitialPrompt string `json:"initial_prompt,omitempty" jsonschema:"prompt sent to the agent after startup"` } type acpSessionCreateOut struct { OK bool `json:"ok"` Session acpSessionDetail `json:"session"` } func registerCreateACPSession(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "create_acp_session", Description: "Start an ACP agent session on a workspace."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createACPSessionIn) (*mcpsdk.CallToolResult, acpSessionCreateOut, error) { if err := CheckToolFromCtx(ctx, "create_acp_session"); err != nil { return nil, acpSessionCreateOut{}, err } if err := checkNotSystemToken(ctx); err != nil { return nil, acpSessionCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, acpSessionCreateOut{}, err } w, _, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, acpSessionCreateOut{}, err } kid, err := uuid.Parse(in.AgentKindID) if err != nil { return nil, acpSessionCreateOut{}, errs.Wrap(err, errs.CodeInvalidInput, "agent_kind_id parse") } input := ACPCreateSessionInput{WorkspaceID: w.ID, AgentKindID: kid} if in.Branch != "" { input.Branch = &in.Branch } if in.IssueNumber > 0 { n := in.IssueNumber input.IssueNumber = &n } if in.RequirementNumber > 0 { n := in.RequirementNumber input.RequirementNumber = &n } if in.InitialPrompt != "" { input.InitialPrompt = &in.InitialPrompt } s, err := deps.ACP.CreateSession(ctx, c.UserID, c.IsAdmin, input) if err != nil { return nil, acpSessionCreateOut{}, err } return nil, acpSessionCreateOut{OK: true, Session: acpSessionDetailFrom(s)}, nil }) } // ===== get/list/terminate_acp_session ===== type getACPSessionIn struct { SessionID string `json:"session_id" jsonschema:"ACP session UUID"` } func registerGetACPSession(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "get_acp_session", Description: "Get a single ACP session by ID."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, acpSessionDetail, error) { if err := CheckToolFromCtx(ctx, "get_acp_session"); err != nil { return nil, acpSessionDetail{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, acpSessionDetail{}, err } s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID) if err != nil { return nil, acpSessionDetail{}, err } return nil, acpSessionDetailFrom(s), nil }) } type listACPSessionsIn struct { All bool `json:"all,omitempty" jsonschema:"admin only: list sessions of all users"` RequirementID string `json:"requirement_id,omitempty" jsonschema:"filter by requirement UUID"` } type listACPSessionsOut struct { Sessions []acpSessionDetail `json:"sessions"` } func registerListACPSessions(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_acp_sessions", Description: "List ACP sessions of the caller (all=true for admins)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listACPSessionsIn) (*mcpsdk.CallToolResult, listACPSessionsOut, error) { if err := CheckToolFromCtx(ctx, "list_acp_sessions"); err != nil { return nil, listACPSessionsOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listACPSessionsOut{}, err } f := ACPSessionFilter{All: in.All} if in.RequirementID != "" { rid, err := uuid.Parse(in.RequirementID) if err != nil { return nil, listACPSessionsOut{}, errs.Wrap(err, errs.CodeInvalidInput, "requirement_id parse") } f.RequirementID = &rid } ss, err := deps.ACP.ListSessions(ctx, c.UserID, c.IsAdmin, f) if err != nil { return nil, listACPSessionsOut{}, err } out := listACPSessionsOut{Sessions: make([]acpSessionDetail, 0, len(ss))} for i := range ss { // 与 list_projects 一致:scope 外的条目静默跳过。 if err := CheckProjectFromCtx(ctx, ss[i].ProjectID); err != nil { continue } out.Sessions = append(out.Sessions, acpSessionDetailFrom(&ss[i])) } return nil, out, nil }) } func registerTerminateACPSession(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "terminate_acp_session", Description: "Terminate a running ACP session."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, okOut, error) { if err := CheckToolFromCtx(ctx, "terminate_acp_session"); err != nil { return nil, okOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, okOut{}, err } s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID) if err != nil { return nil, okOut{}, err } if err := deps.ACP.TerminateSession(ctx, c.UserID, c.IsAdmin, s.ID); err != nil { return nil, okOut{}, err } return nil, okOut{OK: true}, nil }) } // ===== list_pending_permissions ===== type permissionDetail struct { ID string `json:"id"` SessionID string `json:"session_id"` AgentRequestID string `json:"agent_request_id"` ToolName string `json:"tool_name"` Status string `json:"status"` CreatedAt string `json:"created_at"` } type listPendingPermissionsOut struct { Permissions []permissionDetail `json:"permissions"` } func registerListPendingPermissions(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_pending_permissions", Description: "List pending permission requests of an ACP session (read-only; approval stays with humans in the web console)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, listPendingPermissionsOut, error) { if err := CheckToolFromCtx(ctx, "list_pending_permissions"); err != nil { return nil, listPendingPermissionsOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listPendingPermissionsOut{}, err } s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID) if err != nil { return nil, listPendingPermissionsOut{}, err } ps, err := deps.ACP.ListPendingPermissions(ctx, c.UserID, c.IsAdmin, s.ID) if err != nil { return nil, listPendingPermissionsOut{}, err } out := listPendingPermissionsOut{Permissions: make([]permissionDetail, 0, len(ps))} for _, p := range ps { out.Permissions = append(out.Permissions, permissionDetail{ ID: p.ID.String(), SessionID: p.SessionID.String(), AgentRequestID: p.AgentRequestID, ToolName: p.ToolName, Status: p.Status, CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }) } return nil, out, nil }) } // ===== list_pending_approvals (cross-session HITL inbox) ===== func registerListPendingApprovals(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_pending_approvals", Description: "List the caller's pending tool-permission requests across all their ACP sessions (read-only HITL inbox; approval stays with humans in the web console)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, listPendingPermissionsOut, error) { if err := CheckToolFromCtx(ctx, "list_pending_approvals"); err != nil { return nil, listPendingPermissionsOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listPendingPermissionsOut{}, err } ps, err := deps.ACP.ListPendingApprovals(ctx, c.UserID, c.IsAdmin) if err != nil { return nil, listPendingPermissionsOut{}, err } out := listPendingPermissionsOut{Permissions: make([]permissionDetail, 0, len(ps))} for _, p := range ps { out.Permissions = append(out.Permissions, permissionDetail{ ID: p.ID.String(), SessionID: p.SessionID.String(), AgentRequestID: p.AgentRequestID, ToolName: p.ToolName, Status: p.Status, CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }) } return nil, out, nil }) } // ===== get_acp_session_turns ===== type acpTurnDetail struct { TurnIndex int `json:"turn_index"` Status string `json:"status"` StopReason string `json:"stop_reason,omitempty"` UpdateCount int `json:"update_count"` StartedAt string `json:"started_at"` CompletedAt string `json:"completed_at,omitempty"` } type getACPSessionTurnsOut struct { Turns []acpTurnDetail `json:"turns"` } func registerGetACPSessionTurns(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "get_acp_session_turns", Description: "List the agent turns of an ACP session (read-only; turn_index, status, stop_reason, update_count, timestamps)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, getACPSessionTurnsOut, error) { if err := CheckToolFromCtx(ctx, "get_acp_session_turns"); err != nil { return nil, getACPSessionTurnsOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, getACPSessionTurnsOut{}, err } s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID) if err != nil { return nil, getACPSessionTurnsOut{}, err } ts, err := deps.ACP.ListSessionTurns(ctx, c.UserID, c.IsAdmin, s.ID) if err != nil { return nil, getACPSessionTurnsOut{}, err } out := getACPSessionTurnsOut{Turns: make([]acpTurnDetail, 0, len(ts))} for _, t := range ts { d := acpTurnDetail{ TurnIndex: t.TurnIndex, Status: t.Status, UpdateCount: t.UpdateCount, StartedAt: t.StartedAt.Format("2006-01-02T15:04:05Z07:00"), } if t.StopReason != nil { d.StopReason = *t.StopReason } if t.CompletedAt != nil { d.CompletedAt = t.CompletedAt.Format("2006-01-02T15:04:05Z07:00") } out.Turns = append(out.Turns, d) } return nil, out, nil }) } // ===== get_run_dashboard (run-history rollup) ===== type getRunDashboardIn struct { // 可选过滤;留空表示不按该维度过滤。from/to 为 RFC3339;留空走默认 30 天窗口。 ProjectID string `json:"project_id,omitempty"` RequirementID string `json:"requirement_id,omitempty"` From string `json:"from,omitempty"` To string `json:"to,omitempty"` } type runDayDetail struct { Day string `json:"day"` Total int64 `json:"total"` Succeeded int64 `json:"succeeded"` Crashed int64 `json:"crashed"` TotalCostUSD float64 `json:"total_cost_usd"` } type getRunDashboardOut struct { From string `json:"from"` To string `json:"to"` Total int64 `json:"total"` Succeeded int64 `json:"succeeded"` Crashed int64 `json:"crashed"` Active int64 `json:"active"` SuccessRate float64 `json:"success_rate"` TotalCostUSD float64 `json:"total_cost_usd"` TotalTokens int64 `json:"total_tokens"` AvgDurationSeconds float64 `json:"avg_duration_seconds"` ByDay []runDayDetail `json:"by_day"` } func registerGetRunDashboard(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "get_run_dashboard", Description: "Get a run-history rollup of ACP sessions (read-only): counts, success rate, total cost/tokens, avg duration, and a per-day series. Scoped to the caller's own sessions (admins see all). Optional filters: project_id, requirement_id, from/to (RFC3339)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getRunDashboardIn) (*mcpsdk.CallToolResult, getRunDashboardOut, error) { if err := CheckToolFromCtx(ctx, "get_run_dashboard"); err != nil { return nil, getRunDashboardOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, getRunDashboardOut{}, err } var di ACPRunDashboardInput if in.ProjectID != "" { id, perr := uuid.Parse(in.ProjectID) if perr != nil { return nil, getRunDashboardOut{}, errs.Wrap(perr, errs.CodeInvalidInput, "project_id parse") } di.ProjectID = &id } if in.RequirementID != "" { id, perr := uuid.Parse(in.RequirementID) if perr != nil { return nil, getRunDashboardOut{}, errs.Wrap(perr, errs.CodeInvalidInput, "requirement_id parse") } di.RequirementID = &id } if in.From != "" { t, terr := time.Parse(time.RFC3339, in.From) if terr != nil { return nil, getRunDashboardOut{}, errs.Wrap(terr, errs.CodeInvalidInput, "from parse") } di.From = t } if in.To != "" { t, terr := time.Parse(time.RFC3339, in.To) if terr != nil { return nil, getRunDashboardOut{}, errs.Wrap(terr, errs.CodeInvalidInput, "to parse") } di.To = t } d, err := deps.ACP.GetRunDashboard(ctx, c.UserID, c.IsAdmin, di) if err != nil { return nil, getRunDashboardOut{}, err } out := getRunDashboardOut{ From: d.From.Format("2006-01-02T15:04:05Z07:00"), To: d.To.Format("2006-01-02T15:04:05Z07:00"), Total: d.Total, Succeeded: d.Succeeded, Crashed: d.Crashed, Active: d.Active, SuccessRate: d.SuccessRate, TotalCostUSD: d.TotalCostUSD, TotalTokens: d.TotalTokens, AvgDurationSeconds: d.AvgDurationSeconds, ByDay: make([]runDayDetail, 0, len(d.ByDay)), } for _, r := range d.ByDay { out.ByDay = append(out.ByDay, runDayDetail{ Day: r.Day.Format("2006-01-02T15:04:05Z07:00"), Total: r.Total, Succeeded: r.Succeeded, Crashed: r.Crashed, TotalCostUSD: r.TotalCostUSD, }) } return nil, out, nil }) }