You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
+261
-1
@@ -42,8 +42,26 @@ type ACPSession struct {
|
||||
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)。
|
||||
@@ -72,6 +90,38 @@ type ACPSessionFilter struct {
|
||||
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)
|
||||
@@ -81,6 +131,11 @@ type ACPBridge interface {
|
||||
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 工具。
|
||||
@@ -92,6 +147,9 @@ func registerACPTools(srv *mcpsdk.Server, deps ServerDeps) {
|
||||
registerListACPSessions(srv, deps)
|
||||
registerTerminateACPSession(srv, deps)
|
||||
registerListPendingPermissions(srv, deps)
|
||||
registerListPendingApprovals(srv, deps)
|
||||
registerGetACPSessionTurns(srv, deps)
|
||||
registerGetRunDashboard(srv, deps)
|
||||
}
|
||||
|
||||
// ===== DTO =====
|
||||
@@ -124,8 +182,16 @@ type acpSessionDetail struct {
|
||||
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 {
|
||||
@@ -134,7 +200,13 @@ func acpSessionDetailFrom(s *ACPSession) acpSessionDetail {
|
||||
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"),
|
||||
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()
|
||||
@@ -145,6 +217,9 @@ func acpSessionDetailFrom(s *ACPSession) acpSessionDetail {
|
||||
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")
|
||||
}
|
||||
@@ -432,3 +507,188 @@ func registerListPendingPermissions(srv *mcpsdk.Server, deps ServerDeps) {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user