Skip to content

Fuseraft CLI — Design Document

This document describes the architecture and design decisions behind fuseraft-cli. It is meant to be a living reference for contributors and for future conversations with AI assistants working in this codebase.


Table of Contents

  1. What It Is
  2. Layer Map
  3. Configuration
  4. Agent Construction
  5. Orchestrators
  6. Selection Strategies
  7. Termination Strategies
  8. Routing Validators
  9. Session and Checkpoint Layer
  10. Conversation Compaction
  11. Plugin System
  12. Governance
  13. Change Tracking
  14. Event Emission
  15. DevUI
  16. Microsoft Agent Framework Usage
  17. Decisions Against Framework Features

1. What It Is

Fuseraft-cli is a production-grade multi-agent orchestration CLI built on the Microsoft Agent Framework (MAF). It drives teams of LLM agents through configurable workflows — software development pipelines, research tasks, general automation — with built-in governance, budget control, session persistence, and human-in-the-loop support.

A session is started with a natural-language task. The CLI selects which agents speak, validates routing decisions against deterministic rules, persists the conversation to disk after every turn, and streams output to the terminal and an optional browser-based DevUI.


2. Layer Map

Cli/
  Commands/         — Spectre.Console CLI entry points (run, sessions, init, ...)
  DevUI/            — Lightweight SSE server for browser-based session visualization
  OrchestratorBuilder.cs  — Wires up the full object graph from a config file (no DI host)
  SessionRunner.cs  — Drives the streaming loop; handles HITL, compaction, checkpointing

Core/
  Interfaces/       — IOrchestrator, ISessionStore, IAgentSelector, ITerminationCondition,
                      IRoutingValidator, IHumanApprovalService
  Models/           — OrchestrationConfig, AgentConfig, SessionCheckpoint, AgentMessage,
                      TokenUsage, StrategyConfig, ValidationConfig, ...
  Exceptions/       — BudgetExceededException, ValidatorStuckException

Infrastructure/
  AgentFactory.cs         — Builds MAF AIAgent instances from AgentConfig
  ChatClientFactory.cs    — Resolves model aliases; constructs IChatClient per provider
  InMemorySessionStore.cs — In-process checkpoint store (no persistence)
  JsonSessionStore.cs     — File-backed checkpoint store (~/.fuseraft/sessions/)
  McpSessionManager.cs    — Connects to MCP servers; registers their tools at startup
  Plugins/                — Built-in tool plugins (FileSystem, Shell, Git, Http, ...)

Orchestration/
  AgentOrchestrator.cs    — General-purpose multi-agent loop (any selection strategy)
  MagenticOrchestrator.cs — Magentic-One style two-level manager/participant loop
  ConversationCompactor.cs — LLM-based history summarization for long sessions
  ChangeTracker.cs        — Intercepts tool calls to record file/shell/git activity
  EventEmitter.cs         — Appends structured JSONL events to a log file
  Strategies/             — Selection and termination strategy implementations
  Validation/             — Routing validator implementations
  Workflow/               — MAF WorkflowBuilder-based phase orchestrator

3. Configuration

Every session is driven by a single JSON or YAML file under the top-level Orchestration key. The file is loaded by OrchestratorBuilder.BuildAsync and bound to OrchestrationConfig.

Top-level fields:

Field Purpose
Name Human-readable session name
Models Named model aliases reusable across agents (avoids repeating endpoint/key/temp)
Agents Ordered list of AgentConfig — name, instructions, model, plugins, trust score
Selection Controls which agent speaks next (SelectionStrategyConfig)
Termination Controls when the session ends (TerminationStrategyConfig)
Security Filesystem sandbox path, HTTP allowlist, injection detection
MaxCostUsd Hard spend cap for the session
McpServers MCP server definitions; tools registered at startup alongside built-ins
Compaction LLM-based history summarization settings (trigger count, keep-recent count, model)
Validation Routing validator settings (brief path, test report path, change log path)
ChangeTracking Writes file/shell/git activity to a JSONL change log
Scratchpad Per-agent persistent key-value store base path
Chatroom Shared append-only JSONL coordination log for agents
Events Structured JSONL event log for turn_end, validation_fail, hitl_escalation
Checkpoint Storage backend: json (default, ~/.fuseraft/sessions/) or memory
Telemetry OTLP endpoint for OpenTelemetry traces and metrics
ApiProfiles Named HTTP profiles (base URL + default headers with ${ENV_VAR} expansion)

