// Package comm implements agent-to-agent communication for the Cyrene v2 // orchestration layer (DevDocs/design/05-子代理.md §4). Subagents and the main // agent exchange messages over the in-process bus (internal/bus) using a // request/answer protocol built on three message kinds: ask, reply and event. // // # Protocol // // A Probe is bound to one agent id. It can ask another agent a question and // wait for its reply (ask/reply), or fire events at another agent or to anyone // listening (event). Messages are published to the bus with dot-separated // subjects so the bus's NATS-like wildcard matching routes them to the right // agent; see the "Subject convention" section below. // // # Subject convention // // Subjects are built from sanitized agent ids (any rune outside [A-Za-z0-9_-] // is replaced by "_", so an id never breaks a subject token): // // ask: cyrene.agent...ask // reply: cyrene.agent...reply. // event (to): cyrene.agent...event // event (bc): cyrene.agent..event // // The reply subject carries the ask id as its last token, giving each in-flight // ask a private reply address so concurrent asks from the same agent never // cross replies. To receive asks addressed to it, an agent subscribes to // cyrene.agent.*..ask. To receive events (both broadcasts and events // addressed to it) it subscribes to cyrene.agent.*.event and to // cyrene.agent.*..event. // // # Delivery semantics // // Ask blocks until a reply arrives, an ask-timeout elapses, or the passed // context is canceled; it returns ErrTimeout on the former and the wrapped // context error on the latter. Reply and the event senders are fire-and-forget // publishes that return the bus error (if any). Handler callbacks are invoked // on the bus's per-subscription worker goroutines, so an EventHandler may be // called concurrently from two subscriptions and must be safe for concurrent // use; an AskHandler for one agent is serialized by the bus. package comm import ( "errors" "strings" "time" ) // Kind is the wire kind of an agent-to-agent message. type Kind string const ( // KindAsk is a question from one agent to another, answered by a reply. KindAsk Kind = "ask" // KindReply is the answer to a previously published ask. KindReply Kind = "reply" // KindEvent is a one-directional notification, broadcast or addressed. KindEvent Kind = "event" ) // subjectPrefix is the fixed first two tokens of every agent-to-agent subject. const subjectPrefix = "cyrene.agent" // DefaultAskTimeout caps how long Ask waits for a reply when the caller's // context carries no shorter deadline. It bounds a request/answer round so a // silent agent never hangs its caller. const DefaultAskTimeout = 30 * time.Second // Sentinel errors returned by the package. Use errors.Is to test for them; they // are returned directly or wrapped with %w so the underlying cause stays // available. var ( // ErrTimeout is returned by Ask when no reply arrives before the ask // deadline (the caller's context deadline or DefaultAskTimeout) elapses. ErrTimeout = errors.New("comm: reply timeout") // ErrNoReply is returned by Ask when the ask cannot be delivered (the bus // rejected the publish/reply subscription), so no reply is expected. ErrNoReply = errors.New("comm: no reply") ) // Ask is a question sent from one agent to another. type Ask struct { // ID uniquely identifies this ask within the answering exchange. ID string // From is the id of the asking agent (the sender). From string // To is the id of the agent that should answer (the receiver). To string // Question is the human-readable question text. Question string // Data carries an optional structured payload alongside the question. Data any } // Reply is the answer to a previously published ask. type Reply struct { // AskID is the ID of the ask this reply answers. AskID string // From is the id of the replying agent (the sender). From string // To is the id of the asking agent (the receiver). To string // Text is the human-readable answer text. Text string // Data carries an optional structured payload alongside the answer. Data any } // Event is a one-directional notification, either broadcast or addressed to one // agent. It has no reply. type Event struct { // From is the id of the emitting agent. From string // Name is a short event name, e.g. "task.progress" or "tool.used". Name string // Data carries an optional structured payload. Data any } // token sanitizes an agent id so it can be used as a single subject token: any // rune outside [A-Za-z0-9_-] is replaced with "_". This keeps the id useful for // routing while guaranteeing dots (and the other subject metacharacters) never // leak into a token. func token(s string) string { var b strings.Builder for _, r := range s { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_': b.WriteRune(r) default: b.WriteRune('_') } } return b.String() } // subjectFor builds the base subject for a from/to message of the given kind. func subjectFor(from, to string, kind Kind) string { return subjectPrefix + "." + token(from) + "." + token(to) + "." + string(kind) } // askSubject is the subject an ask from→to is published to. func askSubject(from, to string) string { return subjectFor(from, to, KindAsk) } // replySubject is the private subject a reply from→to answering askID is // published to. The trailing askID token gives each in-flight ask its own // address so replies never cross between concurrent asks. func replySubject(from, to, askID string) string { return subjectPrefix + "." + token(from) + "." + token(to) + "." + string(KindReply) + "." + token(askID) } // eventSubject is the subject of an event addressed from→to. func eventSubject(from, to string) string { return subjectFor(from, to, KindEvent) } // broadcastEventSubject is the subject of a broadcast event emitted by from. func broadcastEventSubject(from string) string { return subjectPrefix + "." + token(from) + "." + string(KindEvent) } // askReceivePattern is the subscription pattern an agent named me uses to // receive asks addressed to it from any sender. func askReceivePattern(me string) string { return subjectPrefix + ".*." + token(me) + "." + string(KindAsk) } // eventReceivePatterns are the subscription patterns an agent named me uses to // receive every event it should see: broadcasts from any sender and events // addressed specifically to it. func eventReceivePatterns(me string) []string { return []string{ subjectPrefix + ".*." + string(KindEvent), subjectPrefix + ".*." + token(me) + "." + string(KindEvent), } }