package squad import ( "crypto/rand" "encoding/hex" "time" ) // HitlMode HITL 策略模式。 type HitlMode string const ( // HitlInteractive 默认:挂起并等待人工回应。 HitlInteractive HitlMode = "interactive" // HitlAuto 无值守:阻塞缺口按角色 fallback 假设兜底(未声明则步骤失败); // 高风险决策(计划确认/权限升级)不静默猜测,仍转为失败并留痕。 HitlAuto HitlMode = "auto" // HitlDisabled 不挂起:所有本应挂起的点直接按失败处理。 HitlDisabled HitlMode = "disabled" ) // HitlPolicy HITL 策略(全局默认 → 角色覆盖 → Goal 覆盖,窄者胜)。 type HitlPolicy struct { Mode HitlMode `json:"mode,omitempty"` // TimeoutAfter 非零时,pending 请求超过该时长未回应自动过期; // 过期动作由 ExpireAction 决定(fail / cancel)。 TimeoutAfter time.Duration `json:"timeout_after,omitempty"` // ExpireAction 过期动作:fail(默认,运行失败留痕)/ cancel(运行取消)。 ExpireAction string `json:"expire_action,omitempty"` } func (p HitlPolicy) mode() HitlMode { if p.Mode == "" { return HitlInteractive } return p.Mode } // HumanRequestKind 人工请求类型。 type HumanRequestKind string const ( HitlClarification HumanRequestKind = "clarification" // 规划歧义澄清 HitlConfirmation HumanRequestKind = "confirmation" // 计划/高风险步骤确认 HitlGap HumanRequestKind = "gap" // 阻塞缺口 HitlAdjustment HumanRequestKind = "adjustment" // 动态调整审批 HitlPermission HumanRequestKind = "permission" // 权限升级审批 HitlQualityReview HumanRequestKind = "quality_review" // 质检超限人工裁决 HitlEscalation HumanRequestKind = "escalation" // leader/成员升级问人(委派不了/需要拍板) ) // Severity 人工请求紧急度(inbox 语义:action_required 需要人做决定, // attention 建议关注;用于通知分层与降噪)。 const ( SeverityActionRequired = "action_required" SeverityAttention = "attention" ) // WaitCondition 挂起请求的自动唤醒条件(等事件而非只等人)。 // 条件满足时引擎合成一条自动决策续跑——唤醒报告的是事实,不是业务完成。 type WaitCondition struct { Type string `json:"type"` // run_done | field_set RunID string `json:"run_id,omitempty"` // run_done:等待该运行到达终态 Field string `json:"field,omitempty"` // field_set:等待黑板出现该字段 } // MaxWakeupFires 单个挂起请求自动唤醒次数上限(防失控护栏)。 const MaxWakeupFires = 5 // HumanRequest 一次结构化人工请求(可持久化、可恢复、按 ID 幂等回应)。 type HumanRequest struct { ID string `json:"id"` RunID string `json:"run_id"` StepID string `json:"step_id,omitempty"` Kind HumanRequestKind `json:"kind"` Severity string `json:"severity,omitempty"` // action_required(默认)/ attention Question string `json:"question"` // Reason 区分同一 Kind 的来源:plan、step_approval、interrupted。空值保持旧行为。 Reason string `json:"reason,omitempty"` Options []HumanOption `json:"options,omitempty"` Context string `json:"context,omitempty"` // 脱敏摘要(计划/缺口/质检报告) // WaitCondition 非空时,条件满足可自动唤醒(人工回应仍然优先)。 WaitCondition *WaitCondition `json:"wait_condition,omitempty"` // WakeupFires 已自动唤醒次数(护栏:达到 MaxWakeupFires 后过期)。 WakeupFires int `json:"wakeup_fires,omitempty"` CreatedAt time.Time `json:"created_at"` ExpiresAt *time.Time `json:"expires_at,omitempty"` Status string `json:"status"` // pending, answered, expired, cancelled AnsweredAt *time.Time `json:"answered_at,omitempty"` } // HumanOption 一个可选项;Value 为回应时提交的 choice。 type HumanOption struct { Value string `json:"value"` Label string `json:"label"` } // RespondDecision 对一次 HumanRequest 的人工决策。 type RespondDecision struct { RequestID string `json:"request_id"` // Answer 自由文本回答(clarification/gap)。 Answer string `json:"answer,omitempty"` // Choice 选项值(confirmation/adjustment/permission/quality_review 常用): // approve / reject / retry / skip / abort 等,或 HumanOption.Value。 Choice string `json:"choice,omitempty"` // EditPlan 确认计划时的修改指令(附加在 Answer 上传递给引擎)。 EditPlan string `json:"edit_plan,omitempty"` } // newRequestID 生成请求 ID。 func newRequestID() string { b := make([]byte, 8) _, _ = rand.Read(b) return "hr_" + hex.EncodeToString(b) } // expiredDecision 在请求超过 TimeoutAfter 或 ExpiresAt 时合成过期决策。 // ExpireAction 为 cancel 时选择 cancel,否则 fail。未到期返回 false。 func expiredDecision(req HumanRequest, policy HitlPolicy, now time.Time) (RespondDecision, bool) { var deadline time.Time switch { case req.ExpiresAt != nil: deadline = *req.ExpiresAt case policy.TimeoutAfter > 0 && !req.CreatedAt.IsZero(): deadline = req.CreatedAt.Add(policy.TimeoutAfter) default: return RespondDecision{}, false } if now.Before(deadline) { return RespondDecision{}, false } choice := "fail" if policy.ExpireAction == "cancel" { choice = "cancel" } return RespondDecision{RequestID: req.ID, Choice: choice, Answer: "expired"}, true } // hitlGate 引擎内部:一次挂起的完整上下文(落盘 pending.json)。 type hitlGate struct { Request HumanRequest `json:"request"` Plan *SquadPlan `json:"plan,omitempty"` // confirmation 时携带候选计划 Steps []StepSpec `json:"steps,omitempty"` // adjustment:建议追加的步骤 SkipIDs []string `json:"skip_ids,omitempty"` // adjustment:建议跳过的步骤 PrevStatus RunStatus `json:"prev_status"` // 回应后恢复到的状态 } // mergeHitlPolicy 按全局 → 角色 → Goal 顺序取窄者(后设置者覆盖)。 func mergeHitlPolicy(base HitlPolicy, overrides ...*HitlPolicy) HitlPolicy { out := base for _, ov := range overrides { if ov == nil { continue } if ov.Mode != "" { out.Mode = ov.Mode } if ov.TimeoutAfter != 0 { out.TimeoutAfter = ov.TimeoutAfter } if ov.ExpireAction != "" { out.ExpireAction = ov.ExpireAction } } return out }