AgentConfig fields:

Field Purpose
Name Unique identifier within the session
Instructions System-prompt defining role and routing keywords
Model Model alias or inline ModelConfig (endpoint, API key env var, temperature, max tokens)
Plugins List of plugin names to load as tools
FunctionChoice auto / required / none — maps to tool_choice in the API
TrustScore 0.0–1.0 — governs execution ring assignment and privilege level
ContextWindow Optional per-agent history filter (strips tool noise, limits tail length)

Environment variable expansion for Security.HttpAllowedHosts and all ApiProfiles header values is performed at startup via ${ENV_VAR} tokens. Credentials never appear in agent instructions or conversation history.

Config formats: Both JSON (.json) and YAML (.yaml / .yml) are supported. YAML is parsed via YamlConfigLoader and converted to IConfiguration for the same BindConfig path.


4. Agent Construction

AgentFactory.Create(AgentConfig) produces a fully configured AIAgent ready for use by any orchestrator.

Steps:

  1. Identity — A AgentIdentity (DID: did:fuseraft:<name>) is created and registered with the IdentityRegistry. The governance audit log uses the DID as the actor identifier.

  2. Model resolutionChatClientFactory.Resolve(config.Model) expands model aliases from the Models registry, merging per-agent temperature/max-tokens overrides over the alias baseline. ChatClientFactory.Create(resolvedModel) constructs the IChatClient for the provider (OpenAI, Azure OpenAI, Anthropic, Ollama).

  3. Tool construction — Plugins listed in AgentConfig.Plugins are resolved from PluginRegistry. Scratchpad and Chatroom are per-agent instances built inline; all others are looked up from the registry.

  4. ChatOptions — Temperature, max tokens, and tool_choice are bundled into a ChatOptions. A middleware wrapper merges these defaults into every GetResponseAsync / GetStreamingResponseAsync call. ToolMode.RequireAny is suppressed after the first tool call in a turn (once a tool-result message appears in context) to match OpenAI's behavior and avoid HTTP 400s from providers that reject tool_choice: required mid-tool-loop.

  5. Middleware chain (outermost to innermost):

  6. ChangeTracker.WrapAgent — intercepts every tool call to record it in the change log. Applied first so it observes the final result of all inner middleware, including sandbox denials.
  7. SandboxEnforcementFilter — enforces the filesystem sandbox path and routes calls through the governance injection detector. Applied second so the sandbox decision is visible in the change log.
  8. ChatClientAgent (base) — the MAF ChatClientAgent with tools and instructions.

Tool merge invariant: The MergeOptions helper always ensures ChatOptions.Tools is populated from the agent's own list when the inner FunctionInvokingChatClient does not set it, preventing tool_choice being sent without a tools array (which Bedrock/LiteLLM rejects).


5. Orchestrators

OrchestratorBuilder selects among three orchestrators based on the config's Selection.Type and agent names. The selection order is:

  1. MagenticOrchestrator — when Selection.Type == "magentic"
  2. WorkflowOrchestrator — when Selection.Type == "keyword" AND agents are exactly {planner, developer, tester, reviewer}
  3. AgentOrchestrator — all other cases

All three implement IOrchestrator:

Task<OrchestrationResult> RunAsync(string task, IReadOnlyList<AgentMessage>? priorHistory, CancellationToken ct)
IAsyncEnumerable<AgentMessage> StreamAsync(string task, IReadOnlyList<AgentMessage>? priorHistory, CancellationToken ct)
event Action<string>? AgentStarting
void SetSessionId(string sessionId)
void SetResumeExecutorId(string? executorId)

5.1 AgentOrchestrator

The general-purpose path. Drives any selection strategy through a single while(true) loop:

  1. Call IAgentSelector.SelectAsync(agents, history) → get next agent (null = session ends)
  2. Apply the agent's ContextWindow filter to trim the history slice passed to the LLM
  3. Prepend the agent's system instruction (MAF's ChatClientAgent.RunAsync does not inject instructions automatically when session = null)
  4. Call agent.RunAsync(context, null, null, ct) via the governance circuit breaker
  5. Append all response messages (including tool calls/results) to shared history with AuthorName set
  6. Yield the final text response as an AgentMessage
  7. Check ITerminationCondition.ShouldTerminateAsync(history) — break if true
  8. Check MaxIterations hard cap

