Solon v4.0.5

agent - 会话挂起(Session Pending)

</> markdown
2026年7月30日 下午5:30:11

在 Agent 的执行过程中,有时需要中断当前的计算流。例如:触发 HITL(Human-In-The-Loop,人机交互)等待人工审批,或是在特定条件下进行外部强行中断。

注意:

  • 挂起是一种临时状态。当该会话(Session)被重新触发执行时,系统会自动重置挂起状态(将 isPending 设为 false),以便计算图能够继续向下推进。
  • 无论是 ReActAgent(单智能体)还是 TeamAgent(多智能体团队),它们的 Trace 在每次 prepare() 时都会自动调用 session.pending(false, null) 重置挂起状态。

1、会话的挂起接口

通过 AgentSession 对象,可以对当前会话的运行状态进行精细化控制。一旦进入挂起状态,Agent 内部的计算图(Execution Graph)将停止推进。

方法描述
AgentSession::pending(pending, reason)设置挂起状态(true 为挂起,并需提供挂起原因;false 为恢复执行,此时 reason 传 null 会清除挂起原因)
AgentSession::isPending()判断当前会话是否处于挂起状态
AgentSession::getPendingReason()获取挂起原因(通常用于前端展示或后续逻辑判断)

内部机制说明

pending() 方法的默认实现(在 AgentSession 接口中)的工作流程如下:

default void pending(boolean pending, String reason) {
    FlowContextInternal contextInternal = ((FlowContextInternal) getContext());

    if (pending == true) {
        // 1. 调用 contextInternal.stop() 停止执行流
        // 2. 设置 stopped(true) 标记,后续计算图检测到此标记后停止推进
        contextInternal.stop();
        contextInternal.stopped(true);
    } else {
        // 清除 stopped 标记,允许继续执行
        contextInternal.stopped(false);
    }

    if (reason == null) {
        // 清除挂起原因
        getContext().remove(Agent.KEY_PENDING_REASON);
    } else {
        // 记录挂起原因(可用于前端展示)
        getContext().put(Agent.KEY_PENDING_REASON, reason);
    }
}

2、挂起应用场景

会话挂起主要分为 内部逻辑拦截 和 外部异步中断 两种模式。

通过拦截器挂起(适用于:同步、异步、流式响应)

这是最常用的场景,通常用于在工具执行(Action)前进行合规检查或人工授权。

public void useInterceptor() throws Throwable {
    ReActAgent agent = ReActAgent.of(null)
        .defaultInterceptorAdd(new ReActInterceptor() {
            @Override
            public void onToolCallStart(ReActTrace trace, ToolExchanger toolExchanger) {
                // 模拟 HITL 场景:根据业务逻辑决定是否挂起
                if ("delete_user".equals(toolExchanger.getToolName())) {
                    trace.getSession().pending(true, "敏感操作,等待人工审批");
                }
            }
        })
        .build();

    AgentSession session = InMemoryAgentSession.of();
    // 发起同步调用
    ReActResponse resp = agent.prompt("删除 ID 为 1001 的用户").session(session).call();

    // 检查挂起状态
    if (session.isPending()) {
        System.out.println("执行已挂起,原因:" + session.getPendingReason());
    }
}

注意:旧拦截器方法 onAction 已在 4.0.4 标记为 @Deprecated,推荐使用 onToolCallStart

外部主动中断(适用于:异步调用、流式响应)

在异步或流式任务执行过程中,外部根据用户指令或其他监控事件强行挂起会话。

  • 示例 A:异步调用中断
public void asyncInterrupt() throws Throwable {
    ReActAgent agent = ReActAgent.of(null).build();
    AgentSession session = InMemoryAgentSession.of();

    // 发起异步调用
    agent.prompt("生成一份万字长文报告").session(session).callAsync()
        .whenComplete((resp, err) -> {
            // 处理最终结果
        });

    // 模拟外部干预:立即强行挂起
    session.pending(true, "用户点击了停止生成");
}

  • 示例 B:流式调用中断
public void streamInterrupt() {
    ReActAgent agent = ReActAgent.of(null).build();
    AgentSession session = InMemoryAgentSession.of();

    // 发起流式订阅
    agent.prompt("查一下杭州今天的天气").session(session).stream()
        .doOnNext(event -> {
            // 处理流事件内容
        })
        .subscribe();

    // 在流传输过程中,外部通过 session 强行挂起计算图
    session.pending(true, "资源配额不足,自动挂起");
}

3、恢复执行

挂起的会话可以通过两种方式恢复:

方式一:重新发起调用时自动恢复

当重新对同一个 Session 调用 agent.prompt("新的指令").session(session).call() 时,Trace 的 prepare() 方法会自动调用 session.pending(false, null),清除挂起状态使计算图继续推进。

方式二:手动恢复

// 恢复执行
session.pending(false, null);
// 调用 agent.prompt().session(session).call() 继续推进

4、HITL 场景的完整配合

在 HITL(Human-In-The-Loop)场景中,挂起通常与 HITLPendingEventHITLDecidedEvent 两个流式事件配合使用:

  • 当拦截器触发挂起时,系统自动推送 HITLPendingEvent(携带被拦截的工具调用列表)到流式 Flux
  • 外部审批完成后,推送 HITLDecidedEvent(携带审批决策结果)到流式 Flux
  • 系统根据决策结果决定是否恢复执行

详细请参考 HITL 相关专题文章。