Workspace¶
Design philosophy¶
The workspace is HarnessAgent’s source of truth for agent definition and evolution. Everything that defines what the agent is, and everything the agent learns over time, lives here as a directory of plain Markdown / JSON files — not scattered in code, not pinned to a particular database table.
Four guiding ideas:
1. Source of truth for both agent definition and long-term evolution.
Agent definition — who the agent is and how it behaves — can be declared entirely in the workspace:
What to define |
File |
|---|---|
Persona, behavior rules, system instructions |
|
Domain knowledge |
|
Skills (reusable capability packages) |
|
Subagent declarations |
|
Tool allowlist + MCP servers |
|
All workspace config files are optional. Every file has a fully equivalent API counterpart: you can pass the same configuration via builder methods (
.systemPrompt(...),.skill(SkillDeclaration...),.subagent(SubagentDeclaration...),.toolsConfig(...), etc.). The workspace and the API are always in parity — which one you use is entirely your choice.Why the workspace, then? Because expressing definition as files (rather than code) is what makes one agent natively multi-tenant: the same agent logic can carry a different persona, knowledge base, and skill set per user, just by dropping a per-user override directory — no code branches, no separate deployments. See One agent logic, customized per user below.
Agent evolution — everything the agent learns or accumulates across sessions — is stored automatically in the workspace with no explicit lifecycle management required:
Long-term memory (
MEMORY.md+memory/) — facts extracted from conversations, maintained and compacted by background tasks, injected each turn.Self-learning skills (
skills/) — the agent drafts new skills from successful patterns; after an optional review gate they become reusable capabilities, then a background curator ages out / archives the unused ones.Plans (
plans/) — plans written during Plan Mode persist and survive across calls, keeping “figure it out” decoupled from “do it”.Offloaded tool results (compaction) — oversized tool outputs are written to disk and replaced in-context with a head/tail preview + a
read_filepointer, so the agent can re-read them later without bloating the prompt.Session logs (
agents/<agentId>/sessions/) — the full never-compacted conversation log, queryable at any time.
Evolution data is long-lived by default: memory accumulates indefinitely, session logs are append-only and never purged automatically. How each channel is produced and maintained is detailed in How the agent evolves below.
(The volatile per-call runtime context — AgentState — is not part of this list: it is the resume snapshot for an in-flight conversation, persisted separately in the AgentStateStore, never in the workspace. See the callout under idea 2 below.)
2. Content splits into three lifecycles, kept distinct.
Kind |
Written by |
Read by |
Examples |
|---|---|---|---|
Static assets (engineer-edited) |
You / your team |
Framework injects into the system prompt each turn, or reads on demand at call time |
|
Runtime files (rewritten on every call) |
Framework / agent |
Framework restores them on the next call |
|
Long-term memory (accumulated across sessions) |
Agent + background tasks |
Framework injects into the system prompt + agent queries via tools |
|
They live in one tree purely for deployment convenience (copy a directory, get a complete agent). Inside the framework they travel different read/write paths.
AgentStateis not workspace content — don’t conflate the two. The in-flight context an agent needs to resume mid-conversation (conversation buffer, rolling summary, permission / tool / task / Plan-Mode sub-contexts, plus the metadata pointing at workspace artifacts such as the active plan file) is serialized as a singleAgentStatedocument into theAgentStateStore, a separate subsystem (default~/.agentscope/state/<agentId>/, fully outside the workspace tree). The split is deliberate: the workspace holds the durable file artifacts (the never-compacted session log, plan markdown, task records, memory), whileAgentStateholds the volatile runtime context + workspace metadata. Two stores, two lifecycles — see Context.
3. Natively multi-tenant. Workspace data (memory, sessions, tasks, skills, sandbox state) is bucketed by a single IsolationScope — no application-level partitioning code. The scope decides who shares one bucket:
|
Who shares one bucket |
Typical use |
|---|---|---|
|
each |
per-conversation isolation; disposable sandboxes |
|
all sessions of the same |
a user’s sessions share long-term memory / skills (falls back to |
|
all users & sessions of this agent |
shared-knowledge-base agent |
|
one bucket for the whole store instance |
use with care — every agent/user competes for the same slot |
The chosen scope materializes differently per filesystem mode (path prefix on local disk, KV namespace in a shared store, sandbox state slot in a sandbox). Full semantics, fallback rules, and concurrency notes in Filesystem — IsolationScope.
IsolationScopegoverns the workspace/filesystem buckets above.AgentStatehas its own, orthogonal addressing: it is always keyed by(userId, sessionId)in theAgentStateStore, regardless of scope.
A single HarnessAgent instance can serve thousands of concurrent users with zero cross-user data leakage.
4. Workspace decouples from filesystem. The same directory layout lands in one of three places: local disk, shared KV store (Redis / JDBC), or sandbox container. This decoupling is what lets you switch deployment shape without touching agent code. See Filesystem for the three modes.
Workspace directory layout¶
.agentscope/workspace/
├── AGENTS.md ← static: persona + behavior rules
├── MEMORY.md ← long-term: curated long-term facts
├── tools.json ← static: MCP servers + tool allow/deny (optional)
├── memory/ ← long-term: append-only daily fact log
│ └── YYYY-MM-DD.md
├── knowledge/ ← static: knowledge entry + reference files
│ ├── KNOWLEDGE.md
│ └── ...
├── skills/ ← static: one subdir per skill, each with a SKILL.md
│ └── <skill-name>/SKILL.md
├── subagents/ ← static: subagent specs (filename = agent_id)
│ └── <agent-id>.md
├── plans/ ← runtime: plan files written in Plan Mode
│ └── PLAN.md
└── agents/<agentId>/ ← runtime: each agent's runtime root
├── sessions/ ← runtime: session index + never-compacted log
│ ├── sessions.json
│ └── <sessionId>.log.jsonl
└── tasks/ ← runtime: subagent background task records
└── <sessionId>.json
This tree is a logical layout, not a fixed on-disk path. It is drawn as
.agentscope/workspace/..., but that is only the default local placement. The exact same layout can physically live on local disk, in a remote distributed store (Redis / JDBC / OSS, viaRemoteFilesystemSpec), or be projected into a sandbox container (SandboxFilesystemSpec) — the relative paths below are identical across all three, only the backing store changes, and your agent code does not. Pick the backing store with Filesystem; everything in this document is written against the logical layout.
Only AGENTS.md is something you actually need to write (skip it and the agent still runs — you just lose the persona injection). Everything else appears as you turn on the matching capability:
Enable memory compaction (
.compaction(...)) →memory/+MEMORY.mdDrop in subagent specs →
subagents/Install skills →
skills/Enable Plan Mode →
plans/Any
call()run →agents/<agentId>/
Builder configuration¶
HarnessAgent agent = HarnessAgent.builder()
.name("MyAgent")
.model(model)
.workspace(Paths.get(".agentscope/workspace")) // omit → ${user.dir}/.agentscope/workspace
.additionalContextFile("SOUL.md") // any workspace-relative path, inlined in full
.additionalContextFile("PREFERENCES.md")
.maxContextTokens(8000) // MEMORY injection budget
.build();
Minimum AGENTS.md skeleton:
# MyAgent
You are an XX assistant. Follow these behavior guidelines.
## Behavior
- ...
- ...
Opt-out switches (rare in production, useful for debugging or self-management):
Method |
What it disables |
|---|---|
|
system-prompt injection ( |
|
memory flush + background maintenance |
|
|
|
the entire subagent subsystem |
|
per-turn skill re-merge; falls back to one-shot merge at build time |
|
reading |
|
AgentState auto-persistence |
How workspace content gets loaded¶
Because the workspace is a logical layout (see the callout above), “loading” never assumes a plain local directory — every read goes through the configured AbstractFilesystem, so the same logic works whether files sit on local disk, in a remote store, or inside a sandbox. The two-layer read below is what makes that backing-store independence concrete; Filesystem covers how each mode resolves paths physically.
System-prompt assembly per turn¶
Before every reasoning step, WorkspaceContextMiddleware (io.agentscope.harness.agent.middleware) assembles the following sections and appends them to the sysPrompt you set on the builder to form the final system message:
Section |
Source |
Budgeted |
|---|---|---|
|
Template (today’s date, OS, workspace absolute path, temp dir, current |
no |
|
Built-in templates (teach the model how to use memory + navigate knowledge) |
no |
|
Template, branches per filesystem mode (see below) — tells the model whether it runs locally / sandboxed / on a remote store |
no |
|
Framework auto-loads the following files from the workspace into a |
see below |
|
Full |
unlimited |
|
|
|
|
Full |
unlimited (filenames only as the catalog) |
|
Anything you added with |
unlimited |
Key points:
Re-assembled every turn. Edit
AGENTS.mdorMEMORY.mdand the nextcall()picks up the change — no restart, no rebuild.MEMORY.mdis token-estimated before injection. Overflow truncates by character count with a trailing note that nudges the model towardmemory_search.knowledge/is a directory index + entry file. The full tree never enters the prompt — onlyKNOWLEDGE.mdplus a listing of paths; the agent reads what it needs withread_file.
Two-layer reads (filesystem-first + local fallback)¶
For every “file injected into the prompt” (AGENTS.md / MEMORY.md / knowledge/KNOWLEDGE.md / additionalContextFile), WorkspaceManager.readWithOverride() does a two-layer read:
1. Ask the configured AbstractFilesystem: do you have this relative path?
├─ yes → return that content (the "override" layer)
└─ no → fall through to step 2
2. Read local disk at workspace.resolve(relativePath)
Writes always go through layer 1 (the filesystem store), never directly to local disk.
This pattern earns its keep in shared-store mode: the first replica starts with the team-git-synced AGENTS.md template available on local disk, so it works immediately; later any override (e.g. from an admin console editor) lands in the shared KV, and every replica’s next call() reads the latest version. Template is fallback, remote override is truth.
Override precedence with multiple users sharing one workspace¶
RuntimeContext.userId is the multi-user key — it lets one agent instance serve many users without crosstalk.
For runtime data (sessions / tasks / memory), the framework prefixes paths via the configured NamespaceFactory (local-mode → path prefix, remote-mode → KV namespace, sandbox-mode → state slot). Details in the next section, “How runtime data and memory are stored”.
For static assets (notably skills/ and subagents/), a per-user directory overrides the workspace-shared version:
workspace/
├── skills/code-reviewer/SKILL.md ← shared (visible to everyone)
├── subagents/researcher.md ← shared
└── alice/
├── skills/
│ └── code-reviewer/
│ └── SKILL.md ← only visible to alice; overrides shared
└── subagents/
└── researcher.md ← only visible to alice
When called with RuntimeContext.userId="alice", the framework looks in alice/skills/code-reviewer/ first and falls back to skills/code-reviewer/. Skills unique to a lower layer remain visible; only same-name conflicts are shadowed by the higher layer. Full precedence table in Skills — Conflict resolution.
One agent logic, customized per user¶
This override mechanism is what lets a single HarnessAgent instance behave like a different agent for every tenant — without forking code or spinning up separate deployments. You ship one binary, one agent definition; each user gets their own slice on top:
Per-user layer |
What it customizes |
Resolution |
|---|---|---|
|
persona / behavior for that user |
upper layer of the two-layer read (shared |
|
domain knowledge that user is allowed to see |
per-user directory, shared |
|
capabilities only that user unlocks |
overrides same-name shared skills; unique ones stack |
|
sub-agents only that user can spawn |
overrides same-name shared specs |
runtime data (memory / sessions / tasks) |
that user’s accumulated evolution |
namespaced per |
The result is two layers of multi-tenancy at once: the definition differs per user (via override directories), and the evolution is isolated per user (via namespacing). A shared base stays common to everyone, and each user’s customizations and learned state never leak across tenants — all from the same agent process. This is the file-based payoff the optional-config callout refers to: because definition is data, per-user customization is just another file, not another code path.
Loading behavior under each filesystem mode¶
The workspace is a logical layout; physical placement is up to Filesystem. The same directory loads differently depending on mode — illustrated below.
Mode 1 · Shared store (RemoteFilesystemSpec) — template + remote override
HarnessAgent agent = HarnessAgent.builder()
.name("store")
.model(model)
.workspace(workspace)
.distributedStore(store)
.filesystem(new RemoteFilesystemSpec()
.isolationScope(IsolationScope.USER)) // namespace per userId
.build();
How it loads: at each turn,
AGENTS.md/MEMORY.md/tools.jsonare served by an overlay with the remote KV as the upper layer and the workspace template as the read-only lower layer. The local<workspace>/AGENTS.mdis a read-only seed — used at first boot or to sync across replicas; if the remote KV has a per-user copy under the same key, the remote wins.Routing:
memory//skills//subagents//knowledge//agents/<id>/sessions//agents/<id>/tasks/are namespaced perIsolationScope(default USER → one namespace peruserId; see Filesystem — IsolationScope).Best practice: git-sync the team-agreed
AGENTS.md/knowledge// sharedskills/to every replica’s local disk as the template; let runtime outputs (MEMORY.md,memory/,agents/<id>/...) accrete in the KV.
Mode 2 · Sandbox (DockerFilesystemSpec / K8s / E2B / AgentRun) — projection + hydrate
HarnessAgent agent = HarnessAgent.builder()
.name("sandbox")
.model(model)
.workspace(workspace)
.filesystem(new DockerFilesystemSpec()
.image("ubuntu:24.04")
.isolationScope(IsolationScope.SESSION))
.build();
How it loads: when the sandbox starts, the framework tars the workspace’s “static assets” (
AGENTS.md,skills/,subagents/,knowledge/, plus other projection roots) and hydrates them into/workspaceinside the container.AGENTS.mdetc. still follow the two-layer read (sandbox first, host template fallback).Dedup & incremental: projections are compared by content hash; unchanged → skip; changed files are rewritten incrementally with SHA-256.
Runtime data:
MEMORY.md,memory/,agents/<id>/...all live inside the sandbox; sandbox snapshots preserve them — the nextcall()with the samesessionIdrestoresnode_modules,pip installresults, and everything else.Best practice: keep code execution / shell out of the host. The host only carries the workspace “seed” (team-git-synced persona + shared skills + knowledge). This is the default mode for running untrusted code in production.
Mode 3 · Local + shell (default LocalFilesystemSpec or no filesystem(...)) — direct read / write
HarnessAgent agent = HarnessAgent.builder()
.name("local")
.model(model)
.workspace(workspace)
// omit .filesystem(...) = local + shell
.build();
How it loads: all files are read directly from
<workspace>/; no overlay. Per-user overrides like<userId>/skills/are simple directory-prefix switching.Path safety: default
ROOTEDmode — absolute paths are only allowed under theworkspaceandproject(shellcwd) roots;..traversal is rejected by the path policy.Best practice: single process / local dev / unit tests / trusted env. Do not run untrusted code here in production —
executeis hostsh -c.
How runtime data and memory are stored¶
The framework writes two data planes automatically — and they live in two different places. Keep them distinct:
Data plane |
What it is |
Where it lives |
|---|---|---|
|
volatile runtime context: chat buffer, compaction summary, permission / tool / task / Plan-Mode contexts, plus metadata pointing at workspace artifacts |
the |
Workspace runtime/long-term files |
durable artifacts: session logs, task records, |
inside the workspace tree, physical location follows the filesystem mode |
You don’t hand-edit either. The rest of this section walks the two planes in turn.
Agent state — a separate store, not in the workspace¶
AgentState is the per-(userId, sessionId) runtime context, and it is deliberately kept out of the workspace tree. When a call() completes, it is serialized to JSON and persisted via the configured AgentStateStore, addressed by the call’s (userId, sessionId). The next call() with the same (userId, sessionId) loads it back.
By default HarnessAgent uses a JsonFileAgentStateStore rooted outside the workspace at ~/.agentscope/state/<agentId>/ (override the base via the agentscope.state.home system property), so runtime state stays decoupled from workspace data. Configure another store via .stateStore(...).
Session logs (these are workspace files)¶
Distinct from AgentState, the workspace holds the conversation logs under agents/<agentId>/sessions/:
sessions.json— the agent’s session index (key = sessionId, value = summary + updatedAt).<sessionId>.log.jsonl— the never-compacted raw conversation log, append-only.session_search/session_historyquery it.
The default
JsonFileAgentStateStoreis single-machine only. Multi-replica production must switch to a distributed store (RedisAgentStateStore/MysqlAgentStateStore/ …). If you have configuredfilesystem(SandboxFilesystemSpec)orfilesystem(RemoteFilesystemSpec)without swapping in a distributed state store,build()raisesIllegalStateException— a forced reminder not to make runtime state a single point of failure.
Full details (recovery flow, cross-node continuation, (userId, sessionId) addressing) live in Context.
Memory (long-term)¶
Two layers:
workspace/
├── MEMORY.md ← curated long-term memory, injected each turn
└── memory/
└── YYYY-MM-DD.md ← append-only daily fact log (no dedup)
Write path:
Before compaction,
MemoryFlushMiddlewareextracts new facts from the prefix of the conversation intomemory/YYYY-MM-DD.md(append).A throttled background task periodically merges/dedups
memory/and rewritesMEMORY.md.MEMORY.mdis injected (budgeted) into the system prompt every turn.
Read path:
Framework reads
MEMORY.mditself (two-layer; filesystem first).Agent can actively call
memory_search/memory_getfor older entries. See Memory.
How namespace isolation maps to physical location¶
WorkspaceManager.resolveRuntimeDataPath() asks the NamespaceFactory what namespace the current RuntimeContext maps to. The namespace then materializes per filesystem mode:
Mode |
Physical location of runtime data |
Multi-user isolation mechanism |
|---|---|---|
Local + shell |
|
path prefix |
Shared store (KV) |
KV key prefix, e.g. |
KV namespace |
Sandbox |
sandbox state slot key (with |
sandbox instance isolation |
Without userId, single-tenant default applies and everyone shares one root.
Static assets vs runtime data:
AGENTS.md,tools.json,knowledge/and friends are not auto-partitioned per userId — they are shared across users, and the only way to differentiate is to add per-user override directories (<userId>/skills/...,<userId>/subagents/...). What followsuserIdis runtime data (sessions, tasks, memory).
How the agent evolves¶
Beyond its static definition, the workspace is where the agent’s accumulated experience lands. Five channels accrue automatically — turn on the matching capability and the data starts piling up in the workspace, isolated per tenant exactly like everything else. Each has its own deep-dive page; this table is the index:
Channel |
Where it lives |
Turn it on |
How it accrues |
Deep dive |
|---|---|---|---|---|
Long-term memory |
|
|
|
|
Self-learning skills |
|
|
the agent calls |
|
Plans |
|
|
a read-only planning phase writes the plan via |
|
Offloaded tool results |
the eviction directory under the workspace |
|
when a single tool result exceeds the threshold (default 80K chars), the full output is written to disk and the in-context message is replaced with a head/tail preview + a |
|
Session logs |
|
on by default |
every |
The unifying idea: the agent improves between runs without you wiring up any storage. Memory, skills, plans, session logs, and offloaded results are all just files in the workspace — they get the same per-tenant isolation, the same two-layer reads, and the same filesystem-mode portability as everything else on this page. (The volatile AgentState runtime context is the one exception — it lives in the separate AgentStateStore, not the workspace; see How runtime data and memory are stored.)
Deep dive on key directories¶
skills/¶
A skill is a packaged capability — a directory containing SKILL.md (description + instructions for the agent), optionally with reference docs and scripts.
skills/code-reviewer/
├── SKILL.md ← YAML frontmatter (name + description) + instructions
├── references/style-guide.md ← optional, agent reads on demand
└── scripts/run-checks.sh ← optional, agent invokes via execute_shell_command
There are four registration layers (low → high priority):
projectGlobalSkillsDir(Path)— project global, e.g.~/.agentscope/skills/skillRepository(...)— marketplace stores (Git / Nacos / MySQL / classpath)workspace/skills/— workspace shared<userId>/skills/— per-user (overrides all above)
Unique skills at a lower layer remain visible; same-name skills are shadowed by the higher layer. Each turn, DynamicSkillMiddleware re-merges and renders an <available_skills> block (name + description only) into the system prompt. The agent calls load_skill_through_path to pull full details when relevant. Full mechanics in Skills.
subagents/¶
Each <agent-id>.md is a subagent declaration (filename = agent_id). YAML frontmatter describes identity, model, tool allowlist, workspace strategy; body is the subagent’s system prompt.
---
description: Code review specialist. Use when the user needs a PR review, style feedback, or static checks.
workspace:
mode: isolated # isolated (default) | shared
model: qwen3-max # optional; defaults to inheriting the parent
tools: [read_file, grep_files] # optional; inherited-tool allowlist
---
You are a code review subagent…
Loading: AgentSpecLoader non-recursively scans workspace/subagents/*.md at build time and merges with any declarations you registered programmatically via .subagent(SubagentDeclaration...). The main agent invokes them via agent_spawn agent_id="reviewer" task="...".
Full details (sync vs background, remote subagents, stream forwarding, task storage) in Subagent.
tools.json¶
A JSON file at the workspace root, read once during build():
{
// allowlist: when non-empty, only listed tools survive
"allow": ["read_file", "grep_files", "execute"],
// denylist: listed tools are always removed (wins over allow)
"deny": ["write_file"],
// MCP servers, keyed by name
"mcpServers": {
"amap": {
"transport": "streamableHttp",
"url": "https://mcp.amap.com/mcp?key=${AMAP_API_KEY}"
},
"local-py": {
"transport": "stdio",
"command": "python",
"args": ["mcp_servers/my_server.py"],
"env": {"PYTHONUNBUFFERED": "1"}
}
}
}
Behavior notes:
MCP servers are registered into the toolkit once at build time; the agent sees the tools they expose.
allow/denyare applied after every tool has been registered — including Harness built-ins (read_file/memory_search/agent_spawn/ …). When you useallowto whitelist, list the built-ins you want to keep too, otherwise they get filtered out alongside everything else.${ENV_VAR}syntax substitutes environment variables; missing variables warn and substitute the empty string.Don’t want a file? Pass
builder.toolsConfig(ToolsConfig.builder()...)directly, or fully disable reading withdisableToolsConfig().Under shared-store mode,
tools.jsonfollows the same “remote upper, local-template lower” overlay described above.
plans/¶
Plan files written in Plan Mode land here. Default plans/PLAN.md, changeable via .planFileDirectory("design-docs").
plans/
└── PLAN.md ← current plan written by plan_write
Note: PlanModeContext (whether the plan phase is active, current plan file path) lives in AgentState — it is runtime state, persisted via the AgentStateStore (by default ~/.agentscope/state/<agentId>/, outside the workspace). The files under plans/ are only the markdown content itself. See Plan Mode.
agents/<agentId>/¶
This is the runtime root, framework-written and rarely hand-edited:
agents/<agentId>/
├── sessions/
│ ├── sessions.json ← session index for this agent
│ └── <sessionId>.log.jsonl ← never-compacted raw conversation log (append-only)
└── tasks/
└── <sessionId>.json ← subagent background task records (taskId → TaskRecord)
The serialized
AgentState(agent_state) is not in the workspace by default — it lives in the configuredAgentStateStore(default~/.agentscope/state/<agentId>/). Only the conversation logs and task records above stay in the workspace.
For cross-node recovery / multi-replica deployments this data must be shared (either RedisAgentStateStore + RemoteFilesystemSpec, or sandbox with distributed state). See Context and Filesystem.
knowledge/¶
knowledge/
├── KNOWLEDGE.md ← entry / overview, injected in full into the system prompt
├── api-reference.md
├── domain-terms.md
└── ...
At load time:
The full
KNOWLEDGE.mdgoes into<domain_knowledge_context>.Other files under the same tree (any depth) only contribute their path listing to the prompt; the agent reads them on demand with
read_file/grep_files/glob_files.
This “details on disk, index in the prompt” pattern keeps token budget bounded even with a large knowledge base.
Safety rules for writing to the workspace¶
additionalContextFile, writeUtf8WorkspaceRelative, memory_get, and friends accept workspace-relative paths. The framework does basic path-traversal validation (refusing ../../etc/passwd and similar escapes).
When you need to write files, go through HarnessAgent#getWorkspaceManager(), not java.nio.Files — the latter writes to the wrong place under sandbox or shared-store modes (it lands on the host disk rather than inside the sandbox / in the KV). Exception: builder-time bootstrap scripts (e.g. an initWorkspaceIfAbsent that seeds AGENTS.md) — there is no runtime context yet, and java.nio.Files is correct because the intent is to write the local template.