Why instructions are injected manually: When calling RunAsync without a session, MAF does not prepend the agent's Instructions as a system message. Agents must see their role definition and routing keywords on every turn, so we prepend it explicitly.

Shared history: All agents read from and write to the same List<ChatMessage>. This is intentional — routing strategies (especially KeywordSelectionStrategy) read AuthorName from the most recent assistant message to determine who just spoke and where they want to route.

5.2 WorkflowOrchestrator

A phase-based orchestrator built on MAF's WorkflowBuilder. Used only for the classic Planner → Developer → Tester → Reviewer pipeline.

Phase loop:

Each phase is a fresh linear MAF workflow built from a slice of the four-agent chain starting at currentStart. InProcessExecution.Default.RunStreamingAsync drives the workflow; WatchStreamAsync consumes events. A WorkflowOutputEvent signals phase-break (an agent called YieldOutputAsync). The outer while(phaseCount < maxPhases) loop inspects AgentContext.LastKeyword to determine the next phase's starting executor.

AgentContext is a shared mutable object threaded through all executors via MAF's message passing: - History — shared List<ChatMessage> for the session - MessageSink — unbounded ChannelWriter<AgentMessage> that decouples executor threads from the caller's async enumerable - LastKeyword — the phase-break keyword set by the last executor before YieldOutputAsync - CumulativeCost — session-lifetime cost accumulator - TurnIndex — monotonic turn counter

MAF graph topology: Strictly linear — AddEdge(src, sink) only. WithOutputFrom(tester, reviewer) restricts phase-break output to the two terminal agents. No fan-in, fan-out, conditional edges, or switches are used (see §16).

Routing inside executors: Each FunctionExecutor runs a while(true) loop that calls agent.RunAsync, scans the response for routing keywords via strict per-line matching, and either: - Calls SendMessageAsync(ctx, routeTarget) for in-phase send-forward (HANDOFF TO X) - Calls YieldOutputAsync(ctx) for phase-break (BUGS FOUND, APPROVED, etc.) - Injects a correction message and retries when no keyword is detected

Why routing lives inside executors, not on graph edges: MAF edge conditions (AddEdge(src, dst, condition: msg => ...)) fire once per message — they have no retry loop. When an agent fails to emit a routing keyword, we must inject a correction and call the LLM again in the same turn. This retry semantic requires the routing logic to live inside the executor.

ResumeExecutorId: After compaction or a mid-phase process restart, SetResumeExecutorId provides a one-time hint telling the orchestrator which executor was active when the session paused. It is consumed on the first StreamAsync call and cleared.

5.3 MagenticOrchestrator

A Magentic-One style two-level orchestrator. A dedicated manager LLM drives a planning and evaluation loop; participant agents execute tasks on the manager's instructions.

Two-history model (the core invariant): - sharedHistory — what participant agents see: user task + all participant responses - managerHistory — what the manager sees: fact-gather prompt/response, plan, and JSON ledger evaluations only. The manager never sees participant messages directly.

Phase structure:

  1. Fact gathering — Manager summarizes what it knows about the task and available agents
  2. Planning — Manager produces a step-by-step plan. Optional HITL review via IHumanApprovalService.PromptPlanReviewAsync (feedback loop with revision until approved)
  3. Inner loop — For each round:
  4. Manager evaluates a JSON progress ledger (MagenticProgressLedger) against the current plan and shared history
  5. If IsRequestSatisfied: synthesize final answer → done
  6. If stalled (!IsProgressBeingMade || IsInLoop): increment stall counter
  7. Manager selects next participant and generates a targeted instruction
  8. Selected participant executes against shared history + instruction
  9. Stall counter ≥ MaxStallCount → replan (resets counters); too many replans → terminate

Why MagenticOrchestrator is not built on GroupChatWorkflowBuilder: The framework's GroupChatWorkflowBuilder passes the same shared history to both the manager (SelectNextAgentAsync) and participants. Our manager must never see participant messages directly — it reasons from a private ledger. There is also no equivalent to our planning/fact-gathering phases, stall detection, or HITL plan review loop in the framework abstraction. Mapping our design onto GroupChatManager would require abusing UpdateHistoryAsync to fabricate the manager's context, which would be misleading and fragile. The two-history model is the core architectural invariant that makes this Magentic-style.

