feat(orchestration,gateway): 子代理结果登 WS 帧 subagent_result(裁决 02.4.3)

- 编排层改为在 subagent.result 上发布带父会话归属的 SubagentResultEvent,后台批次结果不再消失在进程内总线
- gateway 中继:新帧常量 wsSubagentResult="subagent_result",eventBridge/bridgePatterns 登记该 topic,normalizeEvent 专用成形(帧壳 sessionId = 派生该批次的父会话)
- 角色门 fail-closed:未登记进 frameReachesAll,owner/admin 可达、viewer 不可达,真 JWT 三角色 WS 断言钉住
- 前景(2s 内跑完)批次仍只回工具结果,不发此帧
This commit is contained in:
AskaEth committed 2026-10-04 12:07:46 +08:00
1 parent 0ce498754a
commit 8c6466cb00
4 files changed
+295 -22

No files matched your search

+19 -9
View File
@@ -42,11 +42,11 @@ const (
minSpawnTimeoutSeconds = 1
maxSpawnTimeoutSeconds = 24 * 60 * 60
// SubagentResultSubject is the bus subject a finished background batch is
// announced on, carrying the *MergedResult. It is the design/05 §9 结果回填 hook
// for whichever component owns the session message queue (design/15); the
// WS-visible per-agent status already travels on the scheduler's own progress
// events, so this subject has no consumer until that queue lands.
// SubagentResultSubject 是后台批次结算后广播其结果的主题,载荷是
// *SubagentResultEvent(父会话 + 批次结果)。它是 design/05 §9「结果回填主
// 会话并提示用户」的载体,消费者是 gateway 的中继:该主题被登记为 WS 下行帧
// subagent_result(Docs/api-client.md §3.10),帧按载荷里的父会话归属,因此结果
// 落回父会话的实时流而非悬空 hook;逐代理进行中状态仍走调度器进度事件。
SubagentResultSubject = "subagent.result"
)
@@ -191,7 +191,7 @@ func (t *SpawnTool) Execute(ctx context.Context, args any) (any, error) {
"persona", specs[0].PersonaID, "mode", specs[0].Mode, "session", specs[0].ParentSession)
if boolArg(m, "background") {
go t.await(ids)
go t.await(ids, specs[0].ParentSession)
return runningSpawnResult(ids, specs), nil
}
return t.foreground(ids, specs), nil
@@ -206,7 +206,7 @@ func (t *SpawnTool) foreground(ids []string, specs []AgentSpec) any {
res, err := t.sched.Wait(waitCtx, ids)
if err != nil {
t.log.Info("sub-agent batch moved to background", "count", len(ids), "waited", t.wait.String())
go t.await(ids)
go t.await(ids, specs[0].ParentSession)
return runningSpawnResult(ids, specs)
}
return doneSpawnResult(res)
@@ -215,7 +215,9 @@ func (t *SpawnTool) foreground(ids []string, specs []AgentSpec) any {
// await blocks until the batch settles and broadcasts the merged outcome as the
// design/05 §2 "结束后写执行总结 → 返回主会话" hand-off. It runs on a detached
// goroutine; a nil bus or an unknown id ends it quietly.
func (t *SpawnTool) await(ids []string) {
// parentSession 是该批次所属的父会话,随载荷一起广播,使中继能把回填结果
// 归属到父会话的实时流,而不是发一张无归属的帧。
func (t *SpawnTool) await(ids []string, parentSession string) {
res, err := t.sched.Wait(context.Background(), ids)
if err != nil {
t.log.Warn("sub-agent batch wait failed", "count", len(ids), "error", err)
@@ -227,7 +229,8 @@ func (t *SpawnTool) await(ids []string) {
if b == nil {
return
}
if perr := b.Publish(context.Background(), SubagentResultSubject, res); perr != nil {
payload := &SubagentResultEvent{SessionID: parentSession, SpawnResult: doneSpawnResult(res)}
if perr := b.Publish(context.Background(), SubagentResultSubject, payload); perr != nil {
t.log.Warn("publish sub-agent results", "error", perr)
}
}
@@ -265,6 +268,13 @@ type SpawnAgentResult struct {
Steps int `json:"steps,omitempty"`
}
// SubagentResultEvent 是后台批次结算后发布的回填载荷(design/05 §9;契约见
// Docs/api-client.md §3.10):SessionID 为父会话,其余字段即派发工具返回给模型的结果。
type SubagentResultEvent struct {
SessionID string `json:"sessionId,omitempty"`
*SpawnResult
}
// doneSpawnResult maps a settled batch onto the tool result.
func doneSpawnResult(res *MergedResult) *SpawnResult {
out := &SpawnResult{Status: "done", Agents: make([]SpawnAgentResult, 0)}