## 已确认问题 ReAct 的模型流订阅仅用 `modelSubscription.compareAndSet(null, subscription)` 接入,没有检查运行是否已取消/失去执行权,也没有用已取消哨兵封闭接入。取消回调遇到尚未到达的 onSubscribe 时只能取消等待 Future;迟到的订阅随后仍被 request(Long.MAX_VALUE),已经取消的 Agent 反而启动了上游模型流。 代码:https://github.com/agentic-ai-java/argi/blob/b34835354b27f1f320ea8438edf2e9d170e69c84/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/react/ReActAgent.java#L405;https://github.com/agentic-ai-java/argi/blob/b34835354b27f1f320ea8438edf2e9d170e69c84/argi-agent/argi-agent-core/src/main/java/io/github/agentic/ai/agent/react/ReActAgent.java#L418。 ## 可控复现 1. 假 ChatClient.stream 返回一个 Publisher:subscribe 时保存 Subscriber,暂不调用 onSubscribe。 2. 使用 `ReActAgent.builder(client).streamModel(true)` 启动 invoke,等待假 Publisher 收到订阅。 3. 调用 Agent 返回 Future 的 cancel(true),使运行先完成取消。 4. 再向保存的 Subscriber 发送一个记录 request/cancel 次数的 Subscription。 实测:`requests=1, cancels=0`;期望 `requests=0, cancels=1`。不需要真实模型、外部服务或依赖网络抖动。此问题不同于正常完成时误取消共享上下文,也不是流式 fallback 的切换策略。 ## 影响 消费者认为运行已经停止,但迟到的上游仍可开始工作,可能继续占用连接与产生调用消耗;停止控制与实际执行脱节。 ## 交付与验收 - [ ] 将模型订阅接入与运行终止建立可靠的并发边界,取消后迟到的订阅立即取消,不再请求数据。 - [ ] 不以清空引用重新开放已终止订阅的接入;旧轮次的迟到回调也不能占据新轮次的订阅槽。 - [ ] 覆盖先取消后 onSubscribe、先 onSubscribe 后取消、并发竞态、重复订阅及正常多轮完成。 - [ ] 通过受控 Publisher/屏障验证调用次数和终止行为,不只断言外层 Future 已取消;检查所有权丢失等停止路径的同类行为。 - [ ] 保持取消向实际模型订阅传播;取消后的回调不再向下游交付事件。 ## 关联与边界 关联 #196、#195、#206。#206 负责额外结构化修复调用的停止边界;本单只修复独立 ReAct 主模型流的迟到订阅竞态,不扩展持久化调度或 fallback 能力。 ## 验证基线与变更约束 Core `b34835354b27f1f320ea8438edf2e9d170e69c84`;Extensions `cb2635ab2fc338e8aade28a132a15f48b4e655fc`。本轮 Core 19 模块 clean install 通过,2517 个测试中 173 个跳过;此处缺陷由额外的独立 Java 探针确认,说明已有测试尚未覆盖对应行为,不代表真实模型或生产环境验收。 优先修复现有契约的实现,不强制拆模块或重命名,不让独立 Agent/Workflow 反向依赖 Graph/Loop,不向 Core 引入供应商 SDK。若必须改变公开 API、事件、配置或持久格式,说明较小替代方案、消费者影响、收益、迁移成本与目标版本;不得通过删测试、吞错误或弱化取消保证获得通过。
Activity
konlue commentedon Oct 29, 2026
您好,我想认领 #276。
方案(单 PR,不动公开 API / 事件 / 配置)
onSubscribe前置校验:onSubscribe中 CAS 之后先判cancelled.get() || future.isCancelled();命中则compareAndSet(subscription, null)归还槽位并subscription.cancel(),不request。cancel()与onLoss回调统一改为getAndSet(null):谁拿到引用谁负责取消,天然实现exactly-once,也不会被后续清理抹掉。finally清理冲突;用“单调标志 + 原子所有权”两件事分别表达“能不能开始”和“谁负责取消”即可。cancel写标志发生在后的极窄窗口里仍可能 request 一次。这是响应式规范允许的(RS 3.6 要求 Publisher 自行处理 request-after-cancel),不为它加锁,PR 正文会如实声明。测试(离线,受控 Publisher,无真实模型)
新增
ReActStreamSubscriptionCancellationTest,共 5 个用例:requests=0/cancels=1(issue 原样复现);requests=1/cancels=0(防止把正常完成误取消);request、0 次cancel(抓finally清理缺失);context.control().cancel()路径(deadline/所有权丢失):迟到订阅同样不得request。fix(agent): cancel late model stream subscriptions after the run stop…
Merge pull request #338 from konlue/fix/react-late-stream-subscription