Checkpoint state (MagenticCheckpointState): CurrentPlan, RoundIndex, StallCount, ResetCount, AwaitingPlanReview — enough to resume the inner loop exactly where it paused. Exposed via CurrentState so SessionRunner can snapshot it after each yielded message.


6. Selection Strategies

Built and returned by StrategyFactory.CreateSelection. All implement IAgentSelector.

Type Behavior
sequential / roundrobin Cycles through agents in order
llm Calls an IChatClient with a configurable prompt template to pick the next agent by name
keyword Scans the last assistant message for configured keywords; each keyword routes to a named agent. Optional validators gate the route before it fires.
structured Evaluates CEL-like condition expressions per route rather than string keywords
magentic Handled entirely by MagenticOrchestrator; StrategyFactory throws if this type reaches it

KeywordSelectionStrategy is the workhorse for WorkflowOrchestrator and structured pipelines. Key behaviors: - Keyword matching is strict per-line (not substring): the full trimmed line must equal the keyword - Source agent filtering: a route can be restricted to fire only when a specific agent authored the last message - Routing validators run synchronously before the route fires; failure injects a correction message and re-invokes the current agent (up to MaxRetries before ValidatorStuckException) - Governance policy violations are emitted to the event log with consecutive-failure counts - RequireHumanApproval: true on a route escalates to IHumanApprovalService.PromptApprovalAsync before routing

StructuredSelectionStrategy evaluates condition strings (e.g. "last_agent == 'Tester' && contains(last_message, 'PASS')") via StructuredConditionEvaluator. Used for configs that need multi-variable routing logic without keyword string matching.


7. Termination Strategies

Built and returned by StrategyFactory.CreateTermination. All implement ITerminationCondition.

Type Behavior
regex Terminates when a regex matches the last assistant message (optional agent-name filter)
maxiterations Never terminates via condition — relies on MaxIterations hard cap in AgentOrchestrator
composite AND of child conditions — all must return true simultaneously

Termination strategies can be decorated with routing validators via the Validators field. A ValidatedTerminationStrategy runs the validators before accepting the termination signal. The requireCurrentTurn: true flag prevents a stale change-log entry from satisfying a validator that was satisfied in an earlier turn.


8. Routing Validators

Validators implement IRoutingValidator and run synchronously before a route or termination fires. They examine external artifacts (change log, test report, brief file) rather than LLM output.

Validator What it checks
RequireShellPass Change log contains a shell command in the current turn that matches RequiredCommandPattern and exited 0
RequireWriteFile (HandoffToTesterValidator) Change log contains a file write in the current turn (or a shell fallback with the configured pattern)
TestReportValid (HandoffToReviewerValidator) Test report file exists, is non-empty, and all TestAssertionPatterns match
RequireBrief Brief file exists and is non-empty
RequireAllFilesWritten All files listed in the brief's deliverables section have been written per the change log
RequireReviewJudgement Last reviewer message contains an explicit APPROVED or REJECTED keyword

When a validator fails, the route is blocked: the source agent is re-invoked with an injected error message describing exactly what was missing. After MaxRetries consecutive failures, ValidatorStuckException is thrown and the session may escalate to HITL.


9. Session and Checkpoint Layer

Every session is backed by a SessionCheckpoint persisted after each agent turn.

SessionCheckpoint fields:

Field Purpose
SessionId 8-character hex ID (Guid.NewGuid().ToString("N")[..8])
Task Original task string
ConfigPath Config file that produced this session (used on resume)
Messages Ordered List<AgentMessage> — the complete conversation transcript
StartedAt UTC timestamp of session creation (immutable)
LastUpdatedAt UTC timestamp of last save (set by SaveAsync)
IsComplete Set to true after the session runs to completion; prevents re-resume
ResumeExecutorId Hint for WorkflowOrchestrator — which executor was active when compacted
MagenticState MagenticCheckpointState snapshot for Magentic loop resume

AgentMessage fields: AgentName, Content, Role, TurnIndex, Timestamp, Usage (tokens + cost), IsCompactionSummary, ToolCalls (name, args summary, succeeded).

ISessionStore contract: - SaveAsync — create or overwrite; sets LastUpdatedAt - LoadAsync — load by session ID, null if not found - DeleteAsync - ListAsync — all checkpoints sorted by LastUpdatedAt descending

JsonSessionStore (default): one JSON file per session at ~/.fuseraft/sessions/<sessionId>.json. Unix file permissions set to 0600 on non-Windows. ListAsync deserializes all .json files in the directory with error logging for unreadable files.

