Skip to content

feat(context-enhance): add RuntimeContext onStateLoaded callback for anchor-based AG-UI message auto-merge - #2533

Open
xzxiaoshan wants to merge 6 commits into
agentscope-ai:mainfrom
xzxiaoshan:feat/context-onstateloaded-aguimessage-automerge
Open

feat(context-enhance): add RuntimeContext onStateLoaded callback for anchor-based AG-UI message auto-merge#2533
xzxiaoshan wants to merge 6 commits into
agentscope-ai:mainfrom
xzxiaoshan:feat/context-onstateloaded-aguimessage-automerge

Conversation

@xzxiaoshan

Copy link
Copy Markdown
Contributor

新增 RuntimeContext onStateLoaded 回调设计 & AG-UI 消息自动融合

为 RuntimeContext 提供 AgentState 加载后置回调,使调用方能在 state 就绪后、doCall 执行前,
基于 AgentState 对 msgs 或 ctx 做二次处理。
以此替代 AG-UI processor 中 hasMemory + extractLatestUserMessage 的粗暴处理(消息的衔接与否跟是否内存无关,跟上下文中是否有历史消息有关),
实现基于 messageId 锚点的全量/增量消息自动融合。

一、背景:AgentState 的时序缺口

AgentState 在 agent 生命周期的 beforeAgentExecution 中才加载(从 stateStore 或 stateCache),而此时 agent 调用已发起,调用方无法再直接处理消息(不管是 context 还是 msgs,有些场景我们是需要基于 agentstate 做对应处理的):

调用方准备 msgs → agent.streamEvents(msgs) → runLifecycle → beforeAgentExecution 加载 state
                                                                    ↓
                                                              state 就绪,但 msgs 已传入
                                                              调用方已无机会基于 state 处理 msgs

这个时序缺口导致:调用方知道需要基于持久化状态处理消息(如去重、注入、过滤),但拿不到 state,只能在 processor 层用 hasMemory 粗暴判断 + extractLatestUserMessage 简化处理。

二、方案:onStateLoaded 回调

原理

调用方在 RuntimeContext 上设置 onStateLoaded 回调。agent 生命周期在 state 加载完成后、doCall 执行前触发它。调用方在回调中基于 AgentState 做任何需要的二次处理。

语义链(均在 beforeAgentExecution 内):
  activateSlotForContext 加载 state
          ↓
  ctx.setAgentState(scope.state)         ← state 加载完成
          ↓
  onStateLoaded 触发(调用方基于 state 二次处理 msgs / ctx)
          ↓
  doCall 执行(传入处理后的 msgs)

回调签名

BiConsumer<RuntimeContext, List<Msg>>

无返回值,回调内三个对象全可访问:

对象 获取方式 用途
RuntimeContext 直接参数 put 属性、读取配置
AgentState ctx.getAgentState() 读取 context 历史、summary、permission 等
List<Msg> 直接参数(可变列表) 原地修改:过滤、去重、注入

AgentState 在 beforeAgentExecution 中通过 ctx.setAgentState(scope.state) 挂载ReActAgent.java,回调触发时 ctx.getAgentState() 可直接获取。

核心代码变更

RuntimeContext 新增属性

RuntimeContext.java

// JDK BiConsumer,无需自定义接口
private volatile BiConsumer<RuntimeContext, List<Msg>> onStateLoaded;

public BiConsumer<RuntimeContext, List<Msg>> getOnAgentStateReady() {
    return onStateLoaded;
}

public void setOnAgentStateReady(
        BiConsumer<RuntimeContext, List<Msg>> onStateLoaded) {
    this.onStateLoaded = onStateLoaded;
}
// Builder
public Builder onStateLoaded(
        BiConsumer<RuntimeContext, List<Msg>> onStateLoaded) {
    this.onStateLoaded = onStateLoaded;
    return this;
}

// Builder.from() 拷贝 + 构造函数赋值
this.onStateLoaded = source.onStateLoaded;
this.onStateLoaded = builder.onStateLoaded;

ReActAgent 在 beforeAgentExecution 中触发

ReActAgent.java ,紧跟 ctx.setAgentState(scope.state) 之后:

