Skip to content

Commit 505d6cb

Browse files
committed
fix(harness): normalize memory glob paths
1 parent 28ef28a commit 505d6cb

3 files changed

Lines changed: 106 additions & 51 deletions

File tree

agentscope-harness/src/main/java/io/agentscope/harness/agent/memory/MemoryConsolidator.java

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.agentscope.harness.agent.filesystem.model.FileInfo;
2525
import io.agentscope.harness.agent.filesystem.model.GlobResult;
2626
import io.agentscope.harness.agent.workspace.WorkspaceManager;
27+
import java.nio.file.Path;
2728
import java.time.Instant;
2829
import java.util.ArrayList;
2930
import java.util.Comparator;
@@ -194,27 +195,30 @@ private String readDailyEntries(Instant watermark) {
194195
return "";
195196
}
196197

197-
List<FileInfo> eligible = new ArrayList<>();
198+
List<String> eligible = new ArrayList<>();
198199
for (FileInfo fi : glob.matches()) {
199200
if (fi.isDirectory()) {
200201
continue;
201202
}
202-
String name = fileName(fi.path());
203+
String rel = workspaceManager.toWorkspaceRelativePath(fi.path());
204+
if (rel.isBlank()) {
205+
continue;
206+
}
207+
String name = fileName(rel);
203208
if (name.equals(STATE_FILE) || name.equals("archive") || !name.endsWith(".md")) {
204209
continue;
205210
}
206211
if (isModifiedAfter(fi, watermark)) {
207-
eligible.add(fi);
212+
eligible.add(rel);
208213
}
209214
}
210-
eligible.sort(Comparator.comparing(fi -> fileName(fi.path())));
215+
eligible.sort(Comparator.comparing(MemoryConsolidator::fileName));
211216

212217
StringBuilder sb = new StringBuilder();
213-
for (FileInfo fi : eligible) {
214-
String rel = toRelative(fi.path());
218+
for (String rel : eligible) {
215219
String content = workspaceManager.readManagedWorkspaceFileUtf8(rel);
216220
if (content != null && !content.isBlank()) {
217-
sb.append("### ").append(fileName(fi.path())).append("\n");
221+
sb.append("### ").append(fileName(rel)).append("\n");
218222
sb.append(content.strip()).append("\n\n");
219223
}
220224
}
@@ -238,23 +242,20 @@ private static String fileName(String path) {
238242
if (path == null || path.isEmpty()) {
239243
return "";
240244
}
245+
try {
246+
Path p = Path.of(path);
247+
Path name = p.getFileName();
248+
if (name != null) {
249+
return name.toString();
250+
}
251+
} catch (Exception ignored) {
252+
// Fall through to string-based parsing.
253+
}
241254
String stripped = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
242-
int idx = stripped.lastIndexOf('/');
255+
int idx = Math.max(stripped.lastIndexOf('/'), stripped.lastIndexOf('\\'));
243256
return idx >= 0 ? stripped.substring(idx + 1) : stripped;
244257
}
245258

246-
/**
247-
* Converts an absolute filesystem path (e.g. {@code /memory/2025-01-01.md}) to a
248-
* workspace-relative path ({@code memory/2025-01-01.md}) for use with
249-
* {@link WorkspaceManager#readManagedWorkspaceFileUtf8}.
250-
*/
251-
private static String toRelative(String path) {
252-
if (path == null) {
253-
return "";
254-
}
255-
return path.startsWith("/") ? path.substring(1) : path;
256-
}
257-
258259
private void writeConsolidatedMemory(String content) {
259260
workspaceManager.writeUtf8WorkspaceRelative("MEMORY.md", content);
260261
}

agentscope-harness/src/main/java/io/agentscope/harness/agent/workspace/WorkspaceManager.java

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,44 @@ public Path getWorkspace() {
149149
return workspace;
150150
}
151151

152+
/**
153+
* Normalizes a filesystem-reported path to a workspace-relative path when possible.
154+
*
155+
* <p>This handles three common cases:
156+
* <ul>
157+
* <li>already-relative workspace paths such as {@code memory/2026-05-20.md}</li>
158+
* <li>virtual/remote paths that start with {@code /}</li>
159+
* <li>local absolute paths that live under the workspace root</li>
160+
* </ul>
161+
*/
162+
public String toWorkspaceRelativePath(String path) {
163+
if (path == null || path.isBlank()) {
164+
return "";
165+
}
166+
167+
String normalized = path.strip().replace('\\', '/');
168+
Path workspaceRoot = workspace.toAbsolutePath().normalize();
169+
try {
170+
Path candidate = Path.of(path).normalize();
171+
if (candidate.isAbsolute()) {
172+
Path absoluteCandidate = candidate.toAbsolutePath().normalize();
173+
if (absoluteCandidate.startsWith(workspaceRoot)) {
174+
return workspaceRoot
175+
.relativize(absoluteCandidate)
176+
.toString()
177+
.replace('\\', '/');
178+
}
179+
}
180+
} catch (Exception ignored) {
181+
// Fall through to string-based normalization.
182+
}
183+
184+
while (normalized.startsWith("/")) {
185+
normalized = normalized.substring(1);
186+
}
187+
return normalized;
188+
}
189+
152190
/** Reads AGENTS.md content, returns empty string if not found. */
153191
public String readAgentsMd() {
154192
return readWithOverride(AGENTS_MD);
@@ -172,7 +210,7 @@ public String readManagedWorkspaceFileUtf8(String relativePath) {
172210
if (relativePath == null || relativePath.isBlank()) {
173211
return "";
174212
}
175-
String normalized = normalizeRelativePath(relativePath);
213+
String normalized = toWorkspaceRelativePath(relativePath);
176214
if (normalized.isEmpty()) {
177215
return "";
178216
}
@@ -204,7 +242,10 @@ public List<Path> listKnowledgeFiles() {
204242
if (glob.isSuccess() && glob.matches() != null) {
205243
for (FileInfo fi : glob.matches()) {
206244
if (fi.path() != null && !fi.path().isBlank()) {
207-
relativePaths.add(normalizeRelativePath(fi.path().trim()));
245+
String rel = toWorkspaceRelativePath(fi.path().trim());
246+
if (!rel.isEmpty()) {
247+
relativePaths.add(rel);
248+
}
208249
}
209250
}
210251
}
@@ -722,7 +763,7 @@ public List<String> listMemoryFilePaths() {
722763
if (glob.isSuccess() && glob.matches() != null) {
723764
for (FileInfo fi : glob.matches()) {
724765
if (fi.path() != null && !fi.path().isBlank()) {
725-
String rel = normalizeRelativePath(fi.path().trim());
766+
String rel = toWorkspaceRelativePath(fi.path().trim());
726767
if (!rel.isEmpty()) {
727768
paths.add(rel);
728769
}
@@ -759,7 +800,7 @@ public List<String> listSessionLogFiles() {
759800
if (glob.isSuccess() && glob.matches() != null) {
760801
for (FileInfo fi : glob.matches()) {
761802
if (fi.path() != null && !fi.path().isBlank()) {
762-
String rel = normalizeRelativePath(fi.path().trim());
803+
String rel = toWorkspaceRelativePath(fi.path().trim());
763804
if (!rel.isEmpty()) {
764805
paths.add(rel);
765806
}

agentscope-harness/src/test/java/io/agentscope/harness/agent/memory/MemoryConsolidatorFilesystemTest.java

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@
1818
import static org.junit.jupiter.api.Assertions.assertEquals;
1919
import static org.junit.jupiter.api.Assertions.assertFalse;
2020
import static org.junit.jupiter.api.Assertions.assertTrue;
21-
21+
import static org.mockito.ArgumentMatchers.any;
22+
import static org.mockito.ArgumentMatchers.anyList;
23+
import static org.mockito.Mockito.mock;
24+
import static org.mockito.Mockito.when;
25+
26+
import io.agentscope.core.message.TextBlock;
27+
import io.agentscope.core.model.ChatResponse;
28+
import io.agentscope.core.model.Model;
29+
import io.agentscope.harness.agent.filesystem.local.LocalFilesystem;
2230
import io.agentscope.harness.agent.filesystem.remote.RemoteFilesystem;
2331
import io.agentscope.harness.agent.store.InMemoryStore;
2432
import io.agentscope.harness.agent.workspace.WorkspaceManager;
@@ -29,11 +37,11 @@
2937
import java.util.Map;
3038
import org.junit.jupiter.api.Test;
3139
import org.junit.jupiter.api.io.TempDir;
40+
import reactor.core.publisher.Flux;
3241

3342
/**
3443
* Verifies that {@link MemoryConsolidator} reads daily ledgers and writes watermark / MEMORY.md
35-
* entirely through {@link io.agentscope.harness.agent.filesystem.AbstractFilesystem}, making it
36-
* backend-agnostic.
44+
* through the filesystem layer.
3745
*/
3846
class MemoryConsolidatorFilesystemTest {
3947

@@ -50,10 +58,6 @@ private static void seedStoreFile(
5058
store.put(ns, path, value);
5159
}
5260

53-
// ======================================================================
54-
// readWatermark: returns EPOCH when state file absent
55-
// ======================================================================
56-
5761
@Test
5862
void readWatermark_returnsEpochWhenStateAbsent(@TempDir Path tmp) {
5963
InMemoryStore store = new InMemoryStore();
@@ -66,10 +70,6 @@ void readWatermark_returnsEpochWhenStateAbsent(@TempDir Path tmp) {
6670
assertEquals(Instant.EPOCH, consolidator.readWatermark());
6771
}
6872

69-
// ======================================================================
70-
// readWatermark / writeWatermark round-trip through filesystem
71-
// ======================================================================
72-
7373
@Test
7474
void watermark_roundTripThroughFilesystem(@TempDir Path tmp) {
7575
InMemoryStore store = new InMemoryStore();
@@ -85,10 +85,6 @@ void watermark_roundTripThroughFilesystem(@TempDir Path tmp) {
8585
assertEquals(ts, consolidator.readWatermark());
8686
}
8787

88-
// ======================================================================
89-
// readWatermark: no local file is touched — only the filesystem
90-
// ======================================================================
91-
9288
@Test
9389
void watermark_doesNotCreateLocalFile(@TempDir Path tmp) {
9490
InMemoryStore store = new InMemoryStore();
@@ -101,48 +97,65 @@ void watermark_doesNotCreateLocalFile(@TempDir Path tmp) {
10197
Instant ts = Instant.now();
10298
wsm.writeUtf8WorkspaceRelative(MemoryConsolidator.STATE_REL_PATH, ts.toString());
10399

104-
// local disk must NOT have the state file — it lives only in the store
105100
Path localState = tmp.resolve("memory").resolve(MemoryConsolidator.STATE_FILE);
106101
assertFalse(
107102
Files.exists(localState),
108103
"state file should not be written to local disk when using RemoteFilesystem");
109104

110-
// but consolidator reads it correctly from the store
111105
assertEquals(ts, consolidator.readWatermark());
112106
}
113107

114-
// ======================================================================
115-
// STATE_FILE constant is preserved
116-
// ======================================================================
117-
118108
@Test
119109
void stateFileRelPath_matchesConstant() {
120110
assertEquals("memory/" + MemoryConsolidator.STATE_FILE, MemoryConsolidator.STATE_REL_PATH);
121111
}
122112

123-
// ======================================================================
124-
// Local filesystem (no store) — watermark uses local disk via WorkspaceManager
125-
// ======================================================================
113+
@Test
114+
void consolidate_readsRootDailyLedgerAndWritesMemoryMd(@TempDir Path tmp) throws Exception {
115+
LocalFilesystem fs = new LocalFilesystem(tmp);
116+
WorkspaceManager wsm = new WorkspaceManager(tmp, fs);
117+
118+
Path memoryDir = Files.createDirectories(tmp.resolve("memory"));
119+
Files.writeString(memoryDir.resolve("2026-05-20.md"), "root daily entry");
120+
121+
MemoryConsolidator consolidator = new MemoryConsolidator(wsm, stubModel("updated memory"));
122+
123+
consolidator.consolidate().block();
124+
125+
assertEquals("updated memory", wsm.readMemoryMd());
126+
assertTrue(consolidator.readWatermark().isAfter(Instant.EPOCH));
127+
}
126128

127129
@Test
128130
void watermark_localFallback_whenNoFilesystem(@TempDir Path tmp) throws Exception {
129131
WorkspaceManager wsm = new WorkspaceManager(tmp);
130132

131133
MemoryConsolidator consolidator = new MemoryConsolidator(wsm, null);
132134

133-
// No file → EPOCH
134135
assertEquals(Instant.EPOCH, consolidator.readWatermark());
135136

136-
// Write via WorkspaceManager (falls to local disk)
137137
Instant ts = Instant.parse("2025-03-10T09:00:00Z");
138138
wsm.writeUtf8WorkspaceRelative(MemoryConsolidator.STATE_REL_PATH, ts.toString());
139139

140140
assertEquals(ts, consolidator.readWatermark());
141141

142-
// Verify the local file actually exists
143142
Path localState = tmp.resolve("memory").resolve(MemoryConsolidator.STATE_FILE);
144143
assertTrue(
145144
Files.exists(localState),
146145
"state file should be written to local disk when no filesystem is configured");
147146
}
147+
148+
private static Model stubModel(String assistantText) {
149+
Model model = mock(Model.class);
150+
when(model.getModelName()).thenReturn("stub-model");
151+
ChatResponse chunk =
152+
new ChatResponse(
153+
"stub-id",
154+
List.of(TextBlock.builder().text(assistantText).build()),
155+
null,
156+
Map.of(),
157+
"stop");
158+
when(model.stream(anyList(), any(), any())).thenReturn(Flux.just(chunk));
159+
return model;
160+
}
148161
}

0 commit comments

Comments
 (0)