Bump Microsoft.Agents.AI.Workflows from 1.17.0 to 1.20.0 - #98
Open
dependabot[bot] wants to merge 554 commits into
Open
Bump Microsoft.Agents.AI.Workflows from 1.17.0 to 1.20.0#98dependabot[bot] wants to merge 554 commits into
dependabot[bot] wants to merge 554 commits into
Conversation
added 30 commits
June 13, 2026 14:31
design.md: - Add sections 6.5 (MapReduceOrchestrator), 6.6 (ScatterGatherOrchestrator), and 6.7 (sub-graph nodes) with execution models and key invariants - Fix section 7 selection strategies table: split sequential/roundrobin into separate rows with accurate descriptions; add mapreduce and scattergather rows examples.md: - Add "Scatter-gather — multi-expert review" example (three parallel reviewers + synthesizer, no routing keywords) - Add "Map-reduce — parallel document analysis" example (Planner splits, Analyst maps per doc, Synthesizer reduces)
- TryParseItems bracket counter was string-blind: a } inside a JSON string value caused premature termination, triggering spurious splitter retries on valid output; replaced with string-aware FindJsonObjectEnd helper - Splitter retry guard used > instead of >=, allowing MaxSplitterRetries+1 actual retries while reporting the configured count to users and the LLM - AgentStarting was never fired for parallel mapper and scatter participant tasks, leaving the UI dark and skipping subscribers during parallel phases - humanApprovalService was forwarded to Graph and Adversarial orchestrators but silently dropped for MapReduce and ScatterGather; now plumbed through consistently so --hitl applies to all orchestrator types - Sub-graph nodes passed null for priorHistory, so sub-orchestrator agents had no visibility into parent phase outputs or handoff context; now builds priorHistory from ctx.History entries after the original task message - ExtractHandoffToolCallKeyword was called with an empty [] in RunSubGraphNodeAsync, making it always return null; removed the dead call and fall directly to text-based keyword detection with an explanatory note - Sub-graph log line used a two-branch ternary that logged "graph" for ScatterGather sub-graphs; extended to a proper three-way branch - MapReduce and ScatterGather sub-orchestrators were constructed with NullLogger, silently discarding all retry warnings and phase diagnostics; GraphOrchestrator now accepts ILoggerFactory and creates typed loggers - gatherNarrative StringBuilder in ScatterGatherOrchestrator was populated but never consumed; removed along with the unused System.Text import
- Empty string can spuriously match ExcludeAgents entries that were accidentally left blank; the sentinel makes the intent explicit
- Covers the three non-interactive modes agents need: simple upstream rebase, --onto rebase, and abort/continue/skip for conflict recovery - Registered in MutationTools so the post-turn fabrication check treats it as a real state-mutating operation
- `fuseraft keychain --set` reads FUSERAFT_API_KEY from the subprocess environment and stores it in the OS keychain (Windows Credential Manager, macOS Keychain, or secret-tool on Linux) - `fuseraft keychain --get` reads from the OS keychain and writes the key to stdout; exits 1 when no key is stored - `fuseraft keychain` (no flags) prints whether a key is stored - Command is hidden from --help; it is for extension-internal use only
- Changes, Chatroom, SessionContext, and Scratchpad were only available in orchestrator sessions; the REPL had no way to opt into them - Paths are session- and project-scoped (matching orchestrator behavior) so artifacts land in the expected ~/.fuseraft/ locations - EnabledPlugins uses OrdinalIgnoreCase so casing in the flag is irrelevant
- Introduce IHasArtifact so each plugin exposes its resolved artifact path and label without hard-coding them in FuseraftPaths - BuildFolderOrientationBlock gains pluginArtifacts and includeInfrastructure params; omits the Runtime artifacts header entirely when there is nothing to list - OrchestratorBuilder builds a per-agent artifact list from the agent's own plugin set, so agents only see paths they can use - ReplCommand tracks activePlugins and forwards them to AddSessionInfo so the REPL system prompt reflects the same filter - Fix CS9113: store humanApprovalService in MapReduceOrchestrator and ScatterGatherOrchestrator to match the rest of the orchestrators - Fix CS8601: guard AuthorName null with ?? string.Empty in GraphOrchestrator sub-orchestrator history projection
Text chunks are now written to the terminal as they arrive so the user sees the model's reasoning incrementally. When streaming completes, the cursor is restored (ANSI \x1b7/\x1b8) to just after the "fuseraft agent:" header and the full response is re-rendered with Markdown formatting, replacing the plain streamed text. Tool-call spinners that fire mid-response print a newline first so the spinner does not overwrite the last streamed characters.
After a tool call completes and the model starts generating the next text segment, restore the cursor to just after the "fuseraft agent:" header (\x1b8\x1b[J) so the new segment overwrites the previous one instead of appending. Also guards inline Console.Write(text) behind !IsOutputRedirected so piped output only receives the final Markdown-rendered response.
- MaxRevisits escalation was indefinite; repeated messages were ignored, allowing planning loops to run past the configured limit unchecked. MaxEscalations (default 2) now aborts via ValidatorStuckException once escalation attempts are exhausted. - Developer instructions explicitly exclude brief-review.json, which the agent was reading and misinterpreting as contradictions in the brief, triggering spurious REPLAN REQUIRED signals after brief approval.
- ReplNextCommand mirrors ReplCommand setup but delegates to ReplNextTurn for terminal rendering - ReplNextTurn streams tokens directly with VT save/restore for in-place markdown re-render - Activated as default via FUSERAFT_REPL_NEXT=1, or explicitly as hidden `fuseraft repl-next`
- Models that emit finish_reason=tool_calls with no text (e.g. grok-4.3 as Developer) produce empty AgentMessage.Content; after StreamAsync replay the handoff keyword was invisible to IsSignalOnOwnLine, causing keyword_not_found re-invocations and unnecessary back-edge fires - TryPinLastRoutingSignal's alreadyPresent guard checked the ToolCalls record layer, but FunctionCallContent is stripped on every replay, so a record existing never guaranteed the signal survived into ChatMessage text; the mitigation silently no-oped for retained turns (the common case when the triggering turn is recent) - Replay loop now extracts route_keyword from ToolCalls when content is empty, making it detectable via text without FunctionCallContent - alreadyPresent now checks whether the keyword appears as text in retained Content, matching the condition that matters post-replay
Issue 1 — Verifier wastes a turn on git outside a non-repo sandbox - Add GitPlugin.IsInsideWorkTreeAsync: returns 'true'/'false' by probing 'git rev-parse --is-inside-work-tree'; maps exit 128/129 to 'false' - Guard Verifier step 4 in InitTemplates.DevTeam.cs: probe git repo presence before running any git command; skip git entirely when not inside a work tree Issue 2 — Developer writes only stubs when task exceeds one context window - Planner step 5b(e): after writing execution_checklist, verify every file-write step's path also appears in files_to_change; add any missing paths - Developer pre-handoff audit: call changes_read_latest and confirm all checklist file-write steps are in filesWritten before HANDOFF TO TESTER; route to REPLAN REQUIRED instead when context budget is exhausted mid-checklist - Add ChecklistComplete predicate to ContractEngine: extracts file-path tokens from execution_checklist items (heuristic: contains '/' or known extension), checks all against the written-file log; registered as 'checklistcomplete' in the dispatch switch - Wire ChecklistComplete into ImplementationComplete contract in InitTemplates Issue 3 — 22 duplicate reads on every compaction restart - BuildExplorationBlockAsync: when ParseToolCallEventsAsync returns zero reads, seed fileReads from read_cache.json (written synchronously on every read_file call, always current) so the exploration block is never empty on first compaction - WorkflowResumptionNote step (1): made conditional — agents skip read_file when goal and files_to_change are already visible in the compaction summary - Add BuildBriefBlockAsync: reads Validation.BriefPath and embeds goal, files_to_change, verify_command, and execution_checklist as a [BRIEF SNAPSHOT] block at the front of every compaction summary; wire briefPath from Validation.BriefPath in OrchestratorBuilder Docs: update plugins.md (git_is_inside_work_tree), configuration.md (ChecklistComplete predicate), validators.md (predicate list), and context-management.md (brief snapshot block, exploration fallback, conditional resumption note)
- StateMachineSelectionStrategy: stop non-current-agent messages from burning the AgentMessageLookback budget before the source-agent guard runs, in both SelectAsync and TrySelectParallelAsync. - Restrict the BLOCKED-signal check to the current state's agent so other agents' narrative text can't abort the session. - Track the ordinal position of the last handoff signal consumed by a fired transition (sequential or parallel) and use it in CompactionCoordinator.TryPinLastRoutingSignal to avoid re-pinning a signal the state machine already acted on across compaction. - Extract the duplicated lookback-scan scaffolding (HandoffPlugin extraction, content resolution, isCurrentAgent check, budget skip) from both selection methods into a shared ScanSignals iterator. - FileSystemPlugin: add the same file-vs-directory guard to ListFiles that ListDirectory already had, with null-safe GetDirectoryName for root paths. - InitTemplates.DevTeam: Planner reads LocalPreflight "if it exists" with a fallback to infer runtime/git info from the codebase, for sessions resumed directly into Planning.
Three bugs that caused every --resume to restart from Preflight: 1. RunCommand.cs never called SetResumeStateName(checkpoint.CurrentStateName) or SetResumeSnapshot(checkpoint.StateMachineState) — both fields were saved during compaction but silently ignored on resume, causing the state machine to always init from Initial (Preflight). 2. SessionRunner.HandleValidatorStuckAsync didn't persist the current state machine position before aborting. checkpoint.CurrentStateName was only updated at compaction time, so an abort between compactions left it stale. Added TrySaveStateMachinePositionAsync() to capture and persist the live state before breaking the loop. 3. ContractEngine.ChecklistComplete treated any '/'-containing token as a file-path requirement. Tokens like 'typer/rich/langchain' and 'src/lily/defaults/' (no extension) triggered false ImplementationComplete failures. Fixed: rely solely on ChecklistFileExtensions for path detection.
Two issues let a model stuck on the wrong handoff signal loop indefinitely without correction. InjectMissingSignalCorrectionIfNeeded skipped FCC-only turns. The guard on empty msg.Text treated handoff-only turns as if the agent said nothing, so no correction was injected when the model called handoff(WRONG SIGNAL) repeatedly. Fix: walk past the empty shortcircuit tail, then when the last substantive agent turn has an FCC(handoff) with a signal not in the valid transition set, inject a targeted correction naming the wrong signal explicitly. Checkpoint reconstruction injected wrong signals as plain text. On resume, FCC-only handoff turns were reconstructed as text assistant messages carrying the route_keyword verbatim. A prior stuck run using the wrong signal produced in-context examples that the model mirrored. Fix: for current-state-agent messages, only inject the handoff keyword as text if it matches a valid transition for the current state. Also removes the DIAG stderr block added during investigation.
- DEC \x1b7/\x1b8 save/restore fails when the terminal scrolls during a long response: the saved viewport-relative position is no longer reachable, so the erase lands nowhere and the markdown re-render appends below the raw streamed text instead of replacing it - Line counting (newlines + word-wrap via termWidth) is scroll-safe because \x1b[nA is always relative to the current cursor position - Tool-call AnsiConsole.WriteLine() calls while text is streaming also increment the counter so the erase reaches back past the spinner row - Applied identically to ReplTurn and ReplNextTurn
- New "greenfield" template: Planner → Developer → Tester → Reviewer without PlannerCritic or Verifier, optimised for new-project speed where there is no legacy to archaeologise and critique overhead adds more friction than value - Bump Developer MaxInTurnContextTokens 30k→60k to fit longer file writes without hitting the in-turn cap mid-patch - Raise WarnTurnTokens 60k→100k and ContextBudget 60k/100k→100k/180k to match the larger context windows in practice - Relax Verifier cadence 4→8 turns (reduces interruption on fast runs) and lower MaxConsecutiveTurnsWithoutSignal 8→5 (catches stuck agents sooner) - Remove hardcoded build|compile regex on CommandSucceeded so the verify command pattern is user-configurable - Ignore .fuseraft/ at the repo level (runtime state, not source)
- Use saved cursor position (\x1b7/\x1b8) instead of line counting for clearing previous output - Detect when output has scrolled beyond terminal height and fall back to appending below - Prevents garbled output when long agent responses exceed visible window height
- Live raw-text printing required cursor save/restore + line-wrap tracking to reconcile with the final markdown re-render, which kept breaking on terminal scroll/resize edge cases (see prior two fixes) - Terminal REPL now only shows the spinner/tool-call chain while generating, then renders the complete response as markdown once the turn finishes - JSON mode (VS Code integration) is untouched and still streams token events for its own renderer
- Soft-wrapped continuation lines (no bullet, no blank line) were treated as a new disconnected paragraph block, dropping the bullet/indent and producing stray unindented lines right after list items - Paragraphs preserved the source's hard line breaks instead of reflowing, so wrapping only looked correct when the source happened to be wrapped at the same width as the terminal - Both now accumulate continuation lines into one logical block joined by spaces, via a shared IsBlockBoundary check, and let Spectre.Console wrap to the actual console width
- KeepLastToolPairs duplicated logic Microsoft Agent Framework now ships natively (Microsoft.Agents.AI.Compaction, already present in the 1.9.0 package this project depends on) — collapsing to MAF's ToolResultCompactionStrategy removes the manual placeholder-splicing and separate ProtectedData-stripping pass in favor of one atomic group collapse, which is also more robust against leaving an orphaned FunctionCallContent for strict providers. - Streaming path now needs its own async iterator (StreamWithToolPairWindowAsync) since the trim call is no longer synchronous; this also let the now-dead EmptyStreamingResponse helper be deleted. - Behavior is pinned down by AgentFactoryKeepLastToolPairsTests, written against the old implementation first and re-verified unchanged after the swap. One intentional semantic shift: the limit now bounds MAF "groups" (one turn + all its tool results) rather than individual tool messages, so turns with parallel tool calls collapse as a single unit. - TrimInTurnContext/AdaptiveTrimMessages, ConversationCompactor, and GraphOrchestrator's cycle handling are intentionally untouched — see the approved plan for why each is out of scope here.
- GraphOrchestrator's "phase restart" mechanism for back-edges was built around a belief that MAF's WorkflowBuilder enforces an acyclic constraint within a phase. That constraint doesn't exist at the general AddEdge level (confirmed by running MAF's own Loop sample) — but phase-restart also does real work beyond cycle support (phase-transition history injection) and GraphOrchestrator has zero existing back-edge test coverage, so it was kept untouched rather than risk a rewrite of the riskiest, least-tested part of the largest file in the orchestration engine. - WorkflowOrchestrator is a new, additive Selection.Type: "workflow" instead: it reuses the same Selection.Graph config shape but compiles the whole graph, cycles included, into one persistent MAF workflow built once per session via plain AddEdge calls — no BFS layering, no back-edge classification, no per-cycle phase rebuild. - v1 explicitly rejects Parallel, SubGraphId, RequireHumanApproval, and RecoveryAgent at config-validation time rather than silently ignoring them, pointing callers at Selection.Type: graph for those features. - Routing is tool-call-only (handoff(route_keyword: ...)), with no text-on-its-own-line fallback the way graph has — matches how MAF's own HandoffWorkflowBuilder routes (verified in its source: real tool calls, never text scanning), and removes a class of correction retries caused by text-parsing fragility. This isn't a new burden in practice: the shipped graph template's agents already have the Handoff plugin enabled and are already instructed to prefer it. Config validation rejects a workflow config whose agents lack it. - GraphOrchestrator, CorrectionEngine, KeywordDetector, HandoffPlugin, and FUSERAFT.md are unchanged — confirmed via diff before committing.
- `fuseraft validate` keeps its own selection-type allowlist separate from OrchestratorBuilder's, so adding WorkflowOrchestrator missed it entirely — validating a correct workflow config reported "Unknown selection type: 'workflow'" before a session was ever run. Found by live-verifying WorkflowOrchestrator end-to-end with a real model. - Added ValidateWorkflowRestrictions, mirroring the same checks OrchestratorBuilder.ValidateAndSelectStrategy enforces at run time (reject Parallel/SubGraphId/RequireHumanApproval/RecoveryAgent/ no-keyword edges, require the Handoff plugin on every node's agent) so `validate` surfaces them statically instead of only at `run`. - mapreduce and scattergather were also missing from the same allowlist — a pre-existing gap, unrelated to workflow, found while fixing it. Neither has dedicated structural validation in this command (no equivalent of ValidateGraph/ValidateMagenticSelection), so the fix here only restores type recognition, not deeper checks.
- mapreduce/scattergather were only just recognized as known selection types in the prior commit, but still had no structural validation in `fuseraft validate` (unlike Graph/Magentic/Adversarial) — a config with a missing Splitter/Mapper/Reducer or empty Participants list would pass validation and only fail later, at `run` time. - Mirrors OrchestratorBuilder.ValidateAndSelectStrategy's existing Selection.MapReduce/Selection.ScatterGather checks exactly (same field rules, same agent-reference checks) so `validate` and `run` agree on what's valid. - Replaced the two placeholder "recognized, not unknown type" tests (which used configs with no MapReduce/ScatterGather block at all — these now correctly fail under the new structural check) with full valid-config and missing-block/bad-agent-reference cases for both types.
- Archaeologist (brownfield) and Preflight (greenfield) previously had full FileSystem write access despite read-only-by-instruction roles. Live model runs showed the model using that access to implement the requested feature itself during recon, before handoff to Planner. - Capabilities only gates by tool tag (read/write/delete), not by path, so a flat FileSystem:[read] lock would also remove these agents' legitimate need to persist conventions.json/brief.brownfield.json/ preflight.json. - Split into ReconPlugin (write_file_conventions/write_file_discovery_brief, used by Archaeologist) and PreflightPlugin (write_file_preflight, used by Preflight) so each agent is locked to FileSystem:[read] while still exposing exactly the one write function it needs — verified at the wire level via scripts/capture_model_request.py that no write_file/patch_file function is offered to either agent. - ReconPlugin's ConventionProfile/BrownfieldDiscoveryBrief field names also fix a pre-existing drift: the old Archaeologist prompt described fields (naming_convention, import_style, ...) that didn't match the real DTOs in BrownfieldConfig.cs, so system-prompt injection of conventions had likely never actually worked. - Add scripts/capture_model_request.py, a reusable mock OpenAI-compatible capture server for proving tool-schema enforcement at the wire level instead of inferring it from model behavior.
TestsValid's HasAssertions check (used by both greenfield and DevTeam templates) was meant to cross-reference test-report.json commands against the change log to catch a Tester writing a plausible-looking report without actually running anything. Two bugs let fabrication through anyway: - It checked "any one result across the whole report matches" instead of verifying every result independently — one genuine command could vouch for an arbitrary number of fabricated rows alongside it. - The substring match was bidirectional. A real aggregate run like "pytest" is a substring of a fabricated, more specific claim like "pytest tests/test_x.py::test_name", so the fabricated claim would satisfy the check purely because it happened to contain the real command's text — exactly backwards from what's verifiable. This is precisely the pattern observed in a live greenfield run: the Tester ran one aggregate pytest invocation but reported distinct per-test commands that were never independently executed. Fix: verify each result row independently, and only allow the direction where the real succeeded command contains the reported claim (an honest abbreviation), not the reverse (a fabricated embellishment). Matches the one-directional containment already used by CommandSucceeded for the same reason.
Same FileSystem write over-exposure bug found and fixed for greenfield's Preflight and brownfield's Archaeologist also exists in two more templates that follow the identical recon-style pattern: - swe's Preflight (InitTemplates.DevTeam.cs) is the same agent as greenfield's, just not yet patched — full FileSystem write access despite being read-only-by-instruction, with no technical enforcement. Reuses the existing PreflightPlugin/write_file_preflight (already wired in PluginRegistry/OrchestratorBuilder for greenfield), just adds Capabilities: FileSystem:[read] and swaps the raw write_file step for write_file_preflight in the prompt. - audit's Auditor is the same class of bug: a security/quality auditor with full read+write+delete FileSystem access despite never being instructed to modify anything — letting an auditor patch the very code it's auditing is both a conflict of interest and a security risk. Its only legitimate write target is the single fixed-path findings report, so it gets the same narrow-plugin treatment: new AuditPlugin.WriteFileAuditFindingsAsync takes parallel arrays (one per finding field) instead of a path parameter, serializes to LocalAuditFindings, and is registered in PluginRegistry. Configure() resolves the findings path against the same sandbox root FileSystemPlugin uses, so Prioritizer's read_file finds it regardless of sandbox configuration. Verified at the wire level for both (scripts/capture_model_request.py): neither agent's tool list contains write_file/patch_file/delete_file after the fix, and RequireWriteFile keeps working unchanged since both new function names still contain the "write_file" substring it matches on.
Auditor/Preflight/Recon were already locked down because their write
access let them implement the task they were supposed to only scope or
inspect. The same risk applies to a second group of agents that never
have a legitimate write target at all: Reviewer (brownfield, swe, the
pipeline template, greenfield), Verifier (swe, audit, devops), and
devops's Executor. Each of these only reads files, runs shell_run for
spot-checks, and reports a verdict via handoff/plain text/
session_context_write — confirmed by reading every line of their
instructions, none of them ever mention write_file/patch_file.
Unlike the Auditor/Preflight fix, these need no new plugin — they have
no artifact to persist, so Capabilities: { FileSystem: [read] } is a
plain capability tightening with no prompt changes. Regenerated and
re-validated all six affected templates (audit, brownfield, devops, swe,
pipeline, greenfield) to confirm the added Capabilities block doesn't
break config validation.
Chars-to-tokens conversion was duplicated as inline /4, /3, and *4 literals across ~13 files (compaction triggers, pre-turn budget guards, REPL context displays, tool-schema estimates). Extracted a single fuseraft.Core.TokenEstimator with named CharsPerToken/CharsPerTokenDense constants so the ratio lives in one place and the dense-vs-default choice is explicit rather than an unexplained magic number. No behavior change — same math, same call sites.
fuseraft's REPL had no workspace-level undo (/rewind only touched conversation history, never files) and no way to attach an MCP server to a live session. Both were closing gaps identified against Cline's CLI. /undo reverts files written, patched, copied, moved, or deleted in the most recent turn — one snapshot per touched path per turn, stored under the session's undo/ directory so it survives --resume. Repeatable; walks back one turn at a time. Directory moves fall out of the same per-file snapshot primitive with no special-casing. /mcp / /mcp add / /mcp remove let a session connect an MCP server interactively (same wizard style as /provider setup) instead of hand-editing YAML. Servers persist to ~/.fuseraft/repl-mcp-servers.json by default and reconnect automatically on the next launch; --session-only skips that. New: UndoSnapshotStore, ReplMcpServerStore, McpSessionManager.ConnectSingleAsync.
cli-reference.md's REPL slash-command table and deep-dive sections had no entries for /undo or /mcp. Add both, plus a pointer from mcp.md (previously config-only) to the REPL wizard as the interactive alternative.
AgentMiddlewareBuilder's adaptive context-trim retry rescued a single overflowing provider call by truncating tool results, then discarded the trim — the checkpoint kept the full oversized history, so the same overflow could recur next turn. A new AdaptiveTrimTracker flags the agent when this fires; CompactionCoordinator consumes it to force a real compaction (not suppressed by the post-compaction grace period, same as a single-turn explosion), and ConversationCompactor's new preferDeterministic mode downgrades llm/hybrid to intent/lossless for that compaction so the recovery itself can't also overflow an LLM summarizer call. Two more bugs surfaced by live-testing this end-to-end: - TrimToolResultsToChars (adaptive-trim stages 1-2) only truncated FunctionResultContent.Result when it was a plain string. In practice it's often a JsonElement instead, silently turning those stages into no-ops and leaving stage 3 (drop everything) as the only one that ever worked. Fixed with the same string/JsonElement/ToString() fallback already used correctly elsewhere in this file and in AgentContextCompactionFilters. - SessionRunner's pre-existing exhausted-retry recovery path (HandleContextExceededAsync) never produced a message, so it never incremented the turn counter MaxIterations depends on — a config whose budget can't fit even one compacted round trip could retry forever, ignoring a configured MaxIterations entirely. Now counts the cycle.
SessionRunner interrupts the agent stream the instant compaction is needed, even when the just-yielded message also satisfies termination — abandoning the IAsyncEnumerable before AgentOrchestrator's own post-turn termination check ever runs for it. The restarted StreamAsync call had no check of its own for "does the priorHistory I was just resumed with already satisfy termination," so it unconditionally ran another turn. Harmless in the rare case this used to be, but the new adaptive-trim compaction trigger fires on nearly every turn in a tight-budget workload, turning a rare coincidence into the common case: an agent that already said e.g. "DONE" would get invoked again, and again, never stopping until MaxIterations. Fix: check priorHistory against the termination condition once, on the first loop iteration, before running another turn. Verified live: same config, same task — before this fix, ran all 5 configured MaxIterations despite the agent saying DONE every turn; after, completes correctly in 1 turn. No AgentOrchestrator test fixture exists yet in this suite to cover it as an automated regression test.
The REPL built its chat client through a separate path (ReplFactory.BuildClient) that never got AgentMiddlewareBuilder's adaptive context-trim retry, so a provider ContextExceeded rejection just killed the turn and dropped the user's message — unlike orchestration, which now self-heals (see 5236d27/f06ec25). Also extends the retry to the streaming path itself (GetStreamingResponseAsync), which only had proactive pre-trim before: a context-limit rejection always surfaces before the first token, so it's safe to retry there too as long as nothing has been yielded to the caller yet. Routes ReplFactory.BuildClient through the same middleware chain, and adds a post-turn AdaptiveTrimTracker check in ReplTurn that forces a real /compact when a trim occurred, mirroring CompactionCoordinator's ContextExceeded branch so the same oversized history doesn't recur next turn.
/mcp remove only hid a server's tools from the model — the underlying connection (a real child process for stdio transport) stayed alive, orphaned, for the rest of the session no matter how many servers were added and removed. McpSessionManager tracked connections in a flat list with no way to tear down one in isolation; it's now keyed by server name with a RemoveAsync that disconnects and disposes just that client. CmdMcpRemove wires it in. Also wraps the REPL's turn loop in try/finally so an unhandled exception out of it still runs MCP disposal, the SessionEnd event, and memory extraction instead of silently skipping all three.
REPL had no way to gate an individual tool call — only whole-category disables via /safe-mode. Wire the same IHumanApprovalService gate that `fuseraft run --hitl` already uses into ShellPlugin at REPL startup, behind a new /hitl on|off toggle: when on, every shell_run/ shell_run_script/shell_run_background call prompts y/N before running. The approver is a closure over a shared HitlModeState instance (ShellPlugin is constructed before ReplSessionContext exists, so the flag can't live as a plain field there) — /hitl flips the same instance, taking effect on the very next shell call with no tool-schema rebuild needed.
ReplCommand.cs's SubAgentPlugin explorer/locate/delegate tools and AgentToolResolver.BuildSubAgentTools' orchestration SubAgent fallback each hand-copied the same three tool-name HashSets (FileSystem read, Shell run/read, Git read) with no reference between them — editing one silently left the other stale. Extract them into ExplorerToolSets as the one source of truth both call sites read.
Add /hitl to the REPL slash-command table and a deep-dive section explaining scope (shell only, no pause-every-turn behavior). Cross-link it from the existing fuseraft run --hitl docs, and fix both shell-approval descriptions to mention shell_run_background, which was already gated in code but missing from the docs.
REPL's only tool-gating was whole-category on/off (/safe-mode, /tools disable) while orchestration agents get fine-grained per-plugin tags via AgentConfig.Capabilities. Add /tools restrict <plugin> <tag...> / /tools unrestrict <plugin>, reusing PluginCapabilityMap.IsAllowed as the same enforcement function orchestration already relies on — e.g. `/tools restrict Git read` removes git_commit/git_push from the model's tool schema entirely while leaving git_status/git_diff available. PluginCapabilityMap now also stores each tool's owning plugin (GetPlugin), so filtering happens per-tool rather than by which REPL tool-category dictionary key currently holds it. That's why restricting reaches further than /safe-mode: a Git tool sitting in the "Extended" category (once --plugins Extended is enabled) is still covered by /tools restrict Git, where /safe-mode's category-key-only disable misses it entirely. Also fixes two stale rows in configuration.md's Capabilities table (missing shell_get_session_temp_dir, git_is_inside_work_tree, git_is_repo_root, and git_rebase).
- Crash: legacy-key migration dereferenced a null UserConfig when ~/.fuseraft/config was missing but an old .key file survived. - Turn-index corruption: mutation/critic/todo correction turns recursed into ExecuteAsync before the outer turn's own bookkeeping ran, inflating ctx.TurnIndex and mislabeling TurnEnd/message_end events. Corrections now run after the turn they follow has fully closed out. - Silent memory loss: LastExtractedTurnIndex wasn't reset by /clear, /rewind, or /compact, so exit-time memory extraction could compare against a stale index and skip a whole post-reset conversation. - Safe-mode desync: /tools enable on a Shell/Git/Http category left SafeMode=true while the category was actually back on, so a later /safe-mode on would no-op instead of re-disabling it. - JSON-mode contract break: /execute's critic-review branch wrote raw ANSI to stdout unconditionally; now gated on JsonMode, with the rejection reason carried in the StepHalted event payload instead. - /tools restrict silently blocked 100% of a plugin's tools when given a tag that plugin doesn't use (e.g. "Http write"); now warns. - /assist never recorded token usage, unlike /explore, /delegate, and /locate; DiagnoseAsync now returns usage like its siblings. - /compact didn't reset PrevCtxEstimate, so the next /context showed a confusing negative delta against pre-compaction usage. - A plan with duplicate step numbers silently dropped a step with no warning; now flagged at plan-capture time. - Removed a dead, no-op reflection pass left over from a refactor. - /mcp add split stdio arguments on bare spaces with no quoting, breaking any argument containing a space; added a quote-aware split.
compact_context/get_context_status are load-bearing for the main agent's own context-budget self-management, so they stay default-on. current/list/read_event_log/read_log let the model read a *different* session's full event log by ID/prefix match — real cross-session data exposure with no turn-to-turn value for the primary loop — so they're withheld from the default set and handed only to /assist's diagnose loop, which previously ran with no tools at all.
The main agent's system prompt told it to use repl_session_* tools to inspect session metadata, list past sessions, or read log files — but the prior ReplSessionPlugin split withheld exactly those tools from the default set, leaving only compact_context/get_context_status. Point the prompt at the two tools it actually still has.
Judge a third thing alongside correctness/completeness: whether the response is right-sized for what the user actually asked for, not just the plan step's own (possibly drifted) description. Threads the original /plan <task> text through as ctx.CurrentPlanRequest so step reviews have the real ask to compare against, not a paraphrase. Live-tested against grok-4-1-fast-reasoning: an over-scoped response (unrequested extra files, unrelated git commits) is reliably rejected citing the added scope. The first rubric wording also flagged a plain write-then-verify (write_file + read_file) as scope creep, which would have fought the main agent's own required post-write verification step — added an explicit carve-out so confirmation actions aren't penalized. Re-verified 3x after the fix: control approves, over-scope still rejects.
/safe-mode on only disabled the Shell/Git/Http category-key buckets, so once --plugins Extended was enabled, tools like git_push, git_reset, and shell_run_background lived under the separate "Extended" bucket and stayed callable through safe mode. Add ReplSessionContext.PassesSafeMode(toolName), which blocks a tool by its owning plugin (PluginCapabilityMap.GetPlugin) rather than by which category dictionary key holds it - the same reach /tools restrict already had. Combined with the existing capability-restriction check via a new IsToolAllowed. This also means safe-mode never touches CapabilityRestrictions, so a prior /tools restrict on Shell/Git/Http now survives /safe-mode on/off untouched instead of needing a save/restore dance. FileSystem-owned Extended tools (delete_file, copy_file, etc.) are left alone - safe-mode has never claimed to touch FileSystem.
Captures the tmux workflow for driving fuseraft repl interactively from outside - injecting single/multi-line input, polling for the idle prompt instead of blind-sleeping, capturing pane output, and verifying results independently rather than trusting the agent's own summary. Written up after using this exact procedure to have a live REPL session fix the /safe-mode Extended-bucket gap.
Was missing from the "Shipped skills" list added alongside the skill itself in 9e0dbe5.
REPL audit: close eleven engineering gaps, safe-mode Extended-bucket fix, tmux driver skill
Automatic function invocation drives multiple model round trips within one streaming enumeration; each round's text was appended directly onto the previous round's with no separator, so consecutive rounds' narration ran together mid-sentence (e.g. "...the full diff.Branch tip matches main..."). Insert a paragraph break before the first text chunk of any round that followed a tool call. Also reword the 20-round iteration-cap warning: it named itself a "tool-call limit" but counts LLM round-trips, not tool calls — a round with no tool call (pure narration) still consumes the cap, so the visible tool-call badge count is routinely lower than the limit even when it's hit. Drop "tool-call" from the wording to match the (correct) step-turn cap message's phrasing.
BackgroundJob.IsRunning was based solely on Process.HasExited, with no
synchronization against the async stdout/stderr reader task — so
GetJobStatus/GetJobOutput could report a job [COMPLETED]/[FAILED] with
output that hadn't finished draining yet, especially for fast-exiting
commands. Reachable in live sessions, not just tests: an agent polling
shell_job_status/shell_job_output on a quick background command could see
a false "no output" or truncated result.
Add BackgroundJob.EnsureDrainedAsync, which awaits the reader task (bounded
at 2s) once the process has exited; GetJobStatus/GetJobOutput call it before
reading job state. Verified 15/15 passes on the previously ~40%-flaky
RunBackgroundAsync_StartsJobAndReportsCompletion test after the fix.
Also add a build.cake Clean step that force-deletes the known bin/obj trees
(root obj/, src/bin, src/obj, tests/FuseraftCli.Tests/{bin,obj}) that
dotnet clean alone doesn't reliably clear.
ConsoleHumanApprovalService prompts via AnsiConsole/Console.ReadLine, but the VS Code panel drives the REPL over a JSON-line stdio bridge: its parser silently drops non-JSON stdout, and its stdin only ever carries the extension's own JSON messages (never a plain "y"). ReplCommand.cs always built the console-based service even under --vscode, so under /hitl on every shell command's approval prompt was invisible to the webview and could never be answered. Add JsonBridgeHumanApprovalService, used in jsonMode instead: it emits an approval_request event and blocks for a matching approval_response reply via ReplJsonBridge.ReadApprovalResponse (failing closed on anything malformed or on EOF). The paired fuseraft-vscode change renders the request as an inline Allow/Deny card and relays the response back over stdin. Live-verified against real grok-4.5 over the actual --vscode stdio protocol: both allow and deny paths surface and resolve correctly with no hangs.
On Windows there's no way to deliver a real SIGINT to a child process, so the
VS Code extension sends the interrupt as an in-band {"type":"interrupt"}
stdin line instead (ReplPanelProvider.ts). But ReplJsonBridge.ReadInput() was
only ever called from inside the main turn loop, once per turn boundary — so
an interrupt line written while a turn was mid-stream just sat unread in the
pipe until the turn finished on its own. By then ctx.ActiveCts was already
null, so the queued interrupt was silently discarded next time around:
clicking Stop mid-response did nothing on Windows.
Replaced the single-shot Console.ReadLine() calls with ReplStdinPump, a
background loop that owns stdin for the life of a JSON-bridge session and
acts on an interrupt line the instant it arrives, independent of whatever
the main loop is currently awaiting. Non-interrupt lines (turn input,
approval responses) are forwarded through a channel that ReadInputAsync/
ReadApprovalResponseAsync consume from instead.
Verified live: on the old code, sending the interrupt after the first
streamed token let a 200-line counting task run to completion untouched
(no 'cancelled' event, ever). With the fix, 'cancelled' arrives within
~20ms on both the Windows-style stdin path and the existing Unix SIGINT
path (unaffected, confirmed still working).
The VS Code webview protocol table in cli-reference.md only listed a handful of the event types the CLI actually emits, and covered the VS Code -> CLI direction with just user_input, omitting approval_response and interrupt entirely. Filled in the missing events/fields and added a paragraph explaining how turn cancellation actually works (SIGINT on Unix, in-band stdin message + ReplStdinPump on Windows) — the mechanism behind the Stop-button fix.
The 20-round ChatIterationLimit was hit routinely on any broad scaffolding task, and FunctionInvokingChatClient silently strips tools on the forced last iteration, letting the model ramble through a text-only wrap-up round. That rambling then glued together with no separator, because the paragraph-break fix from 78edb6b only armed on a FunctionCallContent chunk — a round boundary can also occur via UsageContent/FinishReason alone (a tool-less forced round, or a malformed tool call that never surfaces as a valid FunctionCallContent), and those boundaries never got a break. Arm it on any round boundary instead. Cline (MistakeTracker) and Codex (guardian consecutive-denial cap) both solve the underlying problem differently: stop after a short streak of consecutive failures (both default to 3), not a flat round count, so a long successful tool chain never trips an arbitrary ceiling. Ported the same idea: track consecutive tool failures from each FunctionResultContent's own content (PluginResult's bracketed tags, shell's "[EXIT n]", or a hard .Exception), reset the streak on any success, and break out of the streaming loop before requesting another round once MaxConsecutiveToolFailures (3) is hit — no forced tool-stripping, no rambling, and progress made so far is still finalized as a normal turn. ChatIterationLimit is raised to 50 and now exists only as a backstop against a turn that keeps succeeding at unproductive calls forever. Threaded the same signal into /execute step turns.
--- updated-dependencies: - dependency-name: Microsoft.Agents.AI.Workflows dependency-version: 1.20.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Updated Microsoft.Agents.AI.Workflows from 1.17.0 to 1.20.0.
Release notes
Sourced from Microsoft.Agents.AI.Workflows's releases.
1.20.0
What's Changed
New Contributors
Full Changelog: microsoft/agent-framework@dotnet-1.19.0...dotnet-1.20.0
1.19.0
What's Changed
New Contributors
Full Changelog: microsoft/agent-framework@dotnet-1.18.0...dotnet-1.19.0
1.18.0
What's Changed
New Contributors
Full Changelog: microsoft/agent-framework@dotnet-1.17.0...dotnet-1.18.0
Commits viewable in compare view.
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)