Skip to content

feat(middleware): add final answer filter for ReAct streams - #2926

Merged
jujn merged 2 commits into
agentscope-ai:mainfrom
wmh123wmh1:feat/final-answer-filter-middleware
Sep 2, 2026
Merged

feat(middleware): add final answer filter for ReAct streams#2926
jujn merged 2 commits into
agentscope-ai:mainfrom
wmh123wmh1:feat/final-answer-filter-middleware

Conversation

@wmh123wmh1

Copy link
Copy Markdown
Contributor

AgentScope-Java Version

2.0.3-SNAPSHOT

Description

ReAct streaming currently exposes text from intermediate reasoning rounds as well as the final answer, making it difficult for applications to consume only the final user-facing text.

This PR adds an opt-in FinalAnswerFilterMiddleware that:

  • Buffers text events for each model call.
  • Suppresses buffered text when the round produces a tool call.
  • Emits buffered text when the round completes without a tool call.
  • Preserves tool, thinking, control, and other non-text events.
  • Keeps filtering state isolated per subscription.

Because a round can only be classified after observing whether it produces a tool call, final-round text is emitted when the corresponding model call ends.

Tests include:

  • Final-round text emission.
  • Intermediate-round text suppression.
  • Non-text event forwarding.
  • Per-subscription state isolation.
  • A two-round ReAct integration test with a real tool execution.

English and Chinese middleware documentation have also been updated.

Closes #2872

Checklist

  • Code has been formatted with mvn spotless:apply
  • All tests are passing (mvn test)
  • Javadoc comments are complete and follow project conventions
  • Related documentation has been updated
  • Code is ready for review

@CLAassistant

CLAassistant commented Sep 1, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.00000% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...e/core/middleware/FinalAnswerFilterMiddleware.java 82.00% 4 Missing and 5 partials ⚠️

📢 Thoughts on this report? Let us know!

@jujn
jujn merged commit bb0f796 into agentscope-ai:main Sep 2, 2026
6 checks passed
Buktal pushed a commit that referenced this pull request Sep 2, 2026
…rdown to stop flaky temp-dir deletion (#2935)

### Problem

Two CI runs fail intermittently with JUnit errors that are not
test-logic failures but teardown failures:

- `Failed to delete temp directory` (Windows, PR #2923)
- `Failed to close extension context` / `Failed to delete temp
directory` (Ubuntu, PR #2926)

Both surface only when a test builds a transient `HarnessAgent`, drives
it to completion via `.block()` / `.stream()...block()`, and uses
`@TempDir` for its workspace/state home.

### Root cause

This is a long-standing race, not a regression of any single commit. The
harness memory flush has always been asynchronous: when an agent stream
completes, `MemoryFlushMiddleware#onAgent` dispatches the flush on
`Schedulers.boundedElastic()` via `subscribe(...)` — i.e.
fire-and-forget. The calling test's `.block()` only waits for the
business stream, **not** for that background flush.

So the timeline is:

1. Test calls `.block()` and returns.
2. JUnit begins `@TempDir` teardown and deletes the temp directory.
3. The async flush on `boundedElastic` is **still writing
session/transcript mirror files into that same `@TempDir`** (or still
holds open file handles).

The result: directory/file deletion fails with `IOException` → wrapped
as `JUnitException`. It is timing-dependent (depends on IO speed,
scheduler, and how many files are written), which is why it flakes
rather than failing deterministically, and why Windows (stricter file
locking) fails more readily than Linux.

The normal production path avoids this because `HarnessAgent#close()`
calls `SessionTree.awaitMirrorQuiescence(...)` +
`MemoryBackgroundTasks.awaitQuiescence(...)`. The flaky tests never call
`close()` on their transient agent.

### Fix

Add test-side quiescence that mirrors what `HarnessAgent#close()`
already does, run **after each test method but before the `TempDir`
extension deletes the directory**:

- `HarnessBackgroundTaskQuiescenceExtension` — an `AfterEachCallback`
that calls `SessionTree.awaitMirrorQuiescence(5s)` +
`MemoryBackgroundTasks.awaitQuiescence(5s)`. When nothing is in flight
both calls return immediately, making it a no-op for tests that never
trigger a flush.
- `@HarnessQuiescence` — a composed meta-annotation
(`@ExtendWith(HarnessBackgroundTaskQuiescenceExtension.class)`) so
at-risk tests only need a one-line annotation.
- Applied `@HarnessQuiescence` to 16 harness test classes that match the
at-risk pattern (`HarnessAgent.builder()` + `.call()`/`.stream()` +
`@TempDir`), including the three classes that flaked in CI:
`JsonSessionDefaultLocationTest`, `HarnessAgentIntegrationExampleTest`,
`HarnessAgentDynamicHookBuilderTest`.

Production code is unchanged — the flush remains fire-and-forget so
conversation completion is never blocked.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Distinguish the final-round answer from intermediate reasoning text in ReAct streaming events

3 participants