Message & Event

Message and event are the two fundamental data structures in AgentScope.

  • Message — the primitive of agent-to-agent communication and persistence. Each Msg is a complete conversation turn, stored in the context and passed between agents.

  • Event — the primitive of frontend interaction and streaming. Events carry incremental progress updates (text tokens, tool-call fragments, permission requests, …) and drive real-time UIs and human-in-the-loop flows.

The event sequence emitted by a single call always condenses into exactly one assistant Msg, ensuring the full message state can be reconstructed from the event stream alone.

Message

Msg (io.agentscope.core.message) represents one turn of conversation — a user input, an agent reply, or a system instruction — with content modelled as an ordered list of typed ContentBlocks.

Tip

A single assistant Msg corresponds to one full call cycle (multiple reasoning + acting iterations until the final reply).

Structure

The core fields on Msg (via getters):

Method

Type

Description

getId()

String

Unique message identifier

getName()

String

Sender name (nullable)

getRole()

MsgRole

USER / ASSISTANT / SYSTEM / TOOL

getContent()

List<ContentBlock>

Ordered list of content blocks (immutable)

getMetadata()

Map<String, Object>

Arbitrary key/value metadata

getTimestamp()

String

Creation time (yyyy-MM-dd HH:mm:ss.SSS)

getUsage()

ChatUsage

Token usage (assistant messages only)

getGenerateReason()

GenerateReason

Termination reason: MODEL_STOP / TOOL_SUSPENDED / REASONING_STOP_REQUESTED / ACTING_STOP_REQUESTED / INTERRUPTED / MAX_ITERATIONS

Content blocks

Message content is composed of typed blocks, each representing one type of information. Block classes live in io.agentscope.core.message:

Block

Description

Allowed in

TextBlock

Plain text content

USER, ASSISTANT, SYSTEM

DataBlock

Binary data (image / audio / video) via base64 or URL — unifies the legacy ImageBlock / AudioBlock / VideoBlock

USER, ASSISTANT

ImageBlock / AudioBlock / VideoBlock

Legacy concrete media blocks (still supported; new code should prefer DataBlock)

USER

ThinkingBlock

Model reasoning / chain of thought

ASSISTANT

ToolUseBlock

A tool call: id / name / input / state (ToolCallState)

ASSISTANT

ToolResultBlock

A tool result with state (ToolResultState)

ASSISTANT

HintBlock

Instructions injected into the loop as user context

ASSISTANT

Note

Role constraints are enforced at construction: USER only allows text/data/image/audio/video blocks; SYSTEM only allows TextBlock; ASSISTANT allows all block types.

Creating a message

The role-pinned subclasses (io.agentscope.core.message.UserMessage / AssistantMessage / SystemMessage / ToolResultMessage) provide convenient constructors. When content is a plain string, it is wrapped in a TextBlock automatically.

import io.agentscope.core.message.AssistantMessage;
import io.agentscope.core.message.Base64Source;
import io.agentscope.core.message.DataBlock;
import io.agentscope.core.message.SystemMessage;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.message.UserMessage;

// User message — text only
UserMessage userText = new UserMessage("user", "What's in this image?");

// Multi-modal user message
UserMessage userMulti =
        new UserMessage(
                "user",
                TextBlock.builder().text("Describe this image:").build(),
                DataBlock.builder()
                        .source(Base64Source.builder()
                                .data("...")
                                .mediaType("image/png")
                                .build())
                        .build());

// System message — text only
SystemMessage systemMsg = new SystemMessage("system", "You are a helpful assistant.");

// Assistant message — all block types allowed
AssistantMessage assistantMsg = new AssistantMessage("agent", "Here's the result...");

For more optional fields (metadata, timestamp, usage, generateReason), use each subclass’s builder():

UserMessage msg =
        UserMessage.builder()
                .name("user")
                .textContent("Hello")
                .build();

Accessing content

Msg provides helpers for extracting specific block types:

Method

Returns

getTextContent()

All TextBlocks concatenated by \n; empty string when there are none

getContentBlocks(Class<T>)

List filtered by type

getFirstContentBlock(Class<T>)

The first matching block, or null

hasContentBlocks(Class<T>)

true if a block of the given type exists

import io.agentscope.core.message.ToolUseBlock;
import io.agentscope.core.message.ToolResultBlock;

// All text content
String text = msg.getTextContent();

// All tool calls
List<ToolUseBlock> toolCalls = msg.getContentBlocks(ToolUseBlock.class);

// Whether there are tool results
if (msg.hasContentBlocks(ToolResultBlock.class)) {
    // ...
}

Event

