This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+46 -2
View File
@@ -71,6 +71,14 @@ type Relay struct {
log *slog.Logger
cfg RelayConfig
// 回合完成检测:被动解析 session/update + session/prompt 响应的 stopReason。
// 由 supervisor.Spawn 通过 SetTracker 注入;可为 nil(部分测试)。
tracker *TurnTracker
// 成本核算:被动解析 agent→client 消息中的 token/cost 用量并累加 + 预算判定。
// 由 supervisor.Spawn 通过 SetUsageSink 注入;可为 nil(部分测试 / 未配置)。
usageSink usageSink
// 终止控制
closed atomic.Bool
done chan struct{}
@@ -106,6 +114,17 @@ func NewRelay(sessionID uuid.UUID, dec *Decoder, enc *Encoder,
}
}
// SetTracker 注入回合追踪器。由 supervisor.Spawn 在 NewRelay 之后调用。
func (r *Relay) SetTracker(t *TurnTracker) { r.tracker = t }
// SetUsageSink 注入成本核算 sink。由 supervisor.Spawn 在 NewRelay 之后调用;
// 可为 nil(部分测试 / 未配置 model 价格时由 supervisor 自行决定是否注入)。
func (r *Relay) SetUsageSink(s usageSink) { r.usageSink = s }
// Tracker 返回回合追踪器(可能为 nil)。session_service / handler 在发送
// session/prompt 前用它登记回合 id。
func (r *Relay) Tracker() *TurnTracker { return r.tracker }
// Subscribe 注册一个 WS tab 订阅。
func (r *Relay) Subscribe(userID uuid.UUID) *Subscriber {
sub := &Subscriber{
@@ -228,6 +247,12 @@ func (r *Relay) reader(ctx context.Context, sess handlers.SessionContext) {
envelope := r.persistAndEnvelope(ctx, msg, "out")
if envelope != nil {
r.fanout(envelope)
// 成本核算:在事件落库后用持久化的 event id 喂给 usageSink。usageSink
// 内部仅在真实可计费回合时累加,并在预算突破时以 detached goroutine 触发
// kill(绝不阻塞 reader)。
if r.usageSink != nil {
r.usageSink.Observe(ctx, msg, envelope.ID)
}
}
switch {
@@ -238,10 +263,23 @@ func (r *Relay) reader(ctx context.Context, sess handlers.SessionContext) {
} else {
r.log.Warn("acp.relay.orphan_response", "session_id", r.sessionID, "id", id)
}
} else if sid, ok := msg.IDString(); ok && r.tracker.IsTrackedPrompt(sid) {
// 字符串-id 的 session/prompt 响应:回合完成检测。
// 仅当 id 匹配当前开放回合时拦截;其它字符串 id 仍走既有 fanout(上方已 fanout)。
if msg.Error != nil {
// JSON-RPC error 响应:本回合失败,标 aborted 并发 session_idle(reason=error),
// 而非误记为 completed(stopReason 空)。agent 仍存活,可接受下一条 prompt。
r.tracker.Abort(ctx, "error")
} else {
r.tracker.OnPromptResponse(ctx, sid, ParseStopReason(msg.Result))
}
}
// String IDs go to clients via fanout (handled above); no server action.
// 其它 String IDs go to clients via fanout (handled above); no server action.
case msg.IsNotification():
// Already persisted + fanout'd. No reply.
// Already persisted + fanout'd. No reply. 回合检测:累加 update 计数。
if msg.Method == "session/update" {
r.tracker.OnUpdate(ctx)
}
case msg.IsRequest():
go r.handleAgentRequest(ctx, sess, msg)
}
@@ -272,6 +310,12 @@ func (r *Relay) handleAgentRequest(ctx context.Context, sess handlers.SessionCon
case "fs/write_text_file":
out := r.fsHandler.Write.Handle(ctx, sess, req.Params)
resp = r.fsResultToResponse(req.ID, out)
case "fs/list":
out := r.fsHandler.List.Handle(ctx, sess, req.Params)
resp = r.fsResultToResponse(req.ID, out)
case "fs/grep":
out := r.fsHandler.Grep.Handle(ctx, sess, req.Params)
resp = r.fsResultToResponse(req.ID, out)
case "session/request_permission":
out := r.permHandler.Handle(ctx, sess, string(req.ID), req.Params)
resp = r.fsResultToResponse(req.ID, out)