-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathBlogWorkflow.cs
More file actions
120 lines (101 loc) · 4.79 KB
/
Copy pathBlogWorkflow.cs
File metadata and controls
120 lines (101 loc) · 4.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
using System.Diagnostics;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.Logging;
namespace BlogWriter;
/// <summary>
/// Orchestrates the blog-writing pipeline with workflow stages for planning,
/// research, drafting, and review.
///
/// Execution flows Blogger → Researcher → Author → Reviewer, with a bounded
/// reviewer-to-author revision loop controlled by
/// <see cref="ResearchState.MaxRevisions"/>.
/// </summary>
public class BlogWorkflow(
IBloggerAgent blogger,
IResearcherAgent researcher,
IAuthorAgent author,
IReviewerAgent reviewer,
ILogger<BlogWorkflow> logger) : IBlogWorkflow
{
// Emits the root span for a workflow run. Activated by the ActivityListener
// registered in Program.cs (or an OpenTelemetry TracerProvider).
private static readonly ActivitySource s_activitySource = new("BlogWriter.Workflow");
public async Task<ResearchState> RunAsync(ResearchState state, CancellationToken cancellationToken = default)
{
using Activity? activity = s_activitySource.StartActivity("Workflow.Run");
activity?.SetTag("blog.topic", state.MainTask);
var bloggerExecutor = new BloggerExecutor(blogger);
var researcherExecutor = new ResearcherExecutor(researcher);
var authorExecutor = new AuthorExecutor(author);
var reviewerExecutor = new ReviewerExecutor(reviewer);
Workflow workflow = new WorkflowBuilder(bloggerExecutor)
.AddEdge(bloggerExecutor, researcherExecutor)
.AddEdge(researcherExecutor, authorExecutor)
.AddEdge(authorExecutor, reviewerExecutor)
// Bounded revision loop: route back to the author only while the draft
// still needs work and the revision cap has not been reached. When the
// condition is false the reviewer instead yields the final output.
.AddEdge<ResearchState>(reviewerExecutor, authorExecutor, condition: s => s?.NeedsRevision == true)
.WithOutputFrom(reviewerExecutor)
.Build();
// Stream execution instead of running to completion in one shot. The
// topology is identical to before (proven terminating, MAF-Doctor grade A);
// streaming simply surfaces each executor's lifecycle as it happens, giving
// live progress. The final ResearchState is captured from the
// WorkflowOutputEvent emitted by the reviewer.
StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, state, cancellationToken: cancellationToken);
ResearchState? result = null;
await foreach (WorkflowEvent evt in run.WatchStreamAsync().WithCancellation(cancellationToken))
{
switch (evt)
{
case ExecutorInvokedEvent invoked:
logger.LogInformation("[workflow] -> {ExecutorId} started", invoked.ExecutorId);
break;
case ExecutorCompletedEvent completed:
logger.LogInformation("[workflow] {ExecutorId} completed", completed.ExecutorId);
break;
case ExecutorFailedEvent failed:
logger.LogError(failed.Data as Exception, "[workflow] {ExecutorId} failed", failed.ExecutorId);
// A token-cap breach must abort the whole run, not just the
// node. Re-throw it so it unwinds to the application entry point.
if (failed.Data is Exception ex && FindTokenCap(ex) is { } capEx)
{
throw capEx;
}
break;
case WorkflowOutputEvent { Data: ResearchState finalState }:
// The reviewer yielded the final, approved (or revision-capped) state.
result = finalState;
break;
}
}
// Fall back to the input state only if no output event was ever produced.
return result ?? state;
}
// Walks the exception chain (including AggregateException children) looking
// for a token-cap breach, which the workflow runtime may have wrapped.
private static TokenCapExceededException? FindTokenCap(Exception? exception)
{
while (exception is not null)
{
if (exception is TokenCapExceededException capEx)
{
return capEx;
}
if (exception is AggregateException aggregate)
{
foreach (Exception inner in aggregate.InnerExceptions)
{
if (FindTokenCap(inner) is { } found)
{
return found;
}
}
return null;
}
exception = exception.InnerException;
}
return null;
}
}