Events are the streaming counterpart of messages. While the agent runs, it emits a sequence of AgentEvents (io.agentscope.core.event) representing incremental progress — text tokens arriving, tool calls being assembled, results streaming back. Each event is lightweight and self-contained.

Event lifecycle

Every event carries getReplyId(), tying it to the message being assembled. Within a reply, getBlockId() or getToolCallId() identifies the content block the event belongs to. Events follow a start → delta → end pattern:

        sequenceDiagram
    participant Client
    participant Agent

    Agent->>Client: AgentStartEvent

    rect rgba(100, 150, 255, 0.1)
        Note over Client,Agent: Reasoning phase
        Agent->>Client: ModelCallStartEvent
        rect rgba(200, 200, 100, 0.1)
            Note over Client,Agent: TextBlock (blockId)
            Agent->>Client: TextBlockStartEvent
            Agent->>Client: TextBlockDeltaEvent (×N)
            Agent->>Client: TextBlockEndEvent
        end
        rect rgba(200, 200, 100, 0.1)
            Note over Client,Agent: DataBlock (blockId)
            Agent->>Client: DataBlockStartEvent
            Agent->>Client: DataBlockDeltaEvent (×N)
            Agent->>Client: DataBlockEndEvent
        end
        rect rgba(200, 200, 100, 0.1)
            Note over Client,Agent: ToolUseBlock (toolCallId)
            Agent->>Client: ToolCallStartEvent
            Agent->>Client: ToolCallDeltaEvent (×N)
            Agent->>Client: ToolCallEndEvent
        end
        Agent->>Client: ModelCallEndEvent
    end

    rect rgba(100, 255, 150, 0.1)
        Note over Client,Agent: Acting phase
        rect rgba(200, 200, 100, 0.1)
            Note over Client,Agent: ToolResultBlock (toolCallId)
            Agent->>Client: ToolResultStartEvent
            Agent->>Client: ToolResultTextDeltaEvent (×N)
            Agent->>Client: ToolResultDataDeltaEvent (×N)
            Agent->>Client: ToolResultEndEvent
        end
    end

    Agent->>Client: AgentEndEvent
    

All events in one reply share the same replyId. Within a reply, blockId ties text/thinking/data block events together; toolCallId ties tool calls and tool results.

Event types

All events extend AgentEvent (io.agentscope.core.event), which exposes the common methods:

Method

Type

Description

getId()

String

Unique event identifier

getCreatedAt()

String

ISO 8601 timestamp

getType()

AgentEventType

Event type enum

Events are grouped below; unless noted otherwise, every event also carries getReplyId() linking it to the message being assembled.

Lifecycle events

AgentStartEvent — agent begins a new reply.

| Method | Type | Description |
|--------|------|-------------|
| `getReplyId()` | `String` | Reply message ID |
| `getSessionId()` | `String` | Session ID |
| `getName()` | `String` | Agent name |
| `getRole()` | `String` | Agent role (default `"assistant"`) |

**AgentEndEvent** — agent finishes a reply.

| Method | Type | Description |
|--------|------|-------------|
| `getReplyId()` | `String` | Reply message ID |

**ExceedMaxItersEvent** — agent hit the max reasoning-acting iteration limit.

| Method | Type | Description |
|--------|------|-------------|
| `getReplyId()` | `String` | Reply message ID |

**RequestStopEvent** — early-stop request raised by middleware or a tool.
Text streaming events

TextBlockStartEvent — a new text block begins.

Method

Type

Description

getReplyId()

String

Reply message ID

getBlockId()

String

Unique text block ID

TextBlockDeltaEvent — incremental text content arrives.

Method

Type

Description

getReplyId()

String

Reply message ID

getBlockId()

String

Unique text block ID

getDelta()

String

Incremental text content

TextBlockEndEvent — text block completes.

Method

Type

Description

getReplyId()

String

Reply message ID

getBlockId()

String

Unique text block ID

Thinking streaming events

ThinkingBlockStartEvent / ThinkingBlockDeltaEvent / ThinkingBlockEndEvent — same shape as the text streaming events; specific to the model’s chain of thought.

Data streaming events

DataBlockStartEvent / DataBlockDeltaEvent / DataBlockEndEvent — same shape as the text streaming events, carrying images / audio / video binary data:

  • DataBlockStartEvent: getMediaType() returns the MIME type (e.g. "image/png").

  • DataBlockDeltaEvent: getData() returns incremental base64-encoded data.

Tool-call streaming events

ToolCallStartEvent — agent begins a tool call.

Method

Type

Description

getReplyId()

String

Reply message ID

getToolCallId()

