Files
AskaEth eb1828ee7a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/orchestration(29 文件,纯替换)
- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 裁决编号与 design 节号(02.4.3 / 11.4.2 / design/05 §9 等)逐字保留
- 复核:go build 绿;orchestration 及其子包(audit/comm)测试绿;gofmt 干净
2026-10-04 13:30:04 +08:00

173 lines
5.5 KiB
Go

package comm
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"sync/atomic"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
)
// Probe 是绑定到单个 agent id 的 agent 间通信端点。它可安全并发使用:唯一可变字段是
// 用于生成唯一 ask id 的原子序号计数器,且每个在途 ask 都带有自己的回复订阅与 channel。
type Probe struct {
bus bus.Bus
agentID string
seq atomic.Uint64
askTimeout time.Duration
log *slog.Logger
}
// Option 定制由 New 构建的 Probe。
type Option func(*Probe)
// WithLogger 替换探针的 logger。默认是丢弃式 logger,因此在配置之前探针是安静的。
func WithLogger(l *slog.Logger) Option {
return func(p *Probe) { p.log = newLogger(l) }
}
// WithAskTimeout 设置调用方上下文未携带更短期限时 Ask 等待回复的时长。非正值保持
// DefaultAskTimeout。
func WithAskTimeout(d time.Duration) Option {
return func(p *Probe) { p.askTimeout = d }
}
// New 构建一个绑定到 agentID 的 Probe。agentID 会被净化以便用于总线 subject
// (见 token);空 id 会 panic,因为每个探针都必须可路由。
func New(b bus.Bus, agentID string, opts ...Option) *Probe {
if agentID == "" {
panic("comm: New: agentID must not be empty")
}
p := &Probe{
bus: b,
agentID: token(agentID),
askTimeout: DefaultAskTimeout,
log: newLogger(nil),
}
for _, opt := range opts {
opt(p)
}
if p.askTimeout <= 0 {
p.askTimeout = DefaultAskTimeout
}
return p
}
// AgentID 返回该 Probe 所绑定到的已净化 id。
func (p *Probe) AgentID() string { return p.agentID }
// Ask 向 to 提一个问题并阻塞到匹配的 Reply 到达、ask 期限到期,或 ctx 被取消。成功时
// 返回回复;否则期限到期而无回复时返回 ErrTimeout,ask 完全无法投递时返回 ErrNoReply,
// ctx 被取消时返回包装后的上下文错误。
//
// 调用方上下文设了期限就用它;否则由 p.askTimeout 限制等待时长。回复订阅先于 ask 发布
// 创建,因此再快的应答方也不会被错过。
func (p *Probe) Ask(ctx context.Context, to, question string, data any) (Reply, error) {
if err := ctx.Err(); err != nil {
return Reply{}, fmt.Errorf("comm: ask to %q: %w", to, err)
}
askID := p.nextAskID()
from := p.agentID
replyCh := make(chan Reply, 1)
replySub := replySubject(to, from, askID)
unsub, err := p.bus.Subscribe(ctx, replySub, func(bctx context.Context, e bus.Event) error {
r, ok := e.Payload.(Reply)
if !ok {
return fmt.Errorf("comm: reply payload type %T, want Reply", e.Payload)
}
if r.AskID != askID {
return fmt.Errorf("comm: reply ask id %q, want %q", r.AskID, askID)
}
select {
case replyCh <- r:
default:
}
return nil
})
if err != nil {
return Reply{}, fmt.Errorf("comm: ask to %q: subscribe reply: %w: %w", to, ErrNoReply, err)
}
defer unsub()
ask := Ask{ID: askID, From: from, To: to, Question: question, Data: data}
if err := p.bus.Publish(ctx, askSubject(from, to), ask); err != nil {
return Reply{}, fmt.Errorf("comm: ask to %q: publish ask: %w: %w", to, ErrNoReply, err)
}
waitCtx, cancel := context.WithTimeout(ctx, p.askTimeout)
defer cancel()
select {
case r := <-replyCh:
return r, nil
case <-waitCtx.Done():
if errors.Is(waitCtx.Err(), context.Canceled) {
return Reply{}, fmt.Errorf("comm: ask to %q: %w", to, ctx.Err())
}
return Reply{}, fmt.Errorf("comm: ask to %q: %w", to, ErrTimeout)
}
}
// Reply 向 to 发布对 r.AskID 所标识 ask 的应答。r.From 为空时,应答方默认为本 Probe
// 的 agent id,因此应答处理函数只需传入回复文本与载荷。
func (p *Probe) Reply(ctx context.Context, to string, r Reply) error {
if r.From == "" {
r.From = p.agentID
}
if r.To == "" {
r.To = to
}
if err := p.bus.Publish(ctx, replySubject(r.From, to, r.AskID), r); err != nil {
return fmt.Errorf("comm: reply to %q: %w", to, err)
}
return nil
}
// Answer 是以纯文本回复的便捷方式。它向提问 agent to 发布一条针对 askID 的 Reply。
func (p *Probe) Answer(ctx context.Context, to, askID, text string, data any) error {
return p.Reply(ctx, to, Reply{AskID: askID, From: p.agentID, To: to, Text: text, Data: data})
}
// Send 投递一个专门发给 to 的事件。ev.From 为空时,发射方默认为本 Probe 的 agent id。
func (p *Probe) Send(ctx context.Context, to string, ev Event) error {
if ev.From == "" {
ev.From = p.agentID
}
if err := p.bus.Publish(ctx, eventSubject(ev.From, to), ev); err != nil {
return fmt.Errorf("comm: send event to %q: %w", to, err)
}
return nil
}
// PublishEvent 向每个订阅了事件的 agent 广播一个事件。ev.From 为空时,发射方默认为本
// Probe 的 agent id。
func (p *Probe) PublishEvent(ctx context.Context, ev Event) error {
if ev.From == "" {
ev.From = p.agentID
}
if err := p.bus.Publish(ctx, broadcastEventSubject(ev.From), ev); err != nil {
return fmt.Errorf("comm: publish event: %w", err)
}
return nil
}
// nextAskID 由 agent id 与一个原子计数器生成唯一的 ask id。可安全并发调用。
func (p *Probe) nextAskID() string {
return fmt.Sprintf("%s-%d", p.agentID, p.seq.Add(1))
}
// newLogger 返回标注了来源 orchestration/comm 的 l,并把 nil logger 换成丢弃式
// logger,使探针总能安全记录日志。
func newLogger(l *slog.Logger) *slog.Logger {
if l == nil {
l = slog.New(slog.NewTextHandler(io.Discard, nil))
}
return logger.WithSource(l, "orchestration/comm")
}