Repository navigation
Expand file tree
/
Copy pathLocalFilesystemPersonalAssistantExampleTest.java
More file actions
214 lines (193 loc) · 9.4 KB
/
Copy pathLocalFilesystemPersonalAssistantExampleTest.java
File metadata and controls
214 lines (193 loc) · 9.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/*
* Copyright 2024-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.agentscope.harness.agent.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.MsgRole;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.model.ChatResponse;
import io.agentscope.core.model.Model;
import io.agentscope.harness.agent.HarnessAgent;
import io.agentscope.harness.agent.filesystem.local.LocalFilesystemWithShell;
import io.agentscope.harness.agent.testing.HarnessQuiescence;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import reactor.core.publisher.Flux;
/**
* Example: Fully local personal-assistant mode using {@link LocalFilesystemWithShell}.
*
* <h2>Context</h2>
* <p>This mode is ideal for a <em>personal local assistant</em> running on a developer's machine
* or in a single-process environment:
* <ul>
* <li>All file I/O goes directly to a local workspace directory on disk.</li>
* <li>Shell commands execute in that directory via {@code ProcessBuilder}.</li>
* <li>No sandbox container, no distributed store, no external dependencies.</li>
* <li>State persists naturally as files on disk between calls.</li>
* <li>Changing the {@code userId} or {@code sessionId} in the {@link RuntimeContext} does
* <em>not</em> redirect I/O to a different location — the workspace directory is always
* the same.</li>
* </ul>
*
* <h2>Trade-offs</h2>
* <ul>
* <li><b>Pro</b>: Zero infrastructure, instant setup, full local control.</li>
* <li><b>Con</b>: No isolation between users or sessions, no horizontal scaling.</li>
* </ul>
*
* <p>Configure via {@link HarnessAgent.Builder#abstractFilesystem} with a
* {@link LocalFilesystemWithShell} instance pointing at your desired workspace directory.
*/
@HarnessQuiescence
class LocalFilesystemPersonalAssistantExampleTest {
@TempDir Path workspace;
/**
* Demonstrates that files written to the workspace during one call persist to disk and are
* readable in subsequent calls — the most fundamental characteristic of the local mode.
*/
@Test
void localFilesystem_filesPersistAcrossCalls() throws Exception {
Files.createDirectories(workspace);
// Build the agent with a LocalFilesystemWithShell store.
// No distributed store, no sandbox — all operations go straight to disk.
try (HarnessAgent agent =
HarnessAgent.builder()
.name("my-local-assistant")
.model(stubModel("done"))
.workspace(workspace.toAbsolutePath().normalize().toString())
.abstractFilesystem(new LocalFilesystemWithShell(workspace))
.build()) {
// Call 1: write a note through the workspace manager. A plain user file is
// used on purpose: MEMORY.md is agent-managed (memory consolidation rewrites
// it asynchronously after calls), so its bytes are not stable across calls.
agent.call(userMsg("first call"), ctx("session-1", "alice")).block();
agent.getWorkspaceManager()
.writeUtf8WorkspaceRelative(
RuntimeContext.empty(), "notes.md", "# Notes\n- item 1");
// The file exists on disk after call 1
Path notesFile = workspace.resolve("notes.md");
assertTrue(
Files.isRegularFile(notesFile), "notes.md should exist on disk after call 1");
String content = Files.readString(notesFile, StandardCharsets.UTF_8);
assertTrue(content.contains("item 1"), "notes.md content should be persisted on disk");
// Call 2: same workspace, different session — file is still there
agent.call(userMsg("second call"), ctx("session-2", "alice")).block();
assertTrue(
Files.isRegularFile(notesFile),
"notes.md should still exist on disk in call 2");
assertEquals(
content,
Files.readString(notesFile, StandardCharsets.UTF_8),
"notes.md content should be unchanged after call 2");
}
}
/**
* Demonstrates that changing {@code userId} or {@code sessionId} does NOT redirect I/O to a
* different location in local mode.
*
* <p>This is the key distinction from sandbox/remote modes: in local mode the workspace
* directory is fixed, so all users and sessions share the same disk location.
*/
@Test
void localFilesystem_workspaceIsNotPartitionedByUserOrSession() throws Exception {
Files.createDirectories(workspace);
try (HarnessAgent agent =
HarnessAgent.builder()
.name("my-local-assistant")
.model(stubModel("done"))
.workspace(workspace.toAbsolutePath().normalize().toString())
.abstractFilesystem(new LocalFilesystemWithShell(workspace))
.build()) {
// Alice writes during her session
agent.call(userMsg("alice here"), ctx("session-alice", "alice")).block();
agent.getWorkspaceManager()
.writeUtf8WorkspaceRelative(
RuntimeContext.empty(), "shared.txt", "alice was here");
// Bob calls with a different userId — still reads the same workspace
agent.call(userMsg("bob here"), ctx("session-bob", "bob")).block();
Path sharedFile = workspace.resolve("shared.txt");
assertTrue(
Files.isRegularFile(sharedFile),
"shared.txt written by alice should be visible in the same workspace, "
+ "regardless of userId or sessionId");
}
}
/**
* Demonstrates that the underlying workspace directory is a plain local filesystem path —
* you can read and write files with standard Java I/O alongside the agent.
*/
@Test
void localFilesystem_directDiskAccessFromHostProcess() throws Exception {
Files.createDirectories(workspace);
try (HarnessAgent agent =
HarnessAgent.builder()
.name("my-local-assistant")
.model(stubModel("done"))
.workspace(workspace.toAbsolutePath().normalize().toString())
.abstractFilesystem(new LocalFilesystemWithShell(workspace))
.build()) {
// Write a file from the host process (simulating a user placing a document in the
// workspace)
Path doc = workspace.resolve("document.txt");
Files.writeString(doc, "Host-written document content");
// The agent can see the file through its workspace manager
agent.call(userMsg("check document"), ctx("s1", "user")).block();
String read =
agent.getWorkspaceManager()
.readManagedWorkspaceFileUtf8(RuntimeContext.empty(), "document.txt");
assertNotNull(
read, "agent should be able to read files written directly to the workspace");
assertTrue(read.contains("Host-written"), "agent should see the host-written content");
}
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
private static RuntimeContext ctx(String sessionId, String userId) {
return RuntimeContext.builder().sessionId(sessionId).userId(userId).build();
}
private static Msg userMsg(String text) {
return Msg.builder()
.role(MsgRole.USER)
.content(TextBlock.builder().text(text).build())
.build();
}
private static Model stubModel(String text) {
Model model = mock(Model.class);
when(model.getModelName()).thenReturn("stub-model");
ChatResponse chunk =
new ChatResponse(
"stub-id",
List.of(TextBlock.builder().text(text).build()),
null,
Map.of(),
"stop");
when(model.stream(anyList(), any(), any())).thenReturn(Flux.just(chunk));
return model;
}
}