Harness Overview¶
agentscope-harness builds a production-grade runtime infrastructure on top of agentscope-core’s ReActAgent, through two extension channels: Hooks and Toolkits. Your entry point is one class: HarnessAgent.
A bare ReActAgent handles a single request–reason–tool–reply cycle. Harness answers a different set of questions: what happens on the next turn, what happens the next day, what happens when context overflows, what happens when state is lost, what happens when a task is too heavy. It does not replace the reasoning loop — it injects hooks at critical lifecycle points and provides a baseline set of tools, packaging the default engineering answers to these questions.
Quick Start¶
Add the dependency:
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-harness</artifactId>
<version>${agentscope.version}</version>
</dependency>
The example below demonstrates three core values of Harness: workspace-driven persona, session persistence (a second turn with the same sessionId remembers the first), and explicit conversation compaction. On first run, AGENTS.md is generated under ${cwd}/.agentscope/workspace/; subsequent runs reuse it.
public class QuickstartExample {
public static void main(String[] args) throws Exception {
// 1. Prepare workspace: generate AGENTS.md on first run, reuse afterwards
Path workspace = Paths.get(".agentscope/workspace");
initWorkspaceIfAbsent(workspace);
// 2. Build model
Model model = DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-max")
.stream(true)
.build();
// 3. Build HarnessAgent: workspace injection, session persistence, and trace logging
// are enabled by default; compaction is explicitly configured here
HarnessAgent agent = HarnessAgent.builder()
.name("quickstart-agent")
.sysPrompt("You are a note-taking assistant.")
.model(model)
.workspace(workspace)
.compaction(CompactionConfig.builder()
.triggerMessages(30)
.keepMessages(10)
.flushBeforeCompact(true) // extract facts to daily log before compacting
.build())
.build();
// 4. Two conversation turns with the same RuntimeContext
// Same sessionId → turn 2 auto-restores state from turn 1
RuntimeContext ctx = RuntimeContext.builder()
.sessionId("demo-session")
.userId("alice")
.build();
Msg turn1 = agent.call(
Msg.builder().role(MsgRole.USER)
.textContent("My name is Alice, and I'm preparing a tech talk on ReAct today.")
.build(),
ctx).block();
System.out.println("[turn1] " + turn1.getTextContent());
Msg turn2 = agent.call(
Msg.builder().role(MsgRole.USER)
.textContent("What is my name? What am I doing today?")
.build(),
ctx).block();
System.out.println("[turn2] " + turn2.getTextContent());
}
private static void initWorkspaceIfAbsent(Path workspace) throws Exception {
Files.createDirectories(workspace);
Path agentsMd = workspace.resolve("AGENTS.md");
if (Files.exists(agentsMd)) return;
Files.writeString(agentsMd, """
# Note-taking Assistant
You are an assistant that helps users organize notes and knowledge.
## Behavior Guidelines
- Actively record key facts the user mentions (names, plans, preferences, etc.)
- Reply concisely, using bullet lists when helpful
- For uncertain information, say so clearly rather than guessing
""");
}
}
Full runnable version: agentscope-examples/harness-example/src/main/java/io/agentscope/harness/example/QuickstartExample.java
Run:
export DASHSCOPE_API_KEY=your_key_here
# First run: install dependency modules to local repo (skip javadoc and spotless)
mvn -pl agentscope-examples/harness-example -am install \
-DskipTests -Dspotless.check.skip=true -Dmaven.javadoc.skip=true -q
# Execute main
mvn -pl agentscope-examples/harness-example exec:java \
-Dexec.mainClass=io.agentscope.harness.example.QuickstartExample \
-Dspotless.check.skip=true -q
What to observe after running:
.agentscope/workspace/AGENTS.mdis auto-created — this is the source of the agent’s personaTurn 2 “What is my name?” gets the right answer because the second
call()with the samesessionId=demo-sessionauto-restored turn 1’s state viabindRuntimeContextat the startAfter a few more turns that trigger compaction (≥30 messages), you can see LLM-extracted facts in
workspace/memory/YYYY-MM-DD.md; the backgroundMemoryMaintenanceSchedulerwill continue merging these intoMEMORY.mdOn the next process restart, as long as
sessionIdis unchanged, the agent still remembers everything
About RuntimeContext: it is the identity carrier for the current call(). sessionId determines the storage path and log archive location; userId determines the default filesystem namespace (natural multi-tenant isolation). It is not persisted — it is only shared between hooks and tools within the current call.
Extension directions: place KNOWLEDGE.md, skills/*/SKILL.md, or subagents/*.md in the workspace to enable domain knowledge injection, skill loading, and subagent orchestration respectively. Add .toolResultEviction(ToolResultEvictionConfig.defaults()) to enable large-result offloading. Use Filesystem — Three Declarative Modes to choose between shared storage, sandbox, or local+shell for where files and commands land. For isolated execution prefer filesystem(SandboxFilesystemSpec) (see Sandbox); abstractFilesystem is only an escape hatch for self-managed backends.
Core Capabilities¶
Each capability answers one problem → one component:
Workspace context injection — answers where does the agent’s identity come from. Before every reasoning turn,
WorkspaceContextHookinjectsAGENTS.md,MEMORY.md, today’s memory, andKNOWLEDGE.mdinto the system prompt. The workspace is the agent’s “persona and knowledge base”.Two-layer persistent memory — answers how do conversation facts persist across sessions.
MemoryFlushHookuses an LLM to distill conversation into a daily fact log before compaction;MemoryConsolidatorruns in the background to merge and deduplicate daily logs into the long-termMEMORY.md. Still available on next startup.Compaction and overflow recovery — answers what to do when history gets too long.
CompactionHooksummarizes history and keeps a recent tail when message/token thresholds are exceeded; when the model actually reports a context overflow,HarnessAgentcatches the error, forces compaction, and retries automatically.Large tool result offloading — answers what to do when a single tool call returns too much.
ToolResultEvictionHookwrites oversized results to the filesystem and keeps only a head+tail preview with a placeholder in context; the agent can re-read on demand.Session persistence — answers how to preserve state across processes.
SessionPersistenceHookwrites agent state to the workspace bysessionId; the next call automatically resumes from where it left off.Subagent orchestration — answers how to decompose complex tasks.
SubagentsHookinjectstask/task_outputtools; the parent agent can delegate synchronously or in the background. Subagents can be declared via workspace spec files, programmatic specs, or custom factories.Pluggable filesystem — answers how to isolate and control the agent’s environment. All file tools go through
AbstractFilesystem. Choose from three declarative modes (local+shell, composite+store, sandbox) orabstractFilesystemfor self-managed backends. Multi-tenant / session-level isolation is handled viaRuntimeContext.userIdandIsolationScope.
Additionally, several infrastructure components support the above: RuntimeContext threads through the entire call, MemoryMaintenanceScheduler runs background merges and index maintenance, AgentTraceHook provides unified trace logging, and AgentSkillRepository auto-wires SkillBox.
How Capabilities Work Together¶
These capabilities collectively support three pillars of “stable continuous operation”:
Identity continuity — workspace context injection re-feeds persona and knowledge to the model each turn; two-layer persistent memory distills valuable facts from conversation back into the workspace; skill auto-loading keeps reusable capabilities tied to the workspace. The agent’s persona and knowledge do not disappear when a single call ends — they accumulate in the workspace over time.
Bounded context — conversation compaction controls depth, tool result offloading controls width, and overflow recovery is the final safety net. Together they ensure that even in arbitrarily long sessions, the context never overwhelms the model — and if it does, recovery is seamless.
Recoverable state — session persistence ensures a process restart can continue from where it left off; RuntimeContext threads the call’s identity (
sessionId/userId) through all hooks and tools; pluggable filesystem makes “where state actually lives” (local disk, sandbox, remote) a configuration choice.
The three pillars are connected by three shared objects: WorkspaceManager (who reads/writes the workspace), AbstractFilesystem (where the workspace lives physically), and RuntimeContext (who is speaking in this call). Each hook does only its own job and collaborates with others through these three objects — this is how Harness assembles independent capabilities into one “stably running agent”.
How Capabilities Are Injected¶
HarnessAgent is a thin wrapper around Agent + StateModule, internally holding a ReActAgent delegate. All capability injection happens in HarnessAgent.Builder.build():
Hook channel: hooks are assembled in
priorityorder and passed toReActAgent(includingSandboxLifecycleHookin sandbox mode, see Architecture)Toolkit channel:
filesystem,memory_search,memory_get,session_searchare appended to the user’sToolkit; sandbox backends additionally addshell_execute;SubagentsHookitself registerstask/task_outputSkillBox channel:
SkillBoxis auto-constructed fromworkspace/skills/or a customAgentSkillRepository
At the start of each call(), bindRuntimeContext distributes the current RuntimeContext to all hooks implementing RuntimeContextAwareHook, and restores state from Session as needed.
Detailed behavior, trigger timing, and sequence diagrams for each component are in Architecture.