Skip to content
Open
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
16 changes: 13 additions & 3 deletions agentscope-examples/agents/agentscope-builder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,19 @@ builder:

## Filesystem Modes

Builder supports three filesystem modes that control how per-(user, agent) workspaces are backed. Set via `builder.workspace-store.fs-spec`.
Builder supports three explicit filesystem modes that control how per-(user, agent) workspaces are backed. Set via `builder.workspace-store.fs-spec`.

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 README says "Builder supports three explicit filesystem modes" but there are actually four values: auto, local, sandbox, remote. Consider updating to "four filesystem modes" or "three explicit modes plus auto-detection".


### Local Mode (default)
### Auto Mode (default)

```yaml
builder:
workspace-store:
fs-spec: auto
```

`auto` preserves the deployment-aware behavior: `InMemoryAgentStateStore` and `JsonFileAgentStateStore` select local mode, while a distributed `AgentStateStore` selects remote mode. Set an explicit mode when you need to override this detection.

### Local Mode

```yaml
builder:
Expand Down Expand Up @@ -256,7 +266,7 @@ Per-agent sandbox config (`sandbox.mode` / `sandbox.scope`) is metadata stored w
| `BUILDER_MODEL_NAME` | `builder.dashscope.model-name` | `qwen-max` | Model name |
| `BUILDER_WORKSPACE` | `builder.workspace` | (JVM cwd) | Working directory |
| `BUILDER_JWT_SECRET` | `builder.jwt.secret` | (dev default) | JWT signing secret |
| `BUILDER_WORKSPACE_FS_SPEC` | `builder.workspace-store.fs-spec` | `local` | Filesystem mode |
| `BUILDER_WORKSPACE_FS_SPEC` | `builder.workspace-store.fs-spec` | `auto` | Filesystem mode (`auto`, `local`, `sandbox`, or `remote`) |
| `BUILDER_SANDBOX_ENABLED` | `builder.sandbox.enabled` | `false` | Enable sandbox |
| `BUILDER_SANDBOX_IMAGE` | `builder.sandbox.image` | `agentscope/python-sandbox:py311-slim` | Sandbox Docker image |
| `BUILDER_SANDBOX_ISOLATION` | `builder.sandbox.isolation` | `USER` | Sandbox isolation scope |
Expand Down
16 changes: 13 additions & 3 deletions agentscope-examples/agents/agentscope-builder/README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,19 @@ builder:

## 文件系统模式

Builder 通过 `builder.workspace-store.fs-spec` 切换三种文件系统模式,控制 per-(用户, agent) workspace 的存储后端。
Builder 通过 `builder.workspace-store.fs-spec` 切换三种显式文件系统模式,控制 per-(用户, agent) workspace 的存储后端。

### 本地模式(默认)
### Auto 模式(默认)

```yaml
builder:
workspace-store:
fs-spec: auto
```

`auto` 保留按部署拓扑自动选择的行为:使用 `InMemoryAgentStateStore` 或 `JsonFileAgentStateStore` 时选择本地模式;提供分布式 `AgentStateStore` 时选择 Remote 模式。需要覆盖自动判断时,可以显式指定其他模式。

### 本地模式

```yaml
builder:
Expand Down Expand Up @@ -235,7 +245,7 @@ per-agent 的 sandbox 配置(`sandbox.mode` / `sandbox.scope`)当前只是
| `BUILDER_MODEL_NAME` | `builder.dashscope.model-name` | `qwen-max` | 模型名 |
| `BUILDER_WORKSPACE` | `builder.workspace` | (JVM cwd) | 工作目录 |
| `BUILDER_JWT_SECRET` | `builder.jwt.secret` | (开发默认) | JWT 签名密钥 |
| `BUILDER_WORKSPACE_FS_SPEC` | `builder.workspace-store.fs-spec` | `local` | 文件系统模式 |
| `BUILDER_WORKSPACE_FS_SPEC` | `builder.workspace-store.fs-spec` | `auto` | 文件系统模式(`auto` / `local` / `sandbox` / `remote`) |
| `BUILDER_SANDBOX_ENABLED` | `builder.sandbox.enabled` | `false` | 是否启用 sandbox |
| `BUILDER_SANDBOX_IMAGE` | `builder.sandbox.image` | `agentscope/python-sandbox:py311-slim` | Sandbox Docker 镜像 |
| `BUILDER_SANDBOX_ISOLATION` | `builder.sandbox.isolation` | `USER` | Sandbox 隔离粒度 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@
* activity/activity-{timestampMs}.jsonl # rotated chunks (most recent first by name)
* </pre>
*
* <p>All paths live under the {@code activity/} prefix because the harness composite filesystem
* (see {@link io.agentscope.builder.web.config.BuilderConfig}) registers {@code activity/} as an
* additional shared prefix via {@code RemoteFilesystemSpec.addSharedPrefix("activity/")}. Writes
* therefore land on the shared {@link BaseStore} (visible across
* pods) rather than on per-pod local disk — critical for a multi-tenant deployment.
* <p>All paths live under the {@code activity/} prefix. In remote mode, the harness composite
* filesystem (see {@link io.agentscope.builder.web.config.BuilderConfig}) registers that path via
* {@code RemoteFilesystemSpec.addSharedPrefix("activity/")}, so writes land on the shared {@link
* BaseStore} and remain visible across pods. Local and sandbox modes keep the activity log in their
* selected workspace backend.
*
* <p>Writes go through the per-agent {@link HarnessAgent#workspaceFor(String, String)} view, which
* runs in the owner's namespace ({@code IsolationScope.USER}) so the audit trail is keyed to the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import io.agentscope.harness.agent.filesystem.remote.store.BaseStore;
import io.agentscope.harness.agent.filesystem.spec.LocalFilesystemSpec;
import io.agentscope.harness.agent.filesystem.spec.RemoteFilesystemSpec;
import io.agentscope.harness.agent.filesystem.spec.SandboxFilesystemSpec;
import io.agentscope.harness.agent.gateway.channel.ChannelConfig;
import io.agentscope.harness.agent.gateway.channel.DmScope;
import io.agentscope.harness.agent.gateway.channel.chatui.ChatUiChannel;
Expand Down Expand Up @@ -60,15 +61,12 @@
*
* <h2>Filesystem topology</h2>
*
* <p>Builder is a multi-tenant distributed deployable, so every {@link
* io.agentscope.harness.agent.HarnessAgent} (the global main agent and every user-custom agent)
* runs with a {@link RemoteFilesystemSpec} composite filesystem: the per-pod
* {@code LocalFilesystem} only carries shared/read-only template content, while the {@code
* memory/}, {@code MEMORY.md}, {@code sessions/}, {@code tasks/}, {@code skills/} and {@code
* subagents/} routes are persisted through a {@link BaseStore}-backed {@code RemoteFilesystem}
* shared across pods. The {@link BaseStore} bean is therefore <em>required</em> — operators must
* provide one (e.g. SQLite/JDBC/Redis-backed); a missing bean fails context refresh fast with a
* clear message.
* <p>The {@code builder.workspace-store.fs-spec} property selects the filesystem used by every
* {@link io.agentscope.harness.agent.HarnessAgent}: {@code local}, {@code sandbox}, or {@code
* remote}. The default {@code auto} mode preserves the topology inferred from the configured
* {@link AgentStateStore}: local state stores use {@link LocalFilesystemSpec}, while distributed
* stores use a {@link RemoteFilesystemSpec} backed by {@link BaseStore}. Sandbox mode uses the
* optional {@link SandboxFilesystemSpec} contributed when {@code builder.sandbox.enabled=true}.
*
* <h2>Model wiring (priority order)</h2>
*
Expand Down Expand Up @@ -113,6 +111,9 @@ public class BuilderConfig {
@Value("${builder.workspace:${claw.workspace:}}")
private String workspaceDir;

@Value("${builder.workspace-store.fs-spec:${claw.workspace-store.fs-spec:auto}}")
private String workspaceFilesystemMode;

// -----------------------------------------------------------------
// Model bean — only created when an api-key is set AND no other
// Model bean is already present in the context. The conditional
Expand Down Expand Up @@ -168,21 +169,23 @@ public BaseStore baseStore(DataSource dataSource) {
* Assembles the {@link BuilderBootstrap}, loading agent config from {@code agentscope.json} and
* starting the {@link ChatUiChannel} for per-user isolated sessions.
*
* <p>Every agent built by the bootstrap is wired with a {@link RemoteFilesystemSpec} (per-user
* isolation scope) backed by the shared {@link BaseStore}, so memory/sessions/tasks/skills/
* subagents files persist across pods.
* <p>Every agent built by the bootstrap receives the filesystem selected by {@code
* builder.workspace-store.fs-spec}. The default {@code auto} mode uses a local filesystem for
* in-process state stores and a remote filesystem for distributed state stores.
*
* @param modelOpt the {@link Model} to use, or empty if none is configured
* @param toolEventBus the shared tool-event bus for real-time SSE streaming of tool calls
* @param baseStore the shared {@link BaseStore} that backs every agent's {@code
* RemoteFilesystem} routes; <em>required</em> — operators must provide a bean.
* @param baseStore the shared {@link BaseStore} used when remote filesystem mode is selected
* @param sessionOpt an optional state store; defaults to {@link InMemoryAgentStateStore}
* @param sandboxFilesystemSpecOpt the sandbox filesystem created when sandbox support is enabled
*/
@Bean
public BuilderBootstrap builderBootstrap(
Optional<Model> modelOpt,
ToolEventBus toolEventBus,
BaseStore baseStore,
Optional<AgentStateStore> sessionOpt)
Optional<AgentStateStore> sessionOpt,
Optional<SandboxFilesystemSpec> sandboxFilesystemSpecOpt)
throws IOException {
Path cwd = resolveCwd();
ensureAgentscopeConfig();
Expand All @@ -208,32 +211,27 @@ public BuilderBootstrap builderBootstrap(
AgentStateStore.class.getName());
}

// RemoteFilesystemSpec requires a distributed AgentStateStore; when the effective store is
// local (InMemory/JsonFile), use LocalFilesystemSpec instead so the harness won't reject
// the topology at build time.
boolean localStore = isLocalStateStore(stateStore);
if (localStore) {
log.info(
"Effective AgentStateStore is local ({}); using LocalFilesystemSpec.",
stateStore.getClass().getSimpleName());
} else {
log.info(
"Effective AgentStateStore is distributed ({}); using RemoteFilesystemSpec.",
stateStore.getClass().getSimpleName());
}
FilesystemMode filesystemMode = resolveFilesystemMode(workspaceFilesystemMode, stateStore);
SandboxFilesystemSpec sandboxFilesystemSpec =
filesystemMode == FilesystemMode.SANDBOX
? sandboxFilesystemSpecOpt.orElseThrow(
() ->
new IllegalStateException(
"builder.workspace-store.fs-spec=sandbox requires "
+ "builder.sandbox.enabled=true or a "
+ "SandboxFilesystemSpec bean"))
: null;

log.info(
"Using {} filesystem mode with AgentStateStore {}.",
filesystemMode.configValue(),
stateStore.getClass().getSimpleName());

builder.configureAllAgents(
b -> {
b.middleware(new ToolNotificationMiddleware(toolEventBus));
b.stateStore(stateStore);
if (localStore) {
b.filesystem(new LocalFilesystemSpec().isolationScope(IsolationScope.USER));
} else {
b.filesystem(
new RemoteFilesystemSpec(baseStore)
.isolationScope(IsolationScope.USER)
.addSharedPrefix("activity/"));
}
configureFilesystem(b, filesystemMode, baseStore, sandboxFilesystemSpec);
});

BuilderBootstrap bootstrap = builder.build();
Expand Down Expand Up @@ -295,6 +293,65 @@ private static boolean isLocalStateStore(AgentStateStore store) {
return store instanceof InMemoryAgentStateStore || store instanceof JsonFileAgentStateStore;
}

static FilesystemMode resolveFilesystemMode(String configuredMode, AgentStateStore stateStore) {
FilesystemMode mode = FilesystemMode.parse(configuredMode);
if (mode == FilesystemMode.AUTO) {
return isLocalStateStore(stateStore) ? FilesystemMode.LOCAL : FilesystemMode.REMOTE;
}
if (mode == FilesystemMode.REMOTE && isLocalStateStore(stateStore)) {
throw new IllegalStateException(
"builder.workspace-store.fs-spec=remote requires a distributed "
+ "AgentStateStore; found "
+ stateStore.getClass().getSimpleName());

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] The method validates REMOTE + localStateStore → throws, but does not warn for LOCAL + distributedStateStore. In multi-pod deployments, state is distributed while workspace is local, which could cause file inconsistency. Consider adding a WARN log:

if (mode == FilesystemMode.LOCAL && !isLocalStateStore(stateStore)) {
    log.warn("fs-spec=local with distributed AgentStateStore ({}); "
        + "workspace files will NOT be shared across pods.",
        stateStore.getClass().getSimpleName());
}

}
return mode;
}