InMemorySessionStore: ConcurrentDictionary backed; sessions lost on process exit. Used when Checkpoint.Mode = "memory" in config or when no config-level checkpoint path is set and the user explicitly opts in.

Save points (in SessionRunner): after each agent message, after HITL human redirect, before and after compaction, and at session completion (IsComplete = true).

Resume path (RunCommand): --resume <sessionId> loads the checkpoint, validates IsComplete == false, rehydrates priorHistory, and calls SetResumeExecutorId / SetResumeState on the orchestrator before the next StreamAsync call.

Why we did not use the MAF framework's checkpointing layer: The framework's Checkpoint type captures MAF workflow execution state — executor queue, edge state, outstanding external requests. Our SessionCheckpoint captures conversation semantics — agent messages, token usage, cost, Magentic loop counters. They solve different problems at different levels of abstraction. The framework layer applies only to WorkflowOrchestrator (which uses InProcessExecution); AgentOrchestrator and MagenticOrchestrator are manual loops with no MAF workflow graph. Replacing our layer with the framework's would lose agent identity, role, token usage, cost tracking, and Magentic loop state, while gaining sub-turn recovery that provides no practical benefit given our turns are already fine-grained checkpointed.


10. Conversation Compaction

ConversationCompactor prevents context window exhaustion on long sessions by summarizing older turns using an LLM.

Trigger: ShouldCompact(messages) returns true when messages.Count >= config.TriggerTurnCount.

Process: The oldest Count - KeepRecentTurns messages are compacted into a single summary AgentMessage. The retained tail is kept verbatim. The summary is injected with Role = "user" so agents treat it as context, and IsCompactionSummary = true so tooling can identify it.

Cost accounting: The summary message's Usage.CostUsd carries the cumulative cost of all compacted turns plus the summary call itself, preserving exact budget tracking across compaction boundaries.

Resume note: For WorkflowOrchestrator sessions, a standard WorkflowResumptionNote is appended to the summary prompt instructing agents to re-read the brief and change log (not available from memory alone after compaction). This note is suppressed for Magentic sessions, which have no brief or change log.

After compaction: SessionRunner captures ResumeExecutorId from the last assistant message in the compacted tail, updates the checkpoint, and saves before continuing.


11. Plugin System

Plugins are AIFunction-providing objects registered in PluginRegistry and referenced by name in AgentConfig.Plugins.

Built-in plugins:

Plugin Tools
FileSystem read_file, write_file, delete_file, list_files, move_file
Shell shell_run, shell_run_script
Git git_commit, git_diff, git_log, git_status
Http http_get, http_post, http_put, http_patch, http_delete — uses named ApiProfiles
Json json_read, json_write, json_query (JMESPath)
Search search_web (DuckDuckGo)
CodeExecution execute_code (sandboxed subprocess)
Plan read_plan, write_plan — structured brief/deliverables file
Changes read_changes — read the JSONL change log for observability by downstream agents
Probe probe_url, probe_port — network reachability checks
Process kill_process, list_processes
Scratchpad scratchpad_read, scratchpad_write, scratchpad_list — per-agent key-value store
Chatroom chatroom_send, chatroom_read — shared coordination log

MCP servers (McpSessionManager): connected at startup via ModelContextProtocol. Each server's tools are registered under the server's configured name and are available to any agent that lists that name in Plugins. MCP connections are disposed when the session ends.

SandboxEnforcementFilter (middleware, not a plugin): wraps any agent with a filesystem sandbox. Tool calls that would access paths outside the sandbox root are denied with [DENIED: sandbox]. Prompt injection attempts are detected by the governance kernel's InjectionDetector and also denied.


12. Governance

GovernanceKernel (from Microsoft.AgentGovernance) is constructed by OrchestratorBuilder and threaded through the entire stack.

Capabilities enabled at startup:

Feature Purpose
Audit Hash-chain audit log of every governance event (allow/deny decisions)
Metrics Counters for validator passes/failures
Prompt injection detection Detects and blocks injection attempts in tool inputs
Rings Maps AgentConfig.TrustScore to execution privilege rings (Ring 1 ≥ 0.80, Ring 2 ≥ 0.60, Ring 3 < 0.60)
Circuit breaker Wraps agent.RunAsync calls; trips after 5 failures, resets after 30s, half-open with 1 probe call
SLO engine Tracks routing validator compliance rate over a 1-hour rolling window; 95% target; burn-rate alerts at 2× (warning) and 5× (critical) over 600s

Policy files: If config/policies/default.yaml exists alongside the config file, it is loaded as a governance policy and applied to all agents in the session.

Event bridge: GovernanceEventType.ToolCallBlocked events (from sandbox + injection checks) are forwarded to the EventEmitter as tool_blocked JSONL events. PolicyViolation events are emitted directly by KeywordSelectionStrategy with richer per-turn context.

Agent DIDs: Every agent is assigned a did:fuseraft:<name> identifier at construction. DIDs are used as actor identifiers in the audit log and are resolved by AgentFactory.GetDid(name) for governance lookups.


13. Change Tracking

ChangeTracker wraps every agent with a CapturingMiddleware that intercepts tool call results and records structured entries to a JSONL change log.

Tracked functions: write_file, delete_file, shell_run, shell_run_script, git_commit.

ChangeLog schema (one entry per turn): - ActiveSessionId — current session ID - Entries[]{ Agent, TurnIndex, Timestamp, SessionId, FilesWritten[], FilesDeleted[], CommandsRun[], GitCommits[] }

Downstream use: The Changes plugin exposes read_changes so agents (typically Tester or Reviewer) can read what previous agents actually did rather than inferring it from chat history. RequireShellPass and RequireWriteFile validators also read this log to verify deterministic pre-conditions before routes fire.

SetSessionIdAsync is called by SessionRunner once the session ID is established, stamping the log's ActiveSessionId field so multiple sessions in the same working directory can be distinguished.


14. Event Emission

EventEmitter appends structured JSONL events to a configured file path. All writes are serialized through a SemaphoreSlim. Errors are swallowed — event emission is best-effort and never disrupts the session.

Event schema: { ts, session, agent, turn, event_type, payload }

Event types emitted:

Event Emitter Payload
turn_end AgentOrchestrator Agent name, turn index, cost
validation_fail KeywordSelectionStrategy Validator name, consecutive count, error detail
hitl_escalation RunCommand Reason (stuck validator)
tool_blocked OrchestratorBuilder (governance bridge) Policy name, denial data
magentic_plan MagenticOrchestrator Plan text
magentic_complete MagenticOrchestrator Round count

15. DevUI

DevUIServer is a lightweight ASP.NET Core server (started inline via WebApplication.CreateSlimBuilder) that provides real-time session visualization in a browser.

Endpoints: - GET / — self-contained HTML page (inline in DevUIHtml.cs) - GET /api/stream — Server-Sent Events stream of session events

Event types: session_start, agent_starting, message (with agent name, content, token usage, cost, elapsed ms), session_end.

Full-history replay: New SSE clients receive the complete event history on connect so page refresh always shows the entire session from the beginning.

Port: dynamically assigned via TcpListener(IPAddress.Loopback, 0) at startup; printed to the terminal.

Why we did not use the framework's Microsoft.Agents.AI.DevUI: The framework's DevUI is an API playground for hosted agent services — it requires AddOpenAIResponses(), AddOpenAIConversations(), and ASP.NET Core hosting, and presents a chat interface over those HTTP endpoints. Fuseraft-cli is a console executable with no hosted agent API. Our DevUI visualizes the streaming event flow of a running orchestration session (agent turns, cost, token usage, phase transitions) — a fundamentally different use case that the framework's DevUI does not address.


16. Microsoft Agent Framework Usage

Fuseraft-cli is built on MAF (Microsoft.Agents.AI, Microsoft.Agents.AI.Workflows, Microsoft.Agents.AI.Anthropic).

What we use:

MAF Component How we use it
AIAgent / ChatClientAgent Base agent type; RunAsync(context, null, null, ct) drives each LLM turn
AIAgentExtensions / ChatClientFactory Agent builder helpers
AnthropicClientExtensions Constructs Anthropic-backed AIAgent instances
WorkflowBuilder Builds linear phase workflows for WorkflowOrchestrator
FunctionExecutor<T> Wraps per-agent logic in MAF's executor model
InProcessExecution.RunStreamingAsync Drives the workflow graph; returns an async stream of events
WatchStreamAsync Consumes WorkflowOutputEvent and WorkflowErrorEvent to drive the phase loop
WorkflowOutputEvent Signals a phase-break (agent called YieldOutputAsync)
WithOutputFrom Restricts phase-break output to Tester and Reviewer only
IWorkflowContext.SendMessageAsync Routes AgentContext to the next executor (HANDOFF TO X)
IWorkflowContext.YieldOutputAsync Signals phase-break to the outer loop

