Design philosophy
The workspace isHarnessAgent’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:
All workspace config files are optional. Every file has a fully equivalent API counterpart: you can pass the same configuration via builder methods (Agent evolution — everything the agent learns or accumulates across sessions — is stored automatically in the workspace with no explicit lifecycle management required:.sysPrompt(...),.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.
- Long-term memory (
MEMORY.md+memory/) — facts extracted from conversations, maintained and compacted by background tasks, loaded as reference context per call. - 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 history — Native Session Log holds execution facts/checkpoints;
agents/<agentId>/sessions/JSONL remains a compatibility message view.
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.
Separate file artifacts from recovery state. Plans, skills and memory are Workspace files. AgentState is structured runtime state: default EVENT_LOG checkpoints it in the native Session Log, using a reserved Filesystem partition or custom backend. LEGACY use the separate AgentStateStore for recovery.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:
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.
IsolationScope selects the Filesystem bucket. Native history additionally usesFor multi-user applications, share a Builder and build an Agent per request. Configure identities, isolation and authorization explicitly; see Instance lifecycle. 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.(userId, stableAgentId, sessionId); legacy AgentStateStore uses(userId, sessionId). Migrate data when changing routing or backends.
Workspace directory layout
This tree is a logical layout, not a fixed on-disk path. It is drawn asOnly.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.
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.md - Drop in subagent specs →
subagents/ - Install skills →
skills/ - Enable Plan Mode →
plans/ - Any
call()run →agents/<agentId>/
Builder configuration
Workspace resolution order
Whenworkspace(...) is not called explicitly, build() resolves the workspace directory with
the following priority (highest first):
The system property and environment variable exist mainly for image packaging / container
deployment: keep the path out of application code and inject it at image-build or container-start
time. For example:
A blank value (e.g. " ") is treated as unset and falls through to the next level.
Minimum AGENTS.md skeleton:
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 configuredAbstractFilesystem, 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.
How workspace materials enter requests
Workspace materials load once per Agent call. The final context compiler places instructions and reference data separately rather than appending all files to System.
maxContextTokens defaults to 8000 for workspace material preparation, not the final input cap.
The final budget also includes System, history, state and tool schemas; unresolved overflow rejects
the request. MEMORY.md is excluded when both memory tools and hooks are disabled.
Knowledge loads the entry and index; other files are read on demand.
Write ordinary Markdown in AGENTS.md. Files are not automatically refreshed within a call;
the next call reloads them. See Context management
for message examples, dynamic sources and budgeting.
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:
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 identifies the current user. Each request instance built from the shared Builder uses it to select the corresponding workspace data.
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:
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
One Agent definition can serve multiple tenants. Each request instance built from the shared Builder loads workspace configuration for its user. You maintain one binary and common definition, with per-user customization on top:
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
- 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.
DockerFilesystemSpec / K8s / E2B / AgentRun) — projection + hydrate
- 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.
LocalFilesystemSpec or no filesystem(...)) — direct read / write
- 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
In default EVENT_LOG mode, AgentState checkpoints and execution facts live in native history; file artifacts and long-term memory keep their own lifecycles.Agent state and native history
Calls with the same identity/backend restore committed checkpoints and applicable facts. Shared Workspace Filesystems can also provide distributed native logs if they offer genuine atomic CAS. Sandboxes and unsupported backends need an explicit log store. See Session logs and recovery.Compatibility session files
Memory (long-term)
Two layers:- 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 loaded per call and included as budgeted reference context.
- 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:
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:
Memory, skills, plans and offloaded results follow Workspace isolation/routing. Native history additionally requires atomic storage and reserves its commit structure; compatibility JSONL cannot replace full execution records.
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.
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)
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.
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():
- 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").
agents/<agentId>/
This is the runtime root, framework-written and rarely hand-edited:
This tree shows task records. Native Session Log lives in the reserved Filesystem partition: localShare native history, task/file artifacts and sandbox metadata as appropriate across replicas. Configuring shared AgentStateStore alone does not share a local native log. See Session logs and Filesystem..agentscope-runtime/or remote__agentscope_session_log_v1__. Read history withsessionTranscript(),session_historyandsession_list; legacy JSONL, sessions.json and segmented transcripts are no longer written.
knowledge/
- 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.
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.
Related Pages
- Architecture — how the system prompt is assembled and how capabilities cooperate
- Filesystem — where the workspace physically lives (local / sandbox / shared store),
IsolationScope, multi-user isolation - Context —
AgentStateandAgentStateStorepersistence, cross-node recovery - Memory — how
MEMORY.md/memory/are produced and maintained, compaction, eviction - Skills — four-layer composition, self-learning loop, the
<available_skills>block - Subagent —
subagents/declarations, sync vs background, stream forwarding - Plan Mode —
plans/files, read-only phase, HITL exit