Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.List;
import java.util.stream.Collectors;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

/**
* Appends workspace context (session info, AGENTS.md, MEMORY.md, knowledge) to the
Expand Down Expand Up @@ -181,14 +182,15 @@ public boolean isDisableMemoryHooks() {

@Override
public Mono<String> onSystemPrompt(Agent agent, RuntimeContext ctx, String currentPrompt) {
RuntimeContext rc = ctx != null ? ctx : RuntimeContext.empty();
String section = buildWorkspaceSection(rc);
if (section.isEmpty()) {
return Mono.just(currentPrompt);
}
String base = currentPrompt != null ? currentPrompt : "";
String separator = base.isEmpty() || base.endsWith("\n") ? "" : "\n";
return Mono.just(base + separator + section);
return Mono.fromCallable(
() -> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Removed empty-section early-return guard. The original code had if (section.isEmpty()) return Mono.just(currentPrompt) which short-circuited when no workspace files existed. The new code always concatenates the section (even if empty), which may append an unnecessary trailing newline. Consider restoring the isEmpty guard inside the callable.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Removed empty-section early-return guard. The original code had if (section.isEmpty()) return Mono.just(currentPrompt) which short-circuited when no workspace files existed. The new code always concatenates the section (even if empty), which may append an unnecessary trailing newline. Consider restoring the isEmpty guard inside the callable.

RuntimeContext rc = ctx != null ? ctx : RuntimeContext.empty();
String base = currentPrompt != null ? currentPrompt : "";
String section = buildWorkspaceSection(rc);
String separator = base.isEmpty() || base.endsWith("\n") ? "" : "\n";
return base + separator + section;
})
.subscribeOn(Schedulers.boundedElastic());
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Missing onErrorResume for graceful degradation. buildWorkspaceSection() performs multiple filesystem reads. If any throws, the Mono.fromCallable will propagate the error through the middleware chain, failing the entire agent call. For a supplementary-context middleware, this is overly aggressive. Consider adding .onErrorResume(e -> { log.warn(...); return Mono.just(currentPrompt); }) after subscribeOn to degrade gracefully.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Missing onErrorResume for graceful degradation. buildWorkspaceSection() performs multiple filesystem reads. If any throws, the Mono.fromCallable will propagate the error through the middleware chain, failing the entire agent call. For a supplementary-context middleware, this is overly aggressive. Consider adding .onErrorResume(e -> { log.warn(...); return Mono.just(currentPrompt); }) after subscribeOn to degrade gracefully.


private String buildWorkspaceSection(RuntimeContext rc) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.agentscope.core.agent.RuntimeContext;
Expand All @@ -25,6 +26,7 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
Expand Down Expand Up @@ -52,6 +54,45 @@ private WorkspaceManager track(WorkspaceManager wm) {

@TempDir Path workspace;

@Test
void onSystemPromptBuildsWorkspaceContextOnBoundedElastic() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The test assertion assertFalse(callerThread[0]) is somewhat fragile — it only checks that the caller thread differs from the current thread at assertion time. If the test happens to run on the bounded-elastic scheduler itself, the assertion would still pass but the intent (offloading to a different scheduler) would be better captured by checking the thread name contains 'boundedElastic'.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The test assertion assertFalse(callerThread[0]) is somewhat fragile — it only checks that the caller thread differs from the current thread at assertion time. If the test happens to run on the bounded-elastic scheduler itself, the assertion would still pass but the intent (offloading to a different scheduler) would be better captured by checking the thread name contains 'boundedElastic'.

Thread callerThread = Thread.currentThread();
AtomicReference<Thread> readThread = new AtomicReference<>();
WorkspaceManager wm =
track(
new WorkspaceManager(workspace) {
@Override
public String readAgentsMd(RuntimeContext rc) {
readThread.set(Thread.currentThread());
return "agent persona";
}
});
WorkspaceContextMiddleware mw = new WorkspaceContextMiddleware(wm);

String prompt = mw.onSystemPrompt(null, RuntimeContext.empty(), "BASE\n").block();

assertNotNull(prompt);
assertTrue(prompt.contains("agent persona"));
assertNotNull(readThread.get());
assertNotSame(
callerThread, readThread.get(), "workspace context read ran on caller thread");
}

@Test
void onSystemPromptHandlesNullAndNonNewlineBasePrompts() {
WorkspaceManager wm = track(new WorkspaceManager(workspace));
WorkspaceContextMiddleware mw = new WorkspaceContextMiddleware(wm);

String promptWithoutBase = mw.onSystemPrompt(null, null, null).block();
String promptWithBase = mw.onSystemPrompt(null, RuntimeContext.empty(), "BASE").block();

assertNotNull(promptWithoutBase);
assertFalse(promptWithoutBase.startsWith("null"));
assertTrue(promptWithoutBase.contains("## Domain Knowledge"));
assertNotNull(promptWithBase);
assertTrue(promptWithBase.startsWith("BASE\n"));
}

@Test
void defaultFlags_includeMemoryRecallPersistenceAndContext() throws Exception {
Files.writeString(workspace.resolve("MEMORY.md"), "remember: cats prefer windowsills");
Expand Down
Loading