feat(agent): 在小队编排与任务生命周期中打通阶段性状态与摘要流式通知
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
weichen
co-authored by
Cursor
parent
1bdea54022
commit
0f01f047d5
@@ -12,6 +12,7 @@ import (
|
||||
"chat-agent-system/internal/config"
|
||||
"chat-agent-system/internal/kb"
|
||||
"chat-agent-system/internal/squad"
|
||||
"chat-agent-system/internal/stream"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -82,7 +83,19 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
|
||||
}
|
||||
searchQuery := strings.TrimSpace(req.ProjectName + " " + device + " " + market)
|
||||
if searchQuery != "" {
|
||||
streamer := stream.FromContext(ctx)
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("knowledge_retrieval", fmt.Sprintf("法规专家正在检索权威法规知识库并筛选适用条款(关键词: %s)...", searchQuery), "regulatory", 0, 0, 10)
|
||||
}
|
||||
if kbHits, err := s.kb.Search(ctx, searchQuery, kb.SearchOptions{Limit: 5}); err == nil && len(kbHits) > 0 {
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("knowledge_retrieved", fmt.Sprintf("法规知识库初筛完成,精准命中 %d 条国家与行业标准条款", len(kbHits)), "regulatory", 0, 0, 15)
|
||||
var hitLines []string
|
||||
for _, h := range kbHits {
|
||||
hitLines = append(hitLines, fmt.Sprintf("- [%s] %s (%s)", h.DocCode, h.Title, h.CiteKey))
|
||||
}
|
||||
streamer.EmitDetail("knowledge_hits", "knowledge", "regulatory", "命中法规标准清单", strings.Join(hitLines, "\n"), true)
|
||||
}
|
||||
for _, kh := range kbHits {
|
||||
hits = append(hits, squad.KnowledgeHit{
|
||||
DocCode: kh.DocCode,
|
||||
@@ -183,6 +196,11 @@ func (s *Service) executeStageInternal(ctx context.Context, req StageRunRequest)
|
||||
targetMeta = &SupportedStages[0]
|
||||
}
|
||||
|
||||
streamer := stream.FromContext(ctx)
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("stage_started", fmt.Sprintf("开始推进商务成熟度阶段【%s】(包含 %d 项任务序列)...", req.Stage, len(targetMeta.Tasks)), "coordinator", 0, len(targetMeta.Tasks), 5)
|
||||
}
|
||||
|
||||
// 3. 汇总当前阶段累积的事实与前序文档
|
||||
currentFields := make(map[string]string)
|
||||
if req.Fields != nil {
|
||||
@@ -310,6 +328,14 @@ func (s *Service) executeStageInternal(ctx context.Context, req StageRunRequest)
|
||||
}
|
||||
}
|
||||
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("stage_completed", fmt.Sprintf("商务成熟度阶段【%s】全部任务协同推演完成!", req.Stage), "coordinator", len(targetMeta.Tasks), len(targetMeta.Tasks), 100)
|
||||
if stageBrief != nil {
|
||||
streamer.EmitSummary(targetMeta.Title+"完成", stageBrief.MaturityHeadline, stageBrief.MaturityScore, stageBrief.MaturityLevel)
|
||||
streamer.EmitBriefing(stageBrief)
|
||||
}
|
||||
}
|
||||
|
||||
return &StageRunResult{
|
||||
Stage: req.Stage,
|
||||
StageTitle: targetMeta.Title,
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/intent"
|
||||
"chat-agent-system/internal/squad"
|
||||
"chat-agent-system/internal/stream"
|
||||
)
|
||||
|
||||
// Message 历史消息结构
|
||||
@@ -64,8 +65,24 @@ func NewSession(id string, userCtx *coordinator.UserProjectContext, rec intent.R
|
||||
}
|
||||
}
|
||||
|
||||
// SendMessage 处理一轮用户输入
|
||||
// SendMessage 处理一轮用户输入 (标准阻塞模式)
|
||||
func (s *Session) SendMessage(ctx context.Context, userInput string) (*ChatResponse, error) {
|
||||
streamer := stream.FromContext(ctx)
|
||||
return s.sendMessageInternal(ctx, userInput, streamer)
|
||||
}
|
||||
|
||||
// SendMessageStream 处理一轮用户输入并实时流式推送事件 (支持 SSE / 终端流式监听)
|
||||
func (s *Session) SendMessageStream(ctx context.Context, userInput string, listener stream.Listener, includeDetails bool) (*ChatResponse, error) {
|
||||
ctxWithS, streamer := stream.WithListener(ctx, includeDetails, listener)
|
||||
return s.sendMessageInternal(ctxWithS, userInput, streamer)
|
||||
}
|
||||
|
||||
func (s *Session) sendMessageInternal(ctx context.Context, userInput string, streamer *stream.Streamer) (*ChatResponse, error) {
|
||||
start := time.Now()
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("intent_recognizing", "正在研判您的意图与项目上下文...", "coordinator", 0, 0, 0)
|
||||
}
|
||||
|
||||
userMsg := Message{
|
||||
ID: fmt.Sprintf("msg_%d_u", time.Now().UnixNano()),
|
||||
Role: "user",
|
||||
@@ -81,12 +98,28 @@ func (s *Session) SendMessage(ctx context.Context, userInput string) (*ChatRespo
|
||||
}
|
||||
recRes, err := s.Recognizer.Recognize(ctx, userInput, snap)
|
||||
if err != nil {
|
||||
if streamer != nil {
|
||||
streamer.EmitError(err)
|
||||
}
|
||||
return nil, fmt.Errorf("intent recognition failed: %w", err)
|
||||
}
|
||||
|
||||
if streamer != nil && recRes.TopIntent != nil {
|
||||
streamer.EmitStatus(
|
||||
string(recRes.TopIntent.Intent),
|
||||
string(recRes.Branch),
|
||||
string(s.UserCtx.Project.CurrentStage),
|
||||
fmt.Sprintf("识别分支: %s (置信度: %.0f%%)", recRes.Branch, recRes.TopIntent.Confidence*100),
|
||||
recRes.TopIntent.FilledSlots,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. 协调调度与校验 (L2 协调层)
|
||||
coordRes, err := s.Coordinator.ProcessIntent(ctx, recRes, s.UserCtx)
|
||||
if err != nil {
|
||||
if streamer != nil {
|
||||
streamer.EmitError(err)
|
||||
}
|
||||
return nil, fmt.Errorf("coordination execution failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -115,6 +148,17 @@ func (s *Session) SendMessage(ctx context.Context, userInput string) (*ChatRespo
|
||||
}
|
||||
s.Messages = append(s.Messages, asstMsg)
|
||||
|
||||
// 4. 流式推送参谋自然语言回复与决策锦囊
|
||||
if streamer != nil {
|
||||
if coordRes.NaturalReply != "" {
|
||||
streamer.EmitDelta(coordRes.NaturalReply)
|
||||
}
|
||||
if coordRes.AdvisorBrief != nil {
|
||||
streamer.EmitBriefing(coordRes.AdvisorBrief)
|
||||
}
|
||||
streamer.EmitDone(s.ID, time.Since(start))
|
||||
}
|
||||
|
||||
return &ChatResponse{
|
||||
ReplyText: coordRes.NaturalReply,
|
||||
Branch: recRes.Branch,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/bizdev"
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/intent"
|
||||
"chat-agent-system/internal/stream"
|
||||
)
|
||||
|
||||
func TestChatSession_SendMessageStream(t *testing.T) {
|
||||
u := coordinator.UserInfo{ID: "u1", Name: "李工", Role: "engineer", ProjectRole: "composer"}
|
||||
userCtx := &coordinator.UserProjectContext{
|
||||
User: u,
|
||||
Project: coordinator.ProjectInfo{
|
||||
Name: "内窥镜研发项目",
|
||||
CurrentStage: bizdev.StageV01_Triage,
|
||||
},
|
||||
BlackboardFacts: make(map[string]string),
|
||||
}
|
||||
|
||||
rec := intent.NewRecognizer()
|
||||
engine := coordinator.NewCoordinatorEngine(nil)
|
||||
session := NewSession("test_stream_sess", userCtx, rec, engine)
|
||||
|
||||
var events []stream.StreamEvent
|
||||
listener := func(ev stream.StreamEvent) {
|
||||
events = append(events, ev)
|
||||
}
|
||||
|
||||
// 1. 发送查询进度消息
|
||||
resp, err := session.SendMessageStream(context.Background(), "查一下当前进展", listener, false)
|
||||
if err != nil {
|
||||
t.Fatalf("SendMessageStream failed: %v", err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatalf("expected non-nil response")
|
||||
}
|
||||
|
||||
if len(events) == 0 {
|
||||
t.Fatalf("expected streamed events, got 0")
|
||||
}
|
||||
|
||||
// 验证事件流序列
|
||||
hasPhase := false
|
||||
hasStatus := false
|
||||
hasDelta := false
|
||||
hasDone := false
|
||||
|
||||
for _, ev := range events {
|
||||
switch ev.Type {
|
||||
case stream.EventPhase:
|
||||
hasPhase = true
|
||||
case stream.EventStatus:
|
||||
hasStatus = true
|
||||
case stream.EventDelta:
|
||||
hasDelta = true
|
||||
case stream.EventDone:
|
||||
hasDone = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPhase {
|
||||
t.Errorf("expected at least one phase event")
|
||||
}
|
||||
if !hasStatus {
|
||||
t.Errorf("expected at least one status event")
|
||||
}
|
||||
if !hasDelta {
|
||||
t.Errorf("expected at least one delta event")
|
||||
}
|
||||
if !hasDone {
|
||||
t.Errorf("expected done event")
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"chat-agent-system/internal/bizdev"
|
||||
"chat-agent-system/internal/intent"
|
||||
"chat-agent-system/internal/squad"
|
||||
"chat-agent-system/internal/stream"
|
||||
)
|
||||
|
||||
// CoordinationStatus 协调裁决状态
|
||||
@@ -71,6 +72,11 @@ func (e *CoordinatorEngine) ProcessIntent(
|
||||
}
|
||||
}
|
||||
|
||||
streamer := stream.FromContext(ctx)
|
||||
if streamer != nil {
|
||||
streamer.EmitStatus(string(top.Intent), string(recRes.Branch), string(userCtx.Project.CurrentStage), fmt.Sprintf("意图判定: %s (置信度: %.0f%%)", top.Intent, top.Confidence*100), top.FilledSlots)
|
||||
}
|
||||
|
||||
// 1. 若为分支 3 (模糊/分忧引导),触发参谋主动分析与建议
|
||||
if recRes.Branch == intent.BranchAmbiguousFallback {
|
||||
return e.handleAmbiguousConsult(top, userCtx)
|
||||
@@ -292,6 +298,11 @@ func (e *CoordinatorEngine) handleSupplementInfo(cand *intent.CandidateIntent, u
|
||||
summaryKey = "已补充新的背景信息"
|
||||
}
|
||||
|
||||
streamer := stream.FromContext(context.Background())
|
||||
if streamer != nil {
|
||||
streamer.EmitStatus(string(cand.Intent), string(cand.Branch), string(userCtx.Project.CurrentStage), fmt.Sprintf("已吸收最新事实设定:%s", summaryKey), cand.FilledSlots)
|
||||
}
|
||||
|
||||
// 持久化到 blackboard.json
|
||||
if userCtx.WorkDir != "" {
|
||||
bbPath := filepath.Join(userCtx.WorkDir, "blackboard.json")
|
||||
@@ -348,6 +359,11 @@ func (e *CoordinatorEngine) handleAdvanceStage(ctx context.Context, cand *intent
|
||||
targetStage = bizdev.StageV05_Feasibility
|
||||
}
|
||||
|
||||
streamer := stream.FromContext(ctx)
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("stage_transition", fmt.Sprintf("准备推进项目成熟度阶段至【%s】...", targetStage), "coordinator", 0, 0, 0)
|
||||
}
|
||||
|
||||
// 1. 若携带补丁槽位 (例如 "好,但目标市场改成欧洲CE"),先行持久化更新
|
||||
patchSummary := ""
|
||||
if len(cand.PatchSlots) > 0 {
|
||||
|
||||
@@ -270,3 +270,28 @@ func (r *RoleRegistry) SpawnAdHocRole(req AdHocRoleRequest) (AgentSpec, error) {
|
||||
r.roles[adHocSpec.Role] = adHocSpec
|
||||
return adHocSpec, nil
|
||||
}
|
||||
|
||||
// FriendlyRoleName 将内部角色代号转换为友好的中文专员称谓
|
||||
func FriendlyRoleName(role string) string {
|
||||
switch role {
|
||||
case "organizer":
|
||||
return "材料整理专员"
|
||||
case "searcher":
|
||||
return "资料检索专员"
|
||||
case "regulatory":
|
||||
return "法规专员"
|
||||
case "engineer":
|
||||
return "系统工程师"
|
||||
case "market":
|
||||
return "市场情报专员"
|
||||
case "finance":
|
||||
return "商务报价专员"
|
||||
case "writer":
|
||||
return "撰写专员"
|
||||
case "leader":
|
||||
return "审核主管"
|
||||
default:
|
||||
return role + "专员"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"chat-agent-system/internal/agent"
|
||||
"chat-agent-system/internal/config"
|
||||
"chat-agent-system/internal/quote"
|
||||
"chat-agent-system/internal/stream"
|
||||
)
|
||||
|
||||
// TaskMeta 任务元信息
|
||||
@@ -210,6 +211,11 @@ func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadR
|
||||
}
|
||||
|
||||
// 3. 执行动态工作流调度
|
||||
streamer := stream.FromContext(ctx)
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("squad_assembly", fmt.Sprintf("正在组建并启动【%s】智能体小队(后端: %s)...", TaskArtifactTitle(req.TaskKey), backendName), "leader", 0, 0, 5)
|
||||
}
|
||||
|
||||
backend := agent.NewBackend(backendName, s.cfg)
|
||||
timeout := req.Timeout
|
||||
if timeout <= 0 {
|
||||
@@ -222,6 +228,9 @@ func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadR
|
||||
|
||||
execRes, err := s.driver.Execute(ctx, workDir, backend, execOpts)
|
||||
if err != nil {
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("task_failed", fmt.Sprintf("任务执行异常: %v", err), "leader", 0, 0, 0)
|
||||
}
|
||||
return &SquadRunResult{
|
||||
TaskKey: req.TaskKey,
|
||||
ArtifactTitle: TaskArtifactTitle(req.TaskKey),
|
||||
@@ -234,6 +243,9 @@ func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadR
|
||||
}
|
||||
|
||||
// 4. 自动化质量门禁与修复 (QualityGate)
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("quality_gate", "审核主管正在核验交付物规范并执行质量门禁把关...", "leader", 0, 0, 85)
|
||||
}
|
||||
maxRetries := 0
|
||||
if req.AutoRepair {
|
||||
maxRetries = 2
|
||||
@@ -242,6 +254,11 @@ func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadR
|
||||
if err != nil {
|
||||
// 记录门禁错误但容错返回当前正文
|
||||
report = &QualityReport{Passed: false, Violations: []string{err.Error()}}
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("quality_gate", fmt.Sprintf("质量门禁审查告警: %v", err), "leader", 0, 0, 90)
|
||||
}
|
||||
} else if streamer != nil {
|
||||
streamer.EmitPhase("quality_gate", fmt.Sprintf("质量门禁审核通过(评分: %.1f / 100)", report.Score), "leader", 0, 0, 95)
|
||||
}
|
||||
|
||||
manifest, _ := s.gate.GenerateManifest(workDir, req.TaskKey, *report)
|
||||
@@ -286,6 +303,19 @@ func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadR
|
||||
}
|
||||
}
|
||||
|
||||
if streamer != nil {
|
||||
score := 0.0
|
||||
if report != nil {
|
||||
score = report.Score
|
||||
}
|
||||
streamer.EmitDeliverable(req.TaskKey, TaskArtifactTitle(req.TaskKey), finalOutPath, len(strings.Fields(string(finalBytes))), score)
|
||||
if advisorBrief != nil {
|
||||
streamer.EmitPhase("advisor_briefing", "贴身业务参谋正在提炼关键决策摘要与风险雷达...", "coordinator", 0, 0, 98)
|
||||
streamer.EmitSummary(TaskArtifactTitle(req.TaskKey)+"已生成", advisorBrief.MaturityHeadline, advisorBrief.MaturityScore, advisorBrief.MaturityLevel)
|
||||
streamer.EmitBriefing(advisorBrief)
|
||||
}
|
||||
}
|
||||
|
||||
return &SquadRunResult{
|
||||
TaskKey: req.TaskKey,
|
||||
ArtifactTitle: TaskArtifactTitle(req.TaskKey),
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/agent"
|
||||
"chat-agent-system/internal/stream"
|
||||
)
|
||||
|
||||
// StepEvent 表示小队单步执行生命周期的事件
|
||||
@@ -275,7 +276,7 @@ func (d *WorkflowDriver) Execute(ctx context.Context, wsDir string, backend agen
|
||||
_ = d.SaveWorkflow(wsDir, wf)
|
||||
|
||||
if step.AllowDynamicAdjustment && d.evaluator != nil {
|
||||
d.applyDynamicAdjustment(wsDir, wf, step, &mu, opts.StepNotify)
|
||||
d.applyDynamicAdjustment(ctx, wsDir, wf, step, &mu, opts.StepNotify)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,6 +347,11 @@ func (d *WorkflowDriver) runOneStep(
|
||||
Status: "started",
|
||||
})
|
||||
}
|
||||
streamer := stream.FromContext(ctx)
|
||||
roleTitle := FriendlyRoleName(step.Role)
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("step_executing", fmt.Sprintf("%s正在进行:%s...", roleTitle, step.Title), step.Role, 0, 0, 0)
|
||||
}
|
||||
stepStart := time.Now()
|
||||
|
||||
// 若当前步骤为 leader 审核定稿,且依赖前序专员稿件,先预置 baseline 草稿,供审查与润色
|
||||
@@ -380,6 +386,9 @@ func (d *WorkflowDriver) runOneStep(
|
||||
duration := time.Since(stepStart).Milliseconds()
|
||||
|
||||
if err != nil {
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("step_failed", fmt.Sprintf("%s执行异常:%s", roleTitle, err.Error()), step.Role, 0, 0, 0)
|
||||
}
|
||||
if notify != nil {
|
||||
notify(StepEvent{
|
||||
StepID: step.ID,
|
||||
@@ -396,6 +405,9 @@ func (d *WorkflowDriver) runOneStep(
|
||||
|
||||
if res != nil && !res.Success {
|
||||
errMsg := res.ErrorReason
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("step_failed", fmt.Sprintf("%s执行未成功:%s", roleTitle, errMsg), step.Role, 0, 0, 0)
|
||||
}
|
||||
if notify != nil {
|
||||
notify(StepEvent{
|
||||
StepID: step.ID,
|
||||
@@ -449,6 +461,13 @@ func (d *WorkflowDriver) runOneStep(
|
||||
*producedFiles = append(*producedFiles, filepath.ToSlash(filepath.Join(step.SubWorkspace, deliverableName)))
|
||||
mu.Unlock()
|
||||
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("step_completed", fmt.Sprintf("%s完成任务:%s (耗时 %dms)", roleTitle, step.Title, duration), step.Role, 0, 0, 0)
|
||||
if stepData, err := os.ReadFile(targetPath); err == nil && len(stepData) > 0 {
|
||||
streamer.EmitDetail("step_output", step.ID, step.Role, fmt.Sprintf("%s阶段性产出: %s", roleTitle, step.Title), string(stepData), true)
|
||||
}
|
||||
}
|
||||
|
||||
if notify != nil {
|
||||
notify(StepEvent{
|
||||
StepID: step.ID,
|
||||
@@ -464,6 +483,7 @@ func (d *WorkflowDriver) runOneStep(
|
||||
}
|
||||
|
||||
func (d *WorkflowDriver) applyDynamicAdjustment(
|
||||
ctx context.Context,
|
||||
wsDir string,
|
||||
wf *WorkflowPlan,
|
||||
step StepSpec,
|
||||
@@ -571,6 +591,11 @@ func (d *WorkflowDriver) applyDynamicAdjustment(
|
||||
newSteps = append(newSteps, addStep)
|
||||
newSteps = append(newSteps, wf.Steps[insertIdx:]...)
|
||||
wf.Steps = newSteps
|
||||
|
||||
streamer := stream.FromContext(ctx)
|
||||
if streamer != nil {
|
||||
streamer.EmitPhase("dynamic_adjustment", fmt.Sprintf("单步评估发现缺口,动态追加任务步骤:%s (%s)", addStep.Title, FriendlyRoleName(addStep.Role)), addStep.Role, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
_ = d.SaveWorkflow(wsDir, wf)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/squad"
|
||||
)
|
||||
|
||||
type contextKey struct{}
|
||||
@@ -125,7 +123,7 @@ func (s *Streamer) EmitSummary(title, content string, maturityScore int, maturit
|
||||
}
|
||||
|
||||
// EmitBriefing 发送 B 轨业务参谋决策锦囊
|
||||
func (s *Streamer) EmitBriefing(brief *squad.AdvisorBriefing) {
|
||||
func (s *Streamer) EmitBriefing(brief any) {
|
||||
if s == nil || brief == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package stream
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/squad"
|
||||
)
|
||||
|
||||
// EventType 定义流式事件类型
|
||||
@@ -83,7 +81,7 @@ type StreamEvent struct {
|
||||
Phase *PhasePayload `json:"phase,omitempty"`
|
||||
Status *StatusPayload `json:"status,omitempty"`
|
||||
Summary *SummaryPayload `json:"summary,omitempty"`
|
||||
Briefing *squad.AdvisorBriefing `json:"briefing,omitempty"`
|
||||
Briefing any `json:"briefing,omitempty"`
|
||||
Detail *DetailPayload `json:"detail,omitempty"`
|
||||
Delta string `json:"delta,omitempty"`
|
||||
Deliverable *DeliverablePayload `json:"deliverable,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user