static void configureFilesystem(
io.agentscope.harness.agent.HarnessAgent.Builder builder,
FilesystemMode mode,
BaseStore baseStore,
SandboxFilesystemSpec sandboxFilesystemSpec) {
switch (mode) {
case LOCAL ->
builder.filesystem(
new LocalFilesystemSpec().isolationScope(IsolationScope.USER));
case SANDBOX -> builder.filesystem(sandboxFilesystemSpec);
case REMOTE ->
builder.filesystem(
new RemoteFilesystemSpec(baseStore)

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] In the REMOTE branch of configureFilesystem(), baseStore is passed directly to new RemoteFilesystemSpec(baseStore) without a null check. While baseStore() bean is always created currently (via @ConditionalOnMissingBean), a future change adding conditional annotations could cause an NPE in REMOTE mode. Consider adding a defensive null check:

case REMOTE -> {
    if (baseStore == null) {
        throw new IllegalStateException(
            "REMOTE mode requires a BaseStore bean...");
    }
    builder.filesystem(new RemoteFilesystemSpec(baseStore)...);
}

.isolationScope(IsolationScope.USER)
.addSharedPrefix("activity/"));
case AUTO ->
throw new IllegalStateException(
"AUTO filesystem mode must be resolved before configuring agents");
}
}

enum FilesystemMode {
AUTO,
LOCAL,
SANDBOX,
REMOTE;

static FilesystemMode parse(String raw) {
String value = raw == null || raw.isBlank() ? "auto" : raw.trim();
try {
return valueOf(value.toUpperCase(java.util.Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Unknown builder.workspace-store.fs-spec value '"
+ raw
+ "'. Expected one of auto, local, sandbox, remote.",
e);
}
}

String configValue() {
return name().toLowerCase(java.util.Locale.ROOT);
}
}

private Path resolveCwd() {
if (workspaceDir != null && !workspaceDir.isBlank()) {
return Paths.get(workspaceDir).toAbsolutePath().normalize();
Expand Down
Loading
Loading