@Override
protected Object beforeAgentExecution(List<Msg> msgs, RuntimeContext rc) {
    // ... existing setup ...
    CallExecution scope = activateSlotForContext(ctx);
    ctx.setAgentState(scope.state);

    // state 加载完成,触发 onStateLoaded 回调(可能原地修改 msgs)
    BiConsumer<RuntimeContext, List<Msg>> handler = ctx.getOnAgentStateReady();
    if (handler != null) {
        handler.accept(ctx, msgs);
    }

    // ... rest of setup ...
    return scope;
}

三、应用场景:消息自动合并

作为 onStateLoaded 的首个应用场景,一并优化当前 AG-UI processor 的消息处理逻辑。

问题

AguiRequestProcessor.java 在判定有内存时,仅抽取最后一条 UserMessage:

if (agentResolver.hasMemory(threadId)) {
    effectiveInput = extractLatestUserMessage(input);
}
  1. 丢失同轮其他消息 — 隐性 system 消息、Tool 调用对、当前轮次附加的其他多消息全部丢弃
  2. hasMemory 与上下文管理职责分离 — processor 判断"有没有内存",agent 管理上下文,两处各自假设
  3. 仅适用于 AG-UI 入口 — 直接调用 agent.call() 的入口无法享受去重逻辑

锚点去重实现

通过 onStateLoaded 回调实现。用 messageId 锚点判断传入消息中哪些是新的:

已有 context: [m1, m2, m3]          ← m3 是锚点 (context 的最后一条)
传入 msgs:    [m1, m2, m3, m4, m5]  ← 全量 input
                     ↑ 锚点命中
有效增量:                   [m4, m5]
  • 锚点命中 → 传入是全量,原地删除锚点及之前的消息
  • 锚点未命中 → 传入是增量,不动
  • context 为空 → 无锚点,不动

messageId 在 AguiMessage → Msg 转换中完整保留(AguiMessageConverter.javaMsg.builder().id(aguiMessage.getId())),锚点匹配可靠。

AguiAgentAdapter 设置回调

AguiAgentAdapter.java buildRuntimeContext

.onStateLoaded((ctx, msgs) -> {
    AgentState state = ctx.getAgentState();
    if (state == null) return;
    List<Msg> context = state.getContext();
    if (context.isEmpty()) return;
    String anchorId = context.getLast().getId();
    for (int i = msgs.size() - 1; i >= 0; i--) {
        if (anchorId.equals(msgs.get(i).getId())) {
            msgs.subList(0, i + 1).clear();     // 原地删除锚点及之前
            return;
        }
    }
})

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...e/src/main/java/io/agentscope/core/ReActAgent.java 33.33% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@xzxiaoshan

xzxiaoshan commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

关于 onStateLoaded 的设计,对 msgList 的自动衔接处理,这只是一个场景。
核心作用是为了在拿到持久化的 agentstate 后,可以根据 state 中的状态内容,决定其他相关的业务处理,为业务场景提供一个切入点。
比如根据 state 中的内容,决定 RuntimeContext 中的 input.getContext() 和 input.getForwardedProps() 中相关属性的使用,等等。
属于一个增强扩展回调函数。

@xzxiaoshan

xzxiaoshan commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@jujn 方便的时候看下,谢谢
#2463
#2533
#2531

@AgentScopeJavaBot AgentScopeJavaBot added enhancement New feature or request area/core/agent Agent runtime, pipeline, hooks, plan area/ext/integration External protocols & middleware integrations labels Aug 11, 2026

@AgentScopeJavaBot AgentScopeJavaBot left a comment

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.

🤖 AI Review

This PR introduces an onStateLoaded lifecycle callback on RuntimeContext, fired after AgentState is loaded in ReActAgent.beforeAgentExecution(). The AG-UI adapter leverages this hook to implement anchor-based message deduplication, replacing the crude hasMemory + extractLatestUserMessage approach. Well-designed PR with clear intent, clean separation of concerns, and solid test coverage. Two actionable issues should be addressed before merge.

@AgentScopeJavaBot AgentScopeJavaBot left a comment

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.

🤖 AI Review

This PR introduces an onStateLoaded lifecycle callback on RuntimeContext, fired after AgentState is loaded in ReActAgent.beforeAgentExecution(). The AG-UI adapter leverages this hook to implement anchor-based message deduplication, replacing the crude hasMemory + extractLatestUserMessage approach. Well-designed PR with clear intent, clean separation of concerns, and solid test coverage. Two actionable issues should be addressed before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core/agent Agent runtime, pipeline, hooks, plan area/ext/integration External protocols & middleware integrations enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants