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
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ default io.agentscope.core.state.AgentState getAgentState() {
* Returns the agent's live {@link Toolkit}, or {@code null} if this agent type does not
* maintain one.
*
* <p>This is the <em>runtime</em> toolkit the same instance the agent uses when listing
* <p>This is the <em>runtime</em> toolkit - the same instance the agent uses when listing
* available tools for the model and dispatching tool calls. Middleware that needs to register
* tools dynamically (e.g., skill loaders) must use this accessor rather than any toolkit
* reference captured at build time, because agents may deep-copy the toolkit during
Expand All @@ -111,4 +111,12 @@ default io.agentscope.core.state.AgentState getAgentState() {
default Toolkit getToolkit() {
return null;
}

/**
* Returns the current per-call {@link RuntimeContext} when the agent is executing, or
* {@code null} if this agent type does not expose one or no invocation is active.
*/
default RuntimeContext getRuntimeContext() {
return null;
}
}
Comment thread
guslegend0510 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@ public AgentState getAgentState() {
* on one instance this reflects the latest call — middlewares/tools that need their own call's
* context should read it from the per-subscription {@link RuntimeContext} they are handed.
*/
@Override
public RuntimeContext getRuntimeContext() {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public interface MiddlewareBase {
/**
* Intercept the entire agent invocation.
*
* <p>During an active invocation, per-call metadata is available through
* {@link Agent#getRuntimeContext()} for agent implementations that bind a runtime context.
*
* @param agent the agent instance
* @param ctx per-call runtime context (session, user, attributes)
* @param input agent input (messages)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.agentscope.core;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.agentscope.core.agent.Agent;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.event.AgentEndEvent;
import io.agentscope.core.event.AgentEvent;
import io.agentscope.core.message.ContentBlock;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.middleware.ActingInput;
import io.agentscope.core.middleware.AgentInput;
import io.agentscope.core.middleware.MiddlewareBase;
import io.agentscope.core.middleware.ModelCallInput;
import io.agentscope.core.middleware.ReasoningInput;
import io.agentscope.core.model.ChatModelBase;
import io.agentscope.core.model.ChatResponse;
import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.ToolSchema;
import io.agentscope.core.tool.Toolkit;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

class ReActAgentAgentImplRuntimeContextTest {

private static final class FixedTextModel extends ChatModelBase {
@Override
public String getModelName() {
return "fixed";
}

@Override
protected Flux<ChatResponse> doStream(
List<Msg> messages, List<ToolSchema> tools, GenerateOptions options) {
return Flux.just(
ChatResponse.builder()
.content(List.<ContentBlock>of(TextBlock.builder().text("ok").build()))
.build());
}
}

private static final class CapturingMiddleware implements MiddlewareBase {
private final boolean shortCircuit;
private final AtomicReference<RuntimeContext> seen = new AtomicReference<>();

private CapturingMiddleware(boolean shortCircuit) {
this.shortCircuit = shortCircuit;
}

@Override
public Flux<AgentEvent> onAgent(
Agent agent,
RuntimeContext ctx,
AgentInput input,
Function<AgentInput, Flux<AgentEvent>> next) {
seen.set(ctx);
return shortCircuit ? Flux.empty() : next.apply(input);
}

@Override
public Flux<AgentEvent> onReasoning(
Agent agent,
RuntimeContext ctx,
ReasoningInput input,
Function<ReasoningInput, Flux<AgentEvent>> next) {
return next.apply(input);
}

@Override
public Flux<AgentEvent> onModelCall(
Agent agent,
RuntimeContext ctx,
ModelCallInput input,
Function<ModelCallInput, Flux<AgentEvent>> next) {
return next.apply(input);
}

@Override
public Flux<AgentEvent> onActing(
Agent agent,
RuntimeContext ctx,
ActingInput input,
Function<ActingInput, Flux<AgentEvent>> next) {
return next.apply(input);
}

@Override
public Mono<String> onSystemPrompt(Agent agent, RuntimeContext ctx, String currentPrompt) {
return Mono.just(currentPrompt);
}
}

private static ReActAgent buildAgent(MiddlewareBase middleware) {
return ReActAgent.builder()
.name("asst")
.sysPrompt("hello-system")
.model(new FixedTextModel())
.toolkit(new Toolkit())
.middlewares(List.of(middleware))
.build();
}

@Test
void streamEventsRunsCoreLifecycleWithRuntimeContext() {
CapturingMiddleware middleware = new CapturingMiddleware(false);
ReActAgent agent = buildAgent(middleware);
RuntimeContext runtimeContext =
RuntimeContext.builder().sessionId("runtime-context-session").build();

List<AgentEvent> events =
agent.streamEvents(List.of(), runtimeContext).collectList().block();

assertNotNull(events);
assertTrue(events.get(events.size() - 1) instanceof AgentEndEvent);
assertSame(runtimeContext, middleware.seen.get());
assertNull(agent.getRuntimeContext());
}

@Test
void streamEventsClearsRuntimeContextWhenShortCircuited() {
CapturingMiddleware middleware = new CapturingMiddleware(true);
ReActAgent agent = buildAgent(middleware);
RuntimeContext runtimeContext =
RuntimeContext.builder().sessionId("runtime-context-session").build();

List<AgentEvent> events =
agent.streamEvents(List.of(), runtimeContext).collectList().block();

assertNotNull(events);
assertTrue(events.isEmpty(), events.toString());
assertSame(runtimeContext, middleware.seen.get());
assertNull(agent.getRuntimeContext());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.agentscope.core.agent;

import static org.junit.jupiter.api.Assertions.assertNull;

import com.fasterxml.jackson.databind.JsonNode;
import io.agentscope.core.message.Msg;
import java.util.List;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

class AgentTest {

@Test
void getRuntimeContext_defaultsToNull() {
Agent agent = new BareAgent();

assertNull(agent.getRuntimeContext());
}

private static final class BareAgent implements Agent {

@Override
public String getAgentId() {
return "bare-agent";
}

@Override
public String getName() {
return "bare-agent";
}

@Override
public void interrupt() {}

@Override
public void interrupt(Msg msg) {}

@Override
public Mono<Msg> call(List<Msg> msgs) {
return Mono.empty();
}

@Override
public Mono<Msg> call(List<Msg> msgs, Class<?> structuredModel) {
return Mono.empty();
}

@Override
public Mono<Msg> call(List<Msg> msgs, JsonNode schema) {
return Mono.empty();
}

@Override
public Flux<Event> stream(List<Msg> msgs, StreamOptions options) {
return Flux.empty();
}

@Override
public Flux<Event> stream(List<Msg> msgs, StreamOptions options, Class<?> structuredModel) {
return Flux.empty();
}

@Override
public Flux<Event> stream(List<Msg> msgs, StreamOptions options, JsonNode schema) {
return Flux.empty();
}

@Override
public Mono<Void> observe(Msg msg) {
return Mono.empty();
}

@Override
public Mono<Void> observe(List<Msg> msgs) {
return Mono.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.agentscope.core.skill;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import io.agentscope.core.agent.Agent;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.skill.repository.AgentSkillRepository;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;

class DynamicSkillMiddlewareRuntimeContextTest {

private static final class CapturingDynamicSkillMiddleware extends DynamicSkillMiddleware {
private final AtomicReference<RuntimeContext> seen = new AtomicReference<>();

private CapturingDynamicSkillMiddleware(List<AgentSkillRepository> repositories) {
super(repositories, null);
}

@Override
protected List<AgentSkill> filterVisible(List<AgentSkill> raw, RuntimeContext ctx) {
seen.set(ctx);
return raw;
}
}

@Test
void onSystemPromptFallsBackToEmptyContextWhenAgentIsNull() {
CapturingDynamicSkillMiddleware middleware =
new CapturingDynamicSkillMiddleware(List.of(skillRepo()));

String prompt = middleware.onSystemPrompt(null, RuntimeContext.empty(), "BASE").block();

assertNotNull(prompt);
assertTrue(prompt.startsWith("BASE"));
assertNotNull(middleware.seen.get());
}

@Test
void onSystemPromptUsesSuppliedRuntimeContext() {
RuntimeContext runtimeContext =
RuntimeContext.builder().sessionId("dynamic-skill-session").build();
Agent agent = mock(Agent.class);
CapturingDynamicSkillMiddleware middleware =
new CapturingDynamicSkillMiddleware(List.of(skillRepo()));

String prompt = middleware.onSystemPrompt(agent, runtimeContext, "BASE").block();

assertNotNull(prompt);
assertSame(runtimeContext, middleware.seen.get());
}

private static AgentSkillRepository skillRepo() {
AgentSkillRepository repo = mock(AgentSkillRepository.class);
when(repo.getAllSkills())
.thenReturn(
List.of(
new AgentSkill(
"sample",
"Sample skill",
"You can use the sample skill.",
Map.of())));
return repo;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ public int getMaxIters() {
return delegate.getMaxIters();
}

@Override
public RuntimeContext getRuntimeContext() {
return delegate.getRuntimeContext();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ public Flux<AgentEvent> onReasoning(
return next.apply(input);
}
RuntimeContext rc = ctx != null ? ctx : RuntimeContext.empty();
String sessionId = rc != null ? rc.getSessionId() : null;
String sessionId = rc.getSessionId();

// ---- Phase B-3 push delivery -------------------------------------------------------
// Drain newly-terminal tasks first so the SYSTEM summary built afterwards can omit them.
Expand Down
Loading
Loading