What we do not use:

MAF Feature Reason
Fan-out / fan-in edges All current configs are sequential pipelines; no parallel agent execution
Conditional edge predicates / SwitchBuilder Routing logic lives inside executors (requires retry loop that graph edges cannot provide)
StatefulExecutor AgentContext as a shared context object serves the same purpose without scoped state isolation
AggregatingExecutor No incremental aggregation pattern in any current orchestrator
RequestPort (external request handling) Currently unused; a natural fit for Magentic's HITL plan review loop (see below)
CheckpointManager / FileSystemJsonCheckpointStore Framework layer captures workflow execution state; our layer captures conversation semantics — different problems
GroupChatWorkflowBuilder Requires a single shared history; Magentic's two-history model is incompatible (see §5.3)
Microsoft.Agents.AI.DevUI For hosted agent services with OpenAI-compatible API endpoints; our DevUI serves a different purpose

MAF WorkflowOrchestrator graph topology: The graph is always a linear chain — AddEdge(src, sink) only. Cycles are implemented via the outer while(phaseCount < maxPhases) loop that builds a fresh workflow per phase. This is the correct approach: MAF's WorkflowBuilder validates DAG structure and does not support in-graph cycles.

Future opportunity — RequestPort for Magentic HITL: The framework's RequestPort is a pause-and-wait-for-external-input primitive: the workflow halts at a RequestHaltEvent, the caller calls SendResponseAsync(response) to resume. This maps cleanly onto Magentic's plan review loop (currently a polling IHumanApprovalService call). Migrating the plan review to RequestPort would require MagenticOrchestrator to be backed by a MAF workflow rather than a manual loop, which is a non-trivial refactor but architecturally sound.


17. Decisions Against Framework Features

A summary of explicit decisions not to use certain framework capabilities, with rationale.

GroupChatWorkflowBuilder for MagenticOrchestrator Rejected. The framework's group chat model passes the same conversation history to both the manager and participants. MagenticOrchestrator requires two entirely separate histories: a private manager context (fact-gather, plan, ledger evaluations) and a shared participant context. Forcing this into GroupChatManager.UpdateHistoryAsync would require fabricating the manager's history on every call, which is fragile and defeats the architecture's clarity. The planning phases, stall detection, replan cycles, and HITL plan review also have no equivalent in the framework abstraction.

MAF framework checkpointing (CheckpointManager, FileSystemJsonCheckpointStore) Rejected as a replacement for ISessionStore. The framework's Checkpoint type captures MAF runtime execution state (executor queues, edge state, workflow topology). Our SessionCheckpoint captures conversation semantics (agent messages, token usage, cost, Magentic loop state). They operate at different layers of abstraction and solve different problems. Framework checkpointing applies only to WorkflowOrchestrator and would not help AgentOrchestrator or MagenticOrchestrator at all. Sub-turn recovery (the only benefit the framework layer would add to WorkflowOrchestrator) is not a practical concern given our turns are already fine-grained checkpointed at the conversation level.

Microsoft.Agents.AI.DevUI Rejected as a replacement for our DevUIServer. The framework's DevUI is designed for hosted ASP.NET Core services exposing OpenAI-compatible Responses and Conversations API endpoints. It presents a chat interface over those endpoints. Fuseraft-cli is a console executable — it has no hosted agent API to point the DevUI at. Our DevUIServer visualizes the real-time streaming event flow of a running orchestration session, which is a different problem the framework's DevUI does not address.

StatefulExecutor in WorkflowOrchestrator Not adopted. Each executor sharing AgentContext (a single mutable object passed through MAF's message routing) achieves the same effective state — all agents read from and write to the same conversation history. StatefulExecutor would isolate state per executor, which would require explicit merging of histories and break the shared-history invariant that routing strategies depend on.

Graph-level conditional routing in WorkflowOrchestrator Not adopted. MAF edge conditions fire once per message and have no retry semantics. When an agent fails to emit a routing keyword, the executor injects a correction and calls the LLM again. This retry loop must live inside the executor. Moving routing to graph edges would require removing retries, degrading robustness when models do not follow instructions on the first attempt.