agentbridge

package
v4.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 5, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package agentbridge implements the live-session agent control surface for GWC apps: the gwc.agentbridge wire protocol spoken between a running js/wasm app (which dials out to the agent hub) and the hub that relays commands from MCP tools.

This file set is layered like hotreload: the protocol and any other pure-Go logic are tag-free so they build, vet, and unit-test on both native and js/wasm targets; browser-coupled transport lives in _wasm.go files with _native.go stubs.

Design rule for every command this protocol carries: mutations target the inputs the fiber tree is derived from (atoms, hook slots, events, route, mount roots) - never fibers directly, which the reconciler would overwrite.

Index

Constants

View Source
const (
	// KindHello is the first frame an app sends after connecting: app id,
	// build id, and capabilities ride in the payload.
	KindHello = "hello"
	// KindCommand is a hub-to-app instruction (snapshot, query, set-atom, ...).
	KindCommand = "command"
	// KindAck is the app's reply to exactly one command, matched by AckSeq.
	KindAck = "ack"
	// KindEvent is an unsolicited app-to-hub push (log, diagnostic, lifecycle).
	KindEvent = "event"
)

Envelope kinds. Every frame on the wire is exactly one of these.

View Source
const (
	// ErrorCodeStaleRef means a node ref no longer resolves; the agent should
	// re-query instead of retrying the same ref.
	ErrorCodeStaleRef = "stale-ref"
	// ErrorCodeUnknownCommand means the app build does not implement the
	// requested command name.
	ErrorCodeUnknownCommand = "unknown-command"
	// ErrorCodeBadPayload means the command payload failed validation; state
	// was not touched.
	ErrorCodeBadPayload = "bad-payload"
	// ErrorCodeForbidden means the app is not in agent mode or the caller
	// lacks the session write lease.
	ErrorCodeForbidden = "forbidden"
	// ErrorCodeTimeout means a wait command reached its deadline before the
	// requested runtime condition became true.
	ErrorCodeTimeout = "timeout"
)

Command-layer error codes carried in EnvelopeError.Code. These are part of the wire contract: agents branch on them, so they are stable strings.

View Source
const ProtocolName = "gwc.agentbridge"

ProtocolName identifies the agent bridge wire protocol in every envelope.

View Source
const ProtocolVersion = 1

ProtocolVersion is the newest envelope schema version this build speaks. Parsers reject newer versions loudly instead of guessing.

Variables

This section is empty.

Functions

func EnableAgentBridge

func EnableAgentBridge()

EnableAgentBridge is a no-op stub for non-browser or non-agent builds. Applications call EnableAgentBridge unconditionally at startup; this stub compiles it out on all builds that do not have the js, wasm, and gwcagent build tags simultaneously active so release builds carry zero overhead.

func FormatEnvelopeJSON

func FormatEnvelopeJSON(parseEnvelope Envelope) (string, error)

FormatEnvelopeJSON validates an envelope and serializes it for the wire.

func IsAgentModeActive

func IsAgentModeActive() bool

IsAgentModeActive reports whether the bridge client enabled agent mode.

func ListAgentCommands

func ListAgentCommands() []string

ListAgentCommands returns the sorted names of every registered command.

func ListMountComponents

func ListMountComponents() []string

ListMountComponents returns the sorted names of every component registered for bridge.mount via RegisterMountComponent. It is the discovery surface an agent uses to learn what bridge.mount accepts, instead of guessing component names.

func RecordAgentMutation

func RecordAgentMutation(parseCommand string, parseSummary string, parseUndo func() *EnvelopeError) uint64

RecordAgentMutation appends an audit entry for an applied write command and, when parseUndo is non-nil, pushes a reversible op onto the undo stack. It returns the assigned audit sequence number. parseUndo must restore the exact prior state and is safe to call once.

func RegisterAgentCommand

func RegisterAgentCommand(parseName string, parseHandler AgentCommandHandler)

RegisterAgentCommand registers a bridge command handler by wire name. Later registrations replace earlier ones (last writer wins) so tests can stub commands; production registration happens once at bridge install.

A blank name or nil handler is silently ignored (no registration, no panic), so guard against accidentally passing either if you rely on the command being present.

func RegisterControlCommands

func RegisterControlCommands()

RegisterControlCommands installs bridge.wait-for and bridge.describe.

func RegisterMountComponent

func RegisterMountComponent(parseName string, parseFactory MountComponentFactory)

RegisterMountComponent registers a component factory for bridge.mount.

func RegisterReadCommands

func RegisterReadCommands()

RegisterReadCommands installs the bridge.snapshot and bridge.query command handlers into the process-wide bridge command registry via RegisterAgentCommand. It should be called once, typically from application bootstrap or from an init() guarded by an appropriate build tag. Calling it multiple times is safe (last registration wins per RegisterAgentCommand semantics).

Both commands are read-only and work regardless of whether agent mode is active (reads are always allowed in agent builds).

func RegisterRenderCommands

func RegisterRenderCommands()

RegisterRenderCommands installs bridge.render-tree, which interprets a JSON element tree into a live DOM subtree using the runtime element builders and mounts it into a selector. This is structure-only: it injects arbitrary markup (tags, attributes, text, nesting) at runtime without compiling any Go code. It carries no behavior — event handlers and hooks are Go closures that cannot be shipped over the wire, so on* attributes are dropped.

func RegisterReplayCommands

func RegisterReplayCommands()

RegisterReplayCommands installs bridge.replay, the agent-facing wrapper over the runtime's deterministic update recorder. An agent records a window of scheduling updates, then replays them to reproduce a bug deterministically — the local stand-in for the prod→plan loop.

func RegisterSafetyCommands

func RegisterSafetyCommands()

RegisterSafetyCommands installs bridge.audit and bridge.undo. bridge.audit is read-only; bridge.undo reverses the most recent reversible mutation and is itself gated by agent mode.

func RegisterWriteCommands

func RegisterWriteCommands()

RegisterWriteCommands installs the four input-level mutation commands on the agent bridge:

  • bridge.set-atom — set one atom by id from a JSON value
  • bridge.emit — invoke a fiber's event handler by node ref + event name
  • bridge.publish — publish to the in-app events bus
  • bridge.navigate — navigate the router to a new path

Every handler first checks IsAgentModeActive() and refuses with ErrorCodeForbidden when the app has not been launched in agent mode.

RegisterWriteCommands must be called once at bridge install time (typically inside EnableAgentBridge), before any commands are dispatched.

func SetAgentModeActive

func SetAgentModeActive(parseActive bool)

SetAgentModeActive records whether agent mode is enabled. The bridge client sets it after its gating checks pass; mutating command handlers refuse with the forbidden wire code while it is false.

Types

type AgentAuditEntry

type AgentAuditEntry struct {
	Seq        uint64 `json:"seq"`
	Command    string `json:"command"`
	Summary    string `json:"summary"`
	Reversible bool   `json:"reversible"`
}

AgentAuditEntry is one recorded write the bridge applied, for the bridge.audit trail an agent (or a human reviewing an agent) reads to see what changed.

func AgentAuditEntries

func AgentAuditEntries(parseMax int) []AgentAuditEntry

AgentAuditEntries returns up to parseMax most-recent audit entries, oldest first. A non-positive parseMax returns the whole retained ring.

type AgentCommandHandler

type AgentCommandHandler func(parsePayload json.RawMessage) (json.RawMessage, *EnvelopeError)

AgentCommandHandler executes one bridge command. It returns the ack payload on success, or a structured error (with a stable wire code) on failure. Handlers own their scheduler-locking discipline: anything touching the fiber tree or hook state must take the runtime's scheduler lock the same way Inspect does.

type AgentSocket

type AgentSocket interface {
	// ReadFrame blocks until a text frame arrives or the connection closes.
	// It returns ("", io.EOF) or any non-nil error when the socket is done.
	ReadFrame() (string, error)
	// WriteFrame sends one text frame synchronously. An error means the
	// connection is broken; the client will stop the loop.
	WriteFrame(parseFrame string) error
	// Close terminates the connection. Safe to call more than once.
	Close() error
}

AgentSocket abstracts one bidirectional frame-oriented transport. The bridge client calls ReadFrame and WriteFrame from a single goroutine; Close may be called from any goroutine and must be idempotent.

type BridgeClient

type BridgeClient struct {
	// contains filtered or unexported fields
}

BridgeClient is the transport-agnostic client loop. Create one with NewBridgeClient; the zero value is not valid.

func NewBridgeClient

func NewBridgeClient(parseAppID string, parseBuildID string) *BridgeClient

NewBridgeClient constructs a BridgeClient for the given app and build identifiers. Neither string is validated; empty strings are legal for tests.

func (*BridgeClient) RunLoop

func (parseC *BridgeClient) RunLoop(parseSock AgentSocket)

RunLoop runs the client loop on the given socket. It sends the hello frame, then reads, parses, and dispatches frames until the socket closes or WriteFrame returns an error. RunLoop blocks; run it in a goroutine. The socket is closed before RunLoop returns.

func (*BridgeClient) SendEvent

func (parseC *BridgeClient) SendEvent(parseSession string, parseName string, parsePayload json.RawMessage) error

SendEvent pushes an unsolicited event frame on the current socket, if one is active. It returns an error if no socket is connected or the write fails. SendEvent is safe to call from any goroutine.

type Envelope

type Envelope struct {
	// Protocol is the wire-protocol identifier (always ProtocolName) — receivers reject mismatches.
	Protocol string `json:"protocol"`
	// Version is the protocol version (always ProtocolVersion) for compatibility negotiation.
	Version int `json:"version"`
	// Kind is the frame type: KindHello, KindCommand, KindAck, or KindEvent.
	Kind string `json:"kind"`
	// Seq is this sender's monotonic frame number, starting at 1.
	Seq uint64 `json:"seq"`
	// Session names the app session a frame belongs to. It is empty on the
	// app's hello (the hub assigns the session id in its reply) and may be
	// empty on app<->hub frames where the connection implies the session.
	Session string `json:"session,omitempty"`
	// Name is the command name for KindCommand and the topic for KindEvent.
	Name string `json:"name,omitempty"`
	// Payload is the kind-specific body, left raw so the protocol layer stays
	// decoupled from individual command schemas.
	Payload json.RawMessage `json:"payload,omitempty"`
	// AckSeq is the Seq of the command a KindAck frame answers.
	AckSeq uint64 `json:"ackSeq,omitempty"`
	// OK reports command success on a KindAck frame. It is a pointer so acks
	// are explicit on the wire and other kinds omit it entirely.
	OK *bool `json:"ok,omitempty"`
	// Error carries the structured failure when OK is false.
	Error *EnvelopeError `json:"error,omitempty"`
	// StateVersion is the app's post-commit state version on a successful
	// ack, giving callers read-your-writes ordering without polling.
	StateVersion uint64 `json:"stateVersion,omitempty"`
}

Envelope is one frame of the gwc.agentbridge protocol. Each side numbers its own outbound frames with a monotonic Seq starting at 1; an ack points back at the command it answers via AckSeq.

func BuildAckEnvelope deprecated

func BuildAckEnvelope(parseSeq uint64, parseSession string, parseAckSeq uint64, parseStateVersion uint64, parsePayload json.RawMessage) Envelope

BuildAckEnvelope builds a successful reply to the command numbered parseAckSeq.

Deprecated: use NewAckEnvelope.

func BuildCommandEnvelope deprecated

func BuildCommandEnvelope(parseSeq uint64, parseSession string, parseName string, parsePayload json.RawMessage) Envelope

BuildCommandEnvelope builds a hub-to-app command frame.

Deprecated: use NewCommandEnvelope.

func BuildErrorAckEnvelope deprecated

func BuildErrorAckEnvelope(parseSeq uint64, parseSession string, parseAckSeq uint64, parseCode string, parseMessage string) Envelope

BuildErrorAckEnvelope builds a failed reply to the command numbered parseAckSeq.

Deprecated: use NewErrorAckEnvelope.

func BuildEventEnvelope deprecated

func BuildEventEnvelope(parseSeq uint64, parseSession string, parseName string, parsePayload json.RawMessage) Envelope

BuildEventEnvelope builds an unsolicited app-to-hub push frame.

Deprecated: use NewEventEnvelope.

func BuildHelloEnvelope deprecated

func BuildHelloEnvelope(parseSeq uint64, parsePayload json.RawMessage) Envelope

BuildHelloEnvelope builds the first frame an app sends after connecting.

Deprecated: use NewHelloEnvelope (New* matches the module-wide constructor convention).

func NewAckEnvelope

func NewAckEnvelope(parseSeq uint64, parseSession string, parseAckSeq uint64, parseStateVersion uint64, parsePayload json.RawMessage) Envelope

NewAckEnvelope builds a successful reply to the command numbered parseAckSeq.

func NewCommandEnvelope

func NewCommandEnvelope(parseSeq uint64, parseSession string, parseName string, parsePayload json.RawMessage) Envelope

NewCommandEnvelope builds a hub-to-app command frame.

func NewErrorAckEnvelope

func NewErrorAckEnvelope(parseSeq uint64, parseSession string, parseAckSeq uint64, parseCode string, parseMessage string) Envelope

NewErrorAckEnvelope builds a failed reply to the command numbered parseAckSeq, carrying a stable error code the agent can branch on.

func NewEventEnvelope

func NewEventEnvelope(parseSeq uint64, parseSession string, parseName string, parsePayload json.RawMessage) Envelope

NewEventEnvelope builds an unsolicited app-to-hub push frame.

func NewHelloEnvelope

func NewHelloEnvelope(parseSeq uint64, parsePayload json.RawMessage) Envelope

NewHelloEnvelope builds the first frame an app sends after connecting.

func ParseEnvelope

func ParseEnvelope(parsePayload string) (Envelope, error)

ParseEnvelope decodes one wire frame and rejects anything this build does not speak: wrong protocol, future version, unknown kind, or a frame whose kind-specific required fields are missing.

type EnvelopeError

type EnvelopeError struct {
	Code    string `json:"code"`
	Message string `json:"message,omitempty"`
}

EnvelopeError is the structured failure attached to a non-ok ack.

func ExecuteAgentCommand

func ExecuteAgentCommand(parseName string, parsePayload json.RawMessage) (parseResult json.RawMessage, parseErr *EnvelopeError)

ExecuteAgentCommand runs a registered command by name. Unknown names fail with the stable unknown-command wire code so agents can branch on it. A handler that panics is contained into a bad-payload error so a single bad command can never kill the client's dispatch goroutine (which would strand the app in agent mode with no listener).

type MountComponentFactory

type MountComponentFactory func(map[string]any) *runtime.Element

MountComponentFactory renders a bridge-mountable component from JSON-decoded props. Apps opt components into bridge.mount by registering a factory.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL