Skip to content
Merged
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
314 changes: 210 additions & 104 deletions agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions agentscope-core/src/main/java/io/agentscope/core/agent/Agent.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.agentscope.core.agent;

import io.agentscope.core.message.Msg;
import io.agentscope.core.tool.Toolkit;

/**
* Complete agent interface combining all capabilities.
Expand Down Expand Up @@ -96,4 +97,18 @@ default String getDescription() {
default io.agentscope.core.state.AgentState getAgentState() {
return null;
}

/**
* 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
* 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
* construction.
*/
default Toolkit getToolkit() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,31 @@ public final boolean isCheckRunning() {
*/
@Override
public final Mono<Msg> call(List<Msg> msgs) {
return runLifecycle(msgs, this::doCall);
return callInternal(msgs, null, this::doCall);
}

/**
* Extension point called by every {@code call()} overload, allowing subclasses to wrap the
* entire invocation in additional middleware (e.g. the {@code onAgent} chain in
* {@code ReActAgent}).
*
* <p>The default implementation attaches {@code context} to the Reactor Context (when
* non-null) and delegates straight to {@link #runLifecycle}. Subclasses that override this
* method must eventually invoke {@code runLifecycle(msgs, doCallFn)} to run the standard
* lifecycle (shutdown guard, serialization gate, pre/post hooks, tracing).
*
* @param msgs input messages
* @param context caller-supplied per-call {@link RuntimeContext}, or {@code null}
* @param doCallFn the concrete call implementation ({@link #doCall} or a structured-output
* variant)
* @return response message
*/
protected Mono<Msg> callInternal(
List<Msg> msgs, RuntimeContext context, Function<List<Msg>, Mono<Msg>> doCallFn) {
Mono<Msg> lifecycle = runLifecycle(msgs, doCallFn);
return context == null
? lifecycle
: lifecycle.contextWrite(c -> c.put(RUNTIME_CONTEXT_KEY, context));
}

/**
Expand Down Expand Up @@ -225,7 +249,7 @@ public final Mono<Msg> call(List<Msg> msgs) {
* that scope on the Reactor Context, and run the preCall → doCall → postCall chain with error
* handling, releasing execution on terminate.
*/
private Mono<Msg> runLifecycle(List<Msg> msgs, Function<List<Msg>, Mono<Msg>> doCallFn) {
protected Mono<Msg> runLifecycle(List<Msg> msgs, Function<List<Msg>, Mono<Msg>> doCallFn) {
return Mono.using(
this::acquireExecution,
resource ->
Expand Down Expand Up @@ -351,7 +375,7 @@ private <T> Mono<T> serializeOnKey(Object key, Mono<T> action) {
*/
@Override
public final Mono<Msg> call(List<Msg> msgs, Class<?> structuredOutputClass) {
return runLifecycle(msgs, m -> doCall(m, structuredOutputClass));
return callInternal(msgs, null, m -> doCall(m, structuredOutputClass));
}

/**
Expand All @@ -365,7 +389,7 @@ public final Mono<Msg> call(List<Msg> msgs, Class<?> structuredOutputClass) {
*/
@Override
public final Mono<Msg> call(List<Msg> msgs, JsonNode schema) {
return runLifecycle(msgs, m -> doCall(m, schema));
return callInternal(msgs, null, m -> doCall(m, schema));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

/**
Expand All @@ -34,6 +36,7 @@
@JsonSubTypes({
@JsonSubTypes.Type(value = AgentStartEvent.class, name = "AGENT_START"),
@JsonSubTypes.Type(value = AgentEndEvent.class, name = "AGENT_END"),
@JsonSubTypes.Type(value = AgentResultEvent.class, name = "AGENT_RESULT"),
@JsonSubTypes.Type(value = ModelCallStartEvent.class, name = "MODEL_CALL_START"),
@JsonSubTypes.Type(value = ModelCallEndEvent.class, name = "MODEL_CALL_END"),
@JsonSubTypes.Type(value = TextBlockStartEvent.class, name = "TEXT_BLOCK_START"),
Expand Down Expand Up @@ -62,14 +65,19 @@
value = ExternalExecutionResultEvent.class,
name = "EXTERNAL_EXECUTION_RESULT"),
@JsonSubTypes.Type(value = RequestStopEvent.class, name = "REQUEST_STOP"),
@JsonSubTypes.Type(value = SubagentExposedEvent.class, name = "SUBAGENT_EXPOSED")
@JsonSubTypes.Type(value = SubagentExposedEvent.class, name = "SUBAGENT_EXPOSED"),
@JsonSubTypes.Type(value = HintBlockEvent.class, name = "HINT_BLOCK"),
@JsonSubTypes.Type(value = CustomEvent.class, name = "CUSTOM")
})
public abstract class AgentEvent {

private final String id;
private final String createdAt;
private String source;

@JsonInclude(JsonInclude.Include.NON_EMPTY)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Metadata field lacks validation: The withMetadata method accepts any Map<String, Object> but the metadata field is typed as Map<String, Object>. If callers store non-serializable objects or complex types in this map, Jackson serialization may fail at runtime. Consider adding a note in the Javadoc that values should be JSON-serializable, or provide a builder/validator that enforces this constraint.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Metadata field lacks validation: The withMetadata method accepts any Map<String, Object> but the metadata field is typed as Map<String, Object>. If callers store non-serializable objects or complex types in this map, Jackson serialization may fail at runtime. Consider adding a note in the Javadoc that values should be JSON-serializable, or provide a builder/validator that enforces this constraint.

private Map<String, Object> metadata;

protected AgentEvent() {
this.id = UUID.randomUUID().toString().replace("-", "");
this.createdAt = Instant.now().toString();
Expand Down Expand Up @@ -108,6 +116,21 @@ public AgentEvent withSource(String source) {
return this;
}

/**
* Returns optional metadata attached to this event. May be {@code null} or empty.
*/
public Map<String, Object> getMetadata() {
return metadata;
}

/**
* Attaches arbitrary key-value metadata to this event and returns it for chaining.
*/
public AgentEvent withMetadata(Map<String, Object> metadata) {
this.metadata = metadata != null ? new LinkedHashMap<>(metadata) : null;
return this;
}

@Override
public String toString() {
return getClass().getSimpleName() + "{id='" + id + "', type=" + getType() + '}';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public enum AgentEventType {
AGENT_START("AGENT_START"),
@JsonAlias({"RUN_FINISHED", "REPLY_END"})
AGENT_END("AGENT_END"),
AGENT_RESULT("AGENT_RESULT"),

@JsonAlias({"MODEL_CALL_STARTED"})
MODEL_CALL_START("MODEL_CALL_START"),
Expand Down Expand Up @@ -82,7 +83,10 @@ public enum AgentEventType {
REQUEST_STOP("REQUEST_STOP"),

@JsonAlias({"THREAD_EXPOSED"})
SUBAGENT_EXPOSED("SUBAGENT_EXPOSED");
SUBAGENT_EXPOSED("SUBAGENT_EXPOSED"),

HINT_BLOCK("HINT_BLOCK"),
CUSTOM("CUSTOM");

private final String value;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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.event;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.agentscope.core.message.Msg;

/**
* Emitted when an agent successfully finishes processing an invocation, carrying the final result
* message.
*
* <p>This event is emitted as part of the agent event stream (both {@code call()} and
* {@code streamEvents()} paths) immediately before {@link AgentEndEvent}. Callers of
* {@code streamEvents()} can filter for this event to obtain the final {@link Msg} directly
* from the event stream without subscribing to the {@code Mono<Msg>} return value separately.
*
* <p>{@code call()} internally uses this event to extract the result from the shared
* {@code buildAgentStream()} core, ensuring both paths run through the same {@code onAgent}
* middleware chain.
*/
public class AgentResultEvent extends AgentEvent {

private final Msg result;

public AgentResultEvent(Msg result) {
this.result = result;
}

@JsonCreator
public AgentResultEvent(
@JsonProperty("id") String id,
@JsonProperty("createdAt") String createdAt,
@JsonProperty("result") Msg result) {
super(id, createdAt);
this.result = result;
}

@Override
public AgentEventType getType() {
return AgentEventType.AGENT_RESULT;
}

public Msg getResult() {
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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.event;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;

/**
* Generic extensible event for signals that don't fit a specific {@link AgentEvent} subtype.
*
* <p>Used by service-layer middleware to notify front-end subscribers about state changes (task
* progress, team membership, permission updates, etc.) without polluting the core agent event enum
* with application-specific types.
*
* <p>Front-end implementations should handle unknown {@link #getName()} values gracefully — skip
* with no error.
*
* <p>Well-known {@code name} values:
* <ul>
* <li>{@code "state_updated"} — agent state (tasks / permission) changed during a tool call</li>
* <li>{@code "team_updated"} — team membership changed (member added / team created or
* dissolved)</li>
* </ul>
*/
public class CustomEvent extends AgentEvent {

private final String name;
private final Map<String, Object> value;

@JsonCreator
public CustomEvent(
@JsonProperty("id") String id,
@JsonProperty("createdAt") String createdAt,
@JsonProperty("name") String name,
@JsonProperty("value") Map<String, Object> value) {
super(id, createdAt);
this.name = name;
this.value = value != null ? value : Map.of();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Immutable empty map may surprise callers: When value is null, the constructor assigns Map.of() which returns an immutable empty map. While this is safe, callers who expect to mutate the returned map (e.g., via event.getValue().put(...)) will get an UnsupportedOperationException. Consider documenting this immutability contract in the Javadoc for getValue(), or defensively copy to a mutable map when the input is null.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Immutable empty map may surprise callers: When value is null, the constructor assigns Map.of() which returns an immutable empty map. While this is safe, callers who expect to mutate the returned map (e.g., via event.getValue().put(...)) will get an UnsupportedOperationException. Consider documenting this immutability contract in the Javadoc for getValue(), or defensively copy to a mutable map when the input is null.

}

public CustomEvent(String name, Map<String, Object> value) {
this.name = name;
this.value = value != null ? value : Map.of();
}

public CustomEvent(String name) {
this(name, Map.of());
}

@Override
public AgentEventType getType() {
return AgentEventType.CUSTOM;
}

/**
* Returns the kind of notification. See class javadoc for well-known values.
*/
public String getName() {
return name;
}

/**
* Returns the arbitrary JSON-serializable payload whose schema depends on {@link #getName()}.
*/
public Map<String, Object> getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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.event;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* One-shot hint block event.
*
* <p>Unlike text/thinking blocks, hint blocks are not streamed — the full content is available at
* creation time (team messages, background tool results, user interruptions, etc.). A single event
* carries the complete hint.
*/
public class HintBlockEvent extends AgentEvent {

private final String replyId;
private final String blockId;
private final String hintSource;
private final String hint;

@JsonCreator
public HintBlockEvent(
@JsonProperty("id") String id,
@JsonProperty("createdAt") String createdAt,
@JsonProperty("replyId") String replyId,
@JsonProperty("blockId") String blockId,
@JsonProperty("hintSource") String hintSource,
@JsonProperty("hint") String hint) {
super(id, createdAt);
this.replyId = replyId;
this.blockId = blockId;
this.hintSource = hintSource;
this.hint = hint;
}

public HintBlockEvent(String replyId, String blockId, String hintSource, String hint) {
this.replyId = replyId;
this.blockId = blockId;
this.hintSource = hintSource;
this.hint = hint;
}

@Override
public AgentEventType getType() {
return AgentEventType.HINT_BLOCK;
}

public String getReplyId() {
return replyId;
}

public String getBlockId() {
return blockId;
}

/**
* Returns the sender or origin of this hint. For team messages this is the sender's display
* name (e.g. {@code "alice"}); for system notifications it may be {@code "system"} or
* {@code null}.
*
* <p>Named {@code hintSource} to avoid collision with {@link AgentEvent#getSource()} which
* carries the subagent forwarding path.
*/
public String getHintSource() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] hintSource naming rationale could be clearer: The Javadoc explains that hintSource avoids collision with AgentEvent.getSource(), but the name itself might confuse readers who expect 'source' to mean the event origin. Consider whether hintSender, hintOrigin, or hintAuthor would be more intuitive, or add a concrete example in the Javadoc showing typical values.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] hintSource naming rationale could be clearer: The Javadoc explains that hintSource avoids collision with AgentEvent.getSource(), but the name itself might confuse readers who expect 'source' to mean the event origin. Consider whether hintSender, hintOrigin, or hintAuthor would be more intuitive, or add a concrete example in the Javadoc showing typical values.

return hintSource;
}

public String getHint() {
return hint;
}
}
Loading
Loading