feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Kind identifies which messaging fabric a bot runs on.
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
// KindMatrix is the proven Matrix (mautrix) path — the default, so master
|
||||
// stays on the battle-tested transport.
|
||||
KindMatrix Kind = "matrix"
|
||||
// KindUnibus routes the bot over the unibus message bus.
|
||||
KindUnibus Kind = "unibus"
|
||||
)
|
||||
|
||||
// Select chooses a bot's transport. unibus is used only when the global feature
|
||||
// flag is enabled AND the bot has opted in; otherwise Matrix. This is the
|
||||
// branch-by-abstraction toggle: with the flag on, bots migrate to unibus one at
|
||||
// a time by opting in, while every other bot keeps speaking Matrix unchanged.
|
||||
func Select(flagEnabled, botOptIn bool) Kind {
|
||||
if flagEnabled && botOptIn {
|
||||
return KindUnibus
|
||||
}
|
||||
return KindMatrix
|
||||
}
|
||||
|
||||
// flagsFile mirrors dev/feature_flags.json (see .claude rule feature_flags.md).
|
||||
type flagsFile struct {
|
||||
Flags map[string]struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Issue string `json:"issue"`
|
||||
Description string `json:"description"`
|
||||
} `json:"flags"`
|
||||
}
|
||||
|
||||
// FlagEnabled reports whether the named feature flag is enabled in the given
|
||||
// dev/feature_flags.json file. A missing file or missing flag reads as false
|
||||
// (fail-safe: default to the Matrix path), not an error — only malformed JSON
|
||||
// surfaces an error.
|
||||
func FlagEnabled(path, name string) (bool, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("transport: read flags %q: %w", path, err)
|
||||
}
|
||||
var f flagsFile
|
||||
if err := json.Unmarshal(data, &f); err != nil {
|
||||
return false, fmt.Errorf("transport: parse flags %q: %w", path, err)
|
||||
}
|
||||
return f.Flags[name].Enabled, nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSelect covers the branch-by-abstraction toggle: unibus only when the flag
|
||||
// is on AND the bot opted in; Matrix in every other combination (the default,
|
||||
// so unmigrated bots keep working).
|
||||
func TestSelect(t *testing.T) {
|
||||
cases := []struct {
|
||||
flag, optIn bool
|
||||
want Kind
|
||||
}{
|
||||
{true, true, KindUnibus},
|
||||
{true, false, KindMatrix},
|
||||
{false, true, KindMatrix},
|
||||
{false, false, KindMatrix},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := Select(c.flag, c.optIn); got != c.want {
|
||||
t.Errorf("Select(flag=%v, optIn=%v) = %q, want %q", c.flag, c.optIn, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagEnabled(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "feature_flags.json")
|
||||
const content = `{"flags":{"unibus-transport":{"enabled":true,"issue":"x"},"off":{"enabled":false}}}`
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write flags: %v", err)
|
||||
}
|
||||
|
||||
on, err := FlagEnabled(path, "unibus-transport")
|
||||
if err != nil {
|
||||
t.Fatalf("FlagEnabled: %v", err)
|
||||
}
|
||||
if !on {
|
||||
t.Errorf("expected unibus-transport enabled")
|
||||
}
|
||||
|
||||
off, err := FlagEnabled(path, "off")
|
||||
if err != nil {
|
||||
t.Fatalf("FlagEnabled off: %v", err)
|
||||
}
|
||||
if off {
|
||||
t.Errorf("expected off flag disabled")
|
||||
}
|
||||
|
||||
// Missing flag and missing file both read as false (fail-safe to Matrix).
|
||||
if missing, err := FlagEnabled(path, "does-not-exist"); err != nil || missing {
|
||||
t.Errorf("missing flag should be (false, nil), got (%v, %v)", missing, err)
|
||||
}
|
||||
if absent, err := FlagEnabled(filepath.Join(dir, "nope.json"), "x"); err != nil || absent {
|
||||
t.Errorf("absent file should be (false, nil), got (%v, %v)", absent, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Package transport defines the neutral boundary between an agent's core logic
|
||||
// and the messaging fabric it runs on. It carries NO Matrix (mautrix) types, so
|
||||
// the same agent code can be driven by Matrix today and by the unibus message
|
||||
// bus tomorrow, selected per bot behind a feature flag (branch by abstraction).
|
||||
//
|
||||
// The two pieces are:
|
||||
//
|
||||
// - InboundMessage: a transport-neutral description of an incoming message.
|
||||
// Both the Matrix listener and the unibus subscriber produce one of these.
|
||||
// - Transport: the capability an agent depends on to receive messages and
|
||||
// send replies. A Matrix adapter and a unibus adapter both implement it.
|
||||
package transport
|
||||
|
||||
import "context"
|
||||
|
||||
// InboundMessage is a transport-neutral incoming message. It is the single type
|
||||
// an agent's message handler receives, regardless of whether the underlying
|
||||
// fabric is Matrix or unibus. It deliberately avoids any mautrix type.
|
||||
type InboundMessage struct {
|
||||
// RoomID identifies the conversation on the transport (a Matrix room id or a
|
||||
// unibus room id). Replies are addressed back to it.
|
||||
RoomID string
|
||||
// Subject is the bus subject the message arrived on (unibus). Empty for
|
||||
// transports that do not have a subject address space (Matrix).
|
||||
Subject string
|
||||
|
||||
SenderID string // stable id of the sender (Matrix user id / unibus endpoint id)
|
||||
SenderName string // human-friendly display name, when the transport knows it
|
||||
|
||||
// MsgID is the unique id of this message on its transport: a Matrix event id
|
||||
// or a unibus frame MsgID. Used as the reply/thread anchor.
|
||||
MsgID string
|
||||
ThreadID string // root message id of the thread, empty if not threaded
|
||||
ReplyTo string // message id this message replies to, empty if none
|
||||
|
||||
Body string // plaintext body / content of the message
|
||||
Command string // parsed command name (e.g. "deploy"), empty if not a command
|
||||
Args []string // parsed command arguments
|
||||
|
||||
PowerLevel int // sender power level where the transport models one (Matrix); 0 otherwise
|
||||
IsDirectMsg bool // the message is a direct/1:1 message to the bot
|
||||
IsMention bool // the message addresses/mentions the bot
|
||||
}
|
||||
|
||||
// OutboundReply is a transport-neutral outgoing reply.
|
||||
type OutboundReply struct {
|
||||
RoomID string // conversation to reply into
|
||||
Subject string // bus subject to publish to (unibus); ignored by Matrix
|
||||
ReplyTo string // message id being replied to (renders as a reply)
|
||||
ThreadID string // thread root to keep the reply inside, empty for top-level
|
||||
Markdown string // reply body, in markdown
|
||||
}
|
||||
|
||||
// Handler processes one inbound message. It is the callback an agent registers
|
||||
// with a Transport via Run.
|
||||
type Handler func(ctx context.Context, in InboundMessage)
|
||||
|
||||
// Transport is the messaging fabric an agent depends on. Implementations:
|
||||
// - a Matrix adapter wrapping the existing mautrix client + listener;
|
||||
// - a unibus adapter over github.com/enmanuel/unibus/pkg/client.
|
||||
//
|
||||
// An agent core that depends only on Transport (not on *mautrix.Client) can be
|
||||
// pointed at either fabric without code changes.
|
||||
type Transport interface {
|
||||
// Run delivers each inbound message to handler until ctx is cancelled. It
|
||||
// blocks for the lifetime of the subscription and returns ctx.Err() (or a
|
||||
// transport error) when it stops.
|
||||
Run(ctx context.Context, handler Handler) error
|
||||
// Reply sends a reply addressed by the OutboundReply envelope.
|
||||
Reply(ctx context.Context, out OutboundReply) error
|
||||
// Send posts a standalone markdown message to a conversation (no reply anchor).
|
||||
Send(ctx context.Context, roomID, markdown string) error
|
||||
// Close releases the underlying connection.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// PresenceController is an optional capability for transports that model online
|
||||
// presence (Matrix). unibus does not, so it simply does not implement this and
|
||||
// callers type-assert for it.
|
||||
type PresenceController interface {
|
||||
SetPresence(ctx context.Context, online bool) error
|
||||
}
|
||||
|
||||
// TypingController is an optional capability for transports that model typing
|
||||
// indicators (Matrix). Callers type-assert for it.
|
||||
type TypingController interface {
|
||||
SetTyping(ctx context.Context, roomID string, typing bool) error
|
||||
}
|
||||
Reference in New Issue
Block a user