Skip to content

Bump Microsoft.Agents.AI.Workflows from 1.17.0 to 1.20.0 - #98

Open
dependabot[bot] wants to merge 554 commits into
mainfrom
dependabot/nuget/src/Microsoft.Agents.AI.Workflows-1.20.0
Open

Bump Microsoft.Agents.AI.Workflows from 1.17.0 to 1.20.0#98
dependabot[bot] wants to merge 554 commits into
mainfrom
dependabot/nuget/src/Microsoft.Agents.AI.Workflows-1.20.0

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Sep 7, 2026

Copy link
Copy Markdown
Contributor

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 compatibility score

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 rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will 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 version will 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 dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Scott Stauffer 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.
Scott Stauffer and others added 26 commits September 5, 2026 22:43
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>
@dependabot dependabot Bot added .NET Pull requests that update .NET code dependencies Pull requests that update a dependency file labels Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file .NET Pull requests that update .NET code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants