Skip to content

Commit 5c8e9e1

Browse files
chickenljfang-techCopilot
authored
Introduce agentscope-harness module (#1353)
This pull request introduces a new `agentscope-harness` module built with the `Harness Engineering` practices. ## Purpose agentscope-harness builds on agentscope-core with a higher-level agent runtime for production-style assistants: workspace-first workflows, pluggable execution environments, and richer memory/session tooling. ## Entry point HarnessAgent wraps ReActAgent with opinionated defaults: hooks, toolkit wiring, and configuration for workspace, skills, subagents, and persistence. ## Key design ideas * Workspace-centric context — Loads and injects workspace material (e.g. AGENTS.md, MEMORY.md, knowledge) into the agent’s unified system message via hooks such as WorkspaceContextHook. * Pluggable filesystem — Abstracts file operations behind a filesystem layer (local, sandbox-backed, remote/composite) so tools and agents share one API regardless of where files actually live. * Sandbox & isolation — Optional sandbox execution with lifecycle management, state stores, and options for distributed deployments when session/sandbox state must be shared across replicas. * Session & persistence — WorkspaceSession, session trees, and hooks (e.g. session persistence, traces) tie long-running work to durable session state where configured. * Memory operations — Memory flush/consolidation, compaction, and eviction policies help keep context bounded; dedicated tools (e.g. memory search/get) support retrieval over workspace memory files. * Subagents — Declarative subagent specs, spawn/send/list tooling, and background tasks for delegated or async work (SubagentsHook, task repository). * Relationship to core. Harness does not replace ReActAgent; it composes it with extra hooks, tools, and backends so applications can adopt “harness” behavior incrementally while staying aligned with core APIs (Model, Toolkit, Session, hooks). This pull request also includes significant enhancements to the `ReActAgent` class, focusing on improved runtime context handling, per-call system message management, and better integration of hook-supplied tools. * Runtime Context and System Message Handling * Hook and Toolkit Integration * General Refactoring and Minor Improvements --------- Co-authored-by: fang-tech <tianyufang978@gmail.com> Co-authored-by: Copilot <copilot@github.com>
1 parent f7f1c17 commit 5c8e9e1

233 files changed

Lines changed: 32338 additions & 260 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,9 @@ logs/
5555
**/boba-tea-shop/supervisor-agent/**/static/
5656
**/boba-tea-shop/**/node_modules/
5757
**/boba-tea-shop/**/dist/
58+
59+
##agentscope
60+
.agentscope/
61+
62+
## harness db
63+
**/*.db

.licenserc.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ header:
1717
limitations under the License.
1818
1919
paths-ignore:
20+
# Config / example files without license headers
21+
- 'agentscope-examples/harness-example/.env.example'
22+
- 'agentscope-examples/harness-example/src/main/resources/agentscope.json.example'
23+
- 'agentscope-examples/harness-example/src/main/resources/log4j2.xml'
24+
- 'agentscope-harness/src/main/resources/agentscope.json.example'
2025
- '**/*.versionsBackup'
2126
- '**/.idea/'
2227
- '**/*.iml'

agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java

Lines changed: 160 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
*/
1616
package io.agentscope.core;
1717

18+
import com.fasterxml.jackson.databind.JsonNode;
19+
import io.agentscope.core.agent.Event;
20+
import io.agentscope.core.agent.RuntimeContext;
21+
import io.agentscope.core.agent.StreamOptions;
1822
import io.agentscope.core.agent.StructuredOutputCapableAgent;
1923
import io.agentscope.core.agent.accumulator.ReasoningContext;
2024
import io.agentscope.core.hook.ActingChunkEvent;
@@ -151,6 +155,17 @@ public class ReActAgent extends StructuredOutputCapableAgent {
151155
private final PlanNotebook planNotebook;
152156
private final ToolExecutionContext toolExecutionContext;
153157
private final StatePersistence statePersistence;
158+
private RuntimeContext pendingRuntimeContext;
159+
160+
/**
161+
* Per-call system message, propagated across PreCallEvent → PreReasoningEvent /
162+
* PreSummaryEvent. It is safe to use an {@link java.util.concurrent.atomic.AtomicReference}
163+
* here because {@code AgentBase.acquireExecution()} guarantees that only one {@code call()}
164+
* runs concurrently per agent instance, so this reference is effectively owned by a single
165+
* logical execution at any time.
166+
*/
167+
private final java.util.concurrent.atomic.AtomicReference<Msg> currentSystemMsg =
168+
new java.util.concurrent.atomic.AtomicReference<>();
154169

155170
// ==================== Constructor ====================
156171

@@ -178,6 +193,91 @@ private ReActAgent(Builder builder, Toolkit agentToolkit) {
178193
: StatePersistence.all();
179194
}
180195

196+
// ==================== RuntimeContext ====================
197+
198+
@Override
199+
protected void beforeAgentExecution(List<Msg> msgs) {
200+
RuntimeContext ctx = this.pendingRuntimeContext;
201+
this.pendingRuntimeContext = null;
202+
if (ctx == null) {
203+
ctx = RuntimeContext.empty();
204+
}
205+
bindRuntimeContextToHooks(ctx);
206+
// Reset per-call system message; will be initialised by consumeSystemMsgAfterPreCall
207+
currentSystemMsg.set(null);
208+
}
209+
210+
@Override
211+
protected Msg seedSystemMsg() {
212+
if (sysPrompt != null && !sysPrompt.trim().isEmpty()) {
213+
return Msg.builder()
214+
.name("system")
215+
.role(MsgRole.SYSTEM)
216+
.content(TextBlock.builder().text(sysPrompt).build())
217+
.build();
218+
}
219+
return null;
220+
}
221+
222+
@Override
223+
protected void consumeSystemMsgAfterPreCall(Msg systemMsg) {
224+
currentSystemMsg.set(systemMsg);
225+
}
226+
227+
@Override
228+
protected void afterAgentExecution() {
229+
unbindRuntimeContextFromHooks();
230+
}
231+
232+
private ToolExecutionContext buildMergedToolContext() {
233+
RuntimeContext run = getRuntimeContext();
234+
if (run == null) {
235+
return toolExecutionContext != null
236+
? toolExecutionContext
237+
: ToolExecutionContext.empty();
238+
}
239+
return ToolExecutionContext.merge(run.asToolExecutionContext(), toolExecutionContext);
240+
}
241+
242+
/**
243+
* Calls the agent with a per-call {@link RuntimeContext} (metadata for hooks and tools, not
244+
* persisted).
245+
*/
246+
public Mono<Msg> call(List<Msg> msgs, RuntimeContext context) {
247+
this.pendingRuntimeContext = context;
248+
return call(msgs);
249+
}
250+
251+
public Mono<Msg> call(List<Msg> msgs, Class<?> structuredOutputClass, RuntimeContext context) {
252+
this.pendingRuntimeContext = context;
253+
return call(msgs, structuredOutputClass);
254+
}
255+
256+
public Mono<Msg> call(List<Msg> msgs, JsonNode outputSchema, RuntimeContext context) {
257+
this.pendingRuntimeContext = context;
258+
return call(msgs, outputSchema);
259+
}
260+
261+
public Flux<Event> stream(List<Msg> msgs, StreamOptions options, RuntimeContext context) {
262+
this.pendingRuntimeContext = context;
263+
return stream(msgs, options);
264+
}
265+
266+
public Flux<Event> stream(
267+
List<Msg> msgs,
268+
StreamOptions options,
269+
Class<?> structuredModel,
270+
RuntimeContext context) {
271+
this.pendingRuntimeContext = context;
272+
return stream(msgs, options, structuredModel);
273+
}
274+
275+
public Flux<Event> stream(
276+
List<Msg> msgs, StreamOptions options, JsonNode schema, RuntimeContext context) {
277+
this.pendingRuntimeContext = context;
278+
return stream(msgs, options, schema);
279+
}
280+
181281
// ==================== New StateModule API ====================
182282

183283
/**
@@ -466,17 +566,17 @@ private Mono<Msg> reasoning(int iter, boolean ignoreMaxIters) {
466566
ReasoningContext context = new ReasoningContext(getName());
467567

468568
return checkInterruptedAsync()
469-
.then(notifyPreReasoningEvent(prepareMessages()))
569+
.then(notifyPreReasoningEvent(memory.getMessages()))
470570
.flatMapMany(
471571
event -> {
472572
GenerateOptions options =
473573
event.getEffectiveGenerateOptions() != null
474574
? event.getEffectiveGenerateOptions()
475575
: buildGenerateOptions();
476-
return model.stream(
477-
event.getInputMessages(),
478-
toolkit.getToolSchemas(),
479-
options)
576+
List<Msg> modelInput =
577+
prependSystemMsg(
578+
event.getInputMessages(), event.getSystemMessage());
579+
return model.stream(modelInput, toolkit.getToolSchemas(), options)
480580
.concatMap(chunk -> checkInterruptedAsync().thenReturn(chunk));
481581
})
482582
.doOnNext(
@@ -665,7 +765,7 @@ private Msg buildSuspendedMsg(List<Map.Entry<ToolUseBlock, ToolResultBlock>> pen
665765
*/
666766
private Mono<List<Map.Entry<ToolUseBlock, ToolResultBlock>>> executeToolCalls(
667767
List<ToolUseBlock> toolCalls) {
668-
return toolkit.callTools(toolCalls, toolExecutionConfig, this, toolExecutionContext)
768+
return toolkit.callTools(toolCalls, toolExecutionConfig, this, buildMergedToolContext())
669769
.map(
670770
results ->
671771
IntStream.range(0, toolCalls.size())
@@ -734,7 +834,10 @@ protected Mono<Msg> summarizing() {
734834
return notifyPreSummaryHook(messageList, generateOptions)
735835
.flatMap(
736836
preSummaryEvent -> {
737-
List<Msg> effectiveMessages = preSummaryEvent.getInputMessages();
837+
List<Msg> effectiveMessages =
838+
prependSystemMsg(
839+
preSummaryEvent.getInputMessages(),
840+
preSummaryEvent.getSystemMessage());
738841
GenerateOptions effectiveOptions =
739842
preSummaryEvent.getEffectiveGenerateOptions();
740843

@@ -775,7 +878,7 @@ private Mono<Msg> streamAndAccumulateSummary(
775878
}
776879

777880
private List<Msg> prepareSummaryMessages() {
778-
List<Msg> messageList = prepareMessages();
881+
List<Msg> messageList = new ArrayList<>(memory.getMessages());
779882
messageList.add(
780883
Msg.builder()
781884
.name("user")
@@ -816,20 +919,21 @@ private Mono<Msg> handleSummaryError(Throwable error) {
816919
// ==================== Helper Methods ====================
817920

818921
/**
819-
* Prepare messages for model input.
922+
* Prepends the system message to {@code msgs} if non-null.
923+
*
924+
* <p>Called immediately before each {@code model.stream()} invocation to build the final
925+
* LLM input without contaminating the in-memory message list.
820926
*/
821-
private List<Msg> prepareMessages() {
822-
List<Msg> messages = new ArrayList<>();
823-
if (sysPrompt != null && !sysPrompt.trim().isEmpty()) {
824-
messages.add(
825-
Msg.builder()
826-
.name("system")
827-
.role(MsgRole.SYSTEM)
828-
.content(TextBlock.builder().text(sysPrompt).build())
829-
.build());
927+
private static List<Msg> prependSystemMsg(List<Msg> msgs, Msg systemMsg) {
928+
if (systemMsg == null) {
929+
return msgs != null ? msgs : List.of();
830930
}
831-
messages.addAll(memory.getMessages());
832-
return messages;
931+
List<Msg> result = new ArrayList<>();
932+
result.add(systemMsg);
933+
if (msgs != null) {
934+
result.addAll(msgs);
935+
}
936+
return result;
833937
}
834938

835939
/**
@@ -911,7 +1015,9 @@ private <T extends HookEvent> Mono<T> notifyHooks(T event) {
9111015
}
9121016

9131017
private Mono<PreReasoningEvent> notifyPreReasoningEvent(List<Msg> msgs) {
914-
return notifyHooks(new PreReasoningEvent(this, model.getModelName(), null, msgs));
1018+
PreReasoningEvent event = new PreReasoningEvent(this, model.getModelName(), null, msgs);
1019+
event.setSystemMessage(currentSystemMsg.get());
1020+
return notifyHooks(event);
9151021
}
9161022

9171023
private Mono<PostReasoningEvent> notifyPostReasoning(Msg msg) {
@@ -981,9 +1087,11 @@ private Mono<Void> notifyReasoningChunk(Msg chunkMsg, ReasoningContext context)
9811087

9821088
private Mono<PreSummaryEvent> notifyPreSummaryHook(
9831089
List<Msg> msgs, GenerateOptions generateOptions) {
984-
return notifyHooks(
1090+
PreSummaryEvent event =
9851091
new PreSummaryEvent(
986-
this, model.getModelName(), generateOptions, msgs, maxIters, maxIters));
1092+
this, model.getModelName(), generateOptions, msgs, maxIters, maxIters);
1093+
event.setSystemMessage(currentSystemMsg.get());
1094+
return notifyHooks(event);
9871095
}
9881096

9891097
private Mono<PostSummaryEvent> notifyPostSummaryHook(Msg msg, GenerateOptions generateOptions) {
@@ -1219,6 +1327,7 @@ public Builder maxIters(int maxIters) {
12191327
* @param hook The hook to add, must not be null
12201328
* @return This builder instance for method chaining
12211329
* @see Hook
1330+
* @see Hook#tools()
12221331
*/
12231332
public Builder hook(Hook hook) {
12241333
this.hooks.add(hook);
@@ -1234,6 +1343,7 @@ public Builder hook(Hook hook) {
12341343
* @param hooks The list of hooks to add, must not be null
12351344
* @return This builder instance for method chaining
12361345
* @see Hook
1346+
* @see Hook#tools()
12371347
*/
12381348
public Builder hooks(List<Hook> hooks) {
12391349
this.hooks.addAll(hooks);
@@ -1374,7 +1484,8 @@ public Builder planNotebook(PlanNotebook planNotebook) {
13741484
* <p>The skill box is used to manage the skills for this agent. It will be used to register the skills to the toolkit.
13751485
* <ul>
13761486
* <li>Skill loader tools will be automatically registered to the toolkit</li>
1377-
* <li>A skill hook will be added to inject skill prompts and manage skill activation</li>
1487+
* <li>A skill hook will be added to inject skill prompts on {@link io.agentscope.core.hook.PreCallEvent}
1488+
* and manage skill activation</li>
13781489
* </ul>
13791490
* @param skillBox The skill box to use for this agent
13801491
* @return This builder instance for method chaining
@@ -1562,6 +1673,8 @@ public ReActAgent build() {
15621673
// Deep copy toolkit to avoid state interference between agents
15631674
Toolkit agentToolkit = this.toolkit.copy();
15641675

1676+
registerToolsFromHooks(agentToolkit);
1677+
15651678
if (enableMetaTool) {
15661679
agentToolkit.registerMetaTool();
15671680
}
@@ -1594,6 +1707,26 @@ public ReActAgent build() {
15941707
return new ReActAgent(this, agentToolkit);
15951708
}
15961709

1710+
/**
1711+
* Registers tool objects declared by hooks ({@link Hook#tools()}) on the agent toolkit.
1712+
*
1713+
* <p>Runs after {@link Toolkit#copy()} so hook-supplied tools are scoped to this agent
1714+
* instance without modifying the builder's original toolkit.
1715+
*/
1716+
private void registerToolsFromHooks(Toolkit agentToolkit) {
1717+
for (Hook hook : hooks) {
1718+
List<Object> toolObjects = hook.tools();
1719+
if (toolObjects == null || toolObjects.isEmpty()) {
1720+
continue;
1721+
}
1722+
for (Object toolObject : toolObjects) {
1723+
if (toolObject != null) {
1724+
agentToolkit.registerTool(toolObject);
1725+
}
1726+
}
1727+
}
1728+
}
1729+
15971730
/**
15981731
* Configures long-term memory based on the selected mode.
15991732
*
@@ -1748,7 +1881,9 @@ public <T extends HookEvent> Mono<T> onEvent(T event) {
17481881
* <p>This method automatically:
17491882
* <ul>
17501883
* <li>Registers skill load tool to the toolkit
1751-
* <li>Adds the skill hook to inject skill prompts and manage skill activation
1884+
* <li>Adds the skill hook to inject skill prompts on {@link io.agentscope.core.hook.PreCallEvent}
1885+
* (priority {@link io.agentscope.core.skill.SkillHook#SKILL_HOOK_PRIORITY}) and manage skill
1886+
* activation
17521887
* <li>Uploads skill files to the upload directory if auto upload is enabled
17531888
* </ul>
17541889
*/

0 commit comments

Comments
 (0)