String

Unique tool call ID

getToolCallName()

String

The tool being called

ToolCallDeltaEvent — incremental tool-call arguments arrive; getDelta() returns a JSON fragment.

ToolCallEndEvent — tool-call arguments complete.

Tool-result streaming events

ToolResultStartEvent — tool starts executing (carries toolCallId, toolCallName).

ToolResultTextDeltaEvent — incremental text output from the tool; getDelta() returns a text fragment.

ToolResultDataDeltaEvent — incremental binary output from the tool; similar to DataBlockDeltaEvent with mediaType / data / url.

ToolResultEndEvent — tool completes.

Method

Type

Description

getReplyId()

String

Reply message ID

getToolCallId()

String

The matching tool call ID

getState()

ToolResultState

Final state: SUCCESS, ERROR, INTERRUPTED, DENIED, RUNNING

Model-call events

ModelCallStartEvent — model API call starts (carries modelName).

ModelCallEndEvent — model API call completes (carries inputTokens / outputTokens).

Human-in-the-loop events

RequireUserConfirmEvent — agent pauses for user confirmation.

Method

Type

Description

getReplyId()

String

Reply message ID

getToolCalls()

List<ToolUseBlock>

Tool calls awaiting confirmation

RequireExternalExecutionEvent — agent pauses for external execution.

UserConfirmResultEvent — user provides confirmation results (input event); carries List<ConfirmResult>.

ExternalExecutionResultEvent — external system returns execution results (input event); carries List<ToolResultBlock>.

Reconstructing messages from events

Events and messages are not separate worlds — they are two views of the same data. The event stream from streamEvents can be aggregated by replyId / blockId / toolCallId to reconstruct a complete AssistantMessage, ensuring the final message state is fully recoverable from events alone.

See agentscope-core’s agent/StreamingHook.java and agentscope-examples/documentation/.../streaming/AgentEventStreamExample.java for the standard pattern of grouping by block ID and accumulating content with Reactor operators.

import io.agentscope.core.event.AgentEvent;
import io.agentscope.core.event.AgentStartEvent;
import io.agentscope.core.event.AgentEndEvent;
import io.agentscope.core.event.TextBlockDeltaEvent;
import io.agentscope.core.event.ToolCallStartEvent;
import io.agentscope.core.event.ToolResultEndEvent;

StringBuilder accumulated = new StringBuilder();

agent.streamEvents(userMsg)
        .doOnNext(event -> {
            if (event instanceof AgentStartEvent start) {
                System.out.println("[start replyId=" + start.getReplyId() + "]");
            } else if (event instanceof TextBlockDeltaEvent delta) {
                accumulated.append(delta.getDelta());
            } else if (event instanceof ToolCallStartEvent tc) {
                System.out.println("[tool] " + tc.getToolCallName());
            } else if (event instanceof ToolResultEndEvent end) {
                System.out.println("[tool result state=" + end.getState() + "]");
            } else if (event instanceof AgentEndEvent end) {
                System.out.println("\n[end] full text:\n" + accumulated);
            }
        })
        .blockLast();

Tip

This decoupling makes deployments flexible: the backend pushes the event stream over SSE, and the frontend reconstructs the message client-side. Even if the connection drops, replaying events from any checkpoint restores the message state precisely.

Example: streaming UI

A typical streaming UI loop (a Spring WebFlux SSE form is shown in streaming/StreamingWebExample.java):

import io.agentscope.core.event.AgentEndEvent;
import io.agentscope.core.event.AgentStartEvent;
import io.agentscope.core.event.TextBlockDeltaEvent;
import io.agentscope.core.event.ToolCallStartEvent;
import io.agentscope.core.event.ToolResultEndEvent;
import io.agentscope.core.message.UserMessage;

agent.streamEvents(new UserMessage("user", "Help me fix this bug"))
        .doOnNext(event -> {
            if (event instanceof AgentStartEvent start) {
                System.out.println("[start replyId=" + start.getReplyId() + "]");
            } else if (event instanceof TextBlockDeltaEvent delta) {
                System.out.print(delta.getDelta());
            } else if (event instanceof ToolCallStartEvent tc) {
                System.out.println("\n[calling " + tc.getToolCallName() + "...]");
            } else if (event instanceof ToolResultEndEvent end) {
                System.out.println("[tool finished: " + end.getState() + "]");
            } else if (event instanceof AgentEndEvent end) {
                System.out.println("\n[done]");
            }
        })
        .blockLast();

Further reading

Agent

How agents emit events and messages in the ReAct loop

./agent.html
Context

How messages are stored and persisted

../harness/context.html