foreign

package
v0.30.1 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

pkg/foreign

pkg/foreign defines the composition seams for foreign-loop backends: a session uses Builder to construct a fresh foreign loop and RestoredBuilder to reconstruct one from journal-recovered state. The concrete codex and claude backends live in the sibling looprig/foreignloops module.

What is foreign?

  • Builder — the composition-root seam a session uses to construct a fresh foreign loop. Returns the loop.Backend and the minted ForeignSID the session records.
  • RestoredBuilder — mirrors Builder but carries a RestoredForeign seed (the recovered foreign session id, the committed turn count, and the committed conversation thread) and returns no sid because the seed already holds it. A restored loop comes up idle, seeded with this state, and resumes (never re-creates) the recorded session on its next turn.
  • RestoredForeign — the journal-recovered seed.
  • EventPublisher — the narrow consumer of the session event fan-in a foreign loop holds. A session satisfies it via PublishEvent / PublishEventChecked.

The seams are deliberately narrow: a foreign loop sees only the loop.Backend contract the session drives, the EventPublisher it publishes through, and the loop.BoundDefinition it runs under. It does not see the native loop's gate/commit/drain internals, which a foreign loop has no analogue for.

How to use

You don't call Builder directly — you register one with the rig, and the session calls it when a loop selects the matching engine:

import codexbackend "github.com/looprig/foreignloops/codex"
import claudbackend "github.com/looprig/foreignloops/claude"

r, err := rig.Define(
    rig.WithLoops(
        // a native loop:
        operator,
        // a codex-backed loop:
        codexLoop,  // loop.Define(... loop.WithEngine(loop.EngineForeignCodex) ...)
        // a claude-backed loop:
        claudeLoop, // loop.Define(... loop.WithEngine(loop.EngineForeignClaude) ...)
    ),
    rig.WithForeignBuilders(
        codexbackend.Builder(codexbackend.Options{ /* ... */ }),
        claudbackend.Builder(claudbackend.Options{ /* ... */ }),
    ),
    /* ... */
)

A session routes a SubmitToLoop to a foreign-backed loop the same way it routes one to a native loop. The foreign backend owns its own subprocess; the harness session owns the journal, the hub, and the restore lifecycle.

Sibling packages

  • pkg/looploop.EngineForeignClaude / EngineForeignCodex select a foreign backend; loop.Backend is the contract a backend satisfies; loop.BoundDefinition is the bound recipe a backend runs under.
  • pkg/eventForeignSessionBound is the durable event that records the late-bound foreign session id; restore recovers it.
  • pkg/rigrig.WithForeignBuilders registers the builders the session calls.
  • github.com/looprig/foreignloops — the codex and claude backends behind these seams.

How it is designed

       Loop definition (Engine = ForeignCodex | ForeignClaude)
                       │
                       │  session constructs the loop
                       ▼
            foreign.Builder / RestoredBuilder
                       │
                       │  returns loop.Backend + ForeignSID (Builder only)
                       ▼
            ┌────────────────────────────┐
            │ Foreign loop backend        │
            │ (looprig/foreignloops)      │
            │  • subprocess (codex exec)   │
            │  • JSONL decode             │
            │  • late-bound ForeignSID     │
            │  • durable resume            │
            └────────────┬───────────────┘
                         │
            publish  ────┴────►  EventPublisher  ──►  pkg/hub
                                                  ──►  pkg/event (ForeignSessionBound)
                         │
                         ▼
                  loop.Backend (the contract the session drives)
Late-bound foreign session id

A foreign loop's session id is learned from the foreign process (codex exec --json emits a thread.started event), not known at construction. The session records the learned id as a durable ForeignSessionBound event. Restore recovers that id and resumes the same foreign session — a failed Codex start that produced an empty sid retries StartNew until a nonempty session id has been bound.

Restore is resume, not re-create

A restored foreign loop comes up idle, seeded with the RestoredForeign thread and turn count, and resumes the recorded foreign session on its next turn. It does not re-create the session: the foreign backend's codex exec resume (or equivalent) is the mechanism, and the harness session journal is what makes the resume durable across harness restarts.

The narrow seam

Builder and RestoredBuilder are deliberately small: the loop context, the session/loop ids, the parent loop.Provenance, the EventPublisher, the loop.BoundDefinition, an id generator, and the event Factory. The seam carries no harness internals and no foreign protocol vocabulary; a new foreign backend (a different CLI, an MCP runtime, …) implements just Builder/RestoredBuilder and a loop.Backend.

Documentation

Overview

Package foreign defines the composition seams for foreign loop backends.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BrokerDescriptor

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

BrokerDescriptor is the opaque, per-loop description of a collaboration broker. Harness intentionally keeps the endpoint and capability together; executable discovery, protocol selection, and transport construction belong to the composition root that consumes this value.

The capability is never returned by reference. BrokerDescriptor has no String method so a secret cannot acquire a public formatting contract.

func NewBrokerDescriptor

func NewBrokerDescriptor(endpoint string, capability []byte) BrokerDescriptor

NewBrokerDescriptor takes an endpoint and capability snapshot. The input capability is copied before the descriptor is returned.

func (BrokerDescriptor) Capability

func (d BrokerDescriptor) Capability() []byte

Capability returns an independent copy of the opaque broker capability.

func (BrokerDescriptor) Endpoint

func (d BrokerDescriptor) Endpoint() string

Endpoint returns the broker endpoint from the immutable descriptor.

func (BrokerDescriptor) Format

func (d BrokerDescriptor) Format(state fmt.State, verb rune)

Format deliberately ignores the requested verb, flags, width, and precision. The descriptor may carry a bearer capability and an endpoint path, so every formatting form receives the same fixed bounded redaction.

type Builder

type Builder func(
	loopCtx context.Context,
	sessionID, loopID uuid.UUID,
	parent loop.Provenance,
	pub EventPublisher,
	cfg loop.BoundDefinition,
	idGen func() (uuid.UUID, error),
	fac *event.Factory,
) (loop.Backend, string, error)

Builder is the composition-root seam a session uses to construct a foreign loop. It returns the Backend and the minted ForeignSID, which the caller records.

type BuilderRegistry

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

BuilderRegistry routes foreign-loop construction by the stable runtime profile key. The zero value is ready for use. Registration is serialized and lookup takes a snapshot of the function pair, so a configured registry can be safely composed and read concurrently.

A BuilderRegistry must not be copied after first use.

func (*BuilderRegistry) Builder

Builder returns legacy-shaped live and restored builders registered for profile. Services registrations are adapted with zero Services so legacy callers remain source-compatible without gaining authority. An unknown profile returns a bounded *UnknownProfileError and no builders.

func (*BuilderRegistry) HasServicesBuilder

func (r *BuilderRegistry) HasServicesBuilder(profile loop.RuntimeProfileName) bool

HasServicesBuilder reports whether profile was registered through the additive services-aware shape. Legacy registrations are intentionally false even though ServicesBuilder can return a compatibility adapter for them; callers that manage capabilities must not mint authority for a legacy builder that cannot receive it.

func (*BuilderRegistry) Register

func (r *BuilderRegistry) Register(profile loop.RuntimeProfileName, builder Builder, restored RestoredBuilder) error

Register binds a live/restored builder pair to profile. Empty profiles and duplicate registrations fail closed; an existing binding is never replaced.

func (*BuilderRegistry) RegisterServices

func (r *BuilderRegistry) RegisterServices(profile loop.RuntimeProfileName, builder ServicesBuilder, restored ServicesRestoredBuilder) error

RegisterServices binds a services-aware live/restored builder pair to a profile. A profile has one registration shape; use Register for legacy builders and RegisterServices for the additive services shape.

func (*BuilderRegistry) ServicesBuilder

ServicesBuilder returns the services-aware live/restored builders for profile. Legacy registrations are adapted with a zero Services value so callers can use one dispatch path without changing legacy behavior.

type DeliveryFallback

type DeliveryFallback = DeliveryIntent

DeliveryFallback identifies the one normal-queue fallback for a request. The hook implementation reuses the command already bound to RequestID and writes its fallback phase before returning; callers never supply a second payload.

type DeliveryHook

type DeliveryHook interface {
	CreateIntent(context.Context, DeliveryIntent) error
	Reserve(context.Context, DeliveryReservation) error
	QueueFallback(context.Context, DeliveryFallback) error
	Resolve(context.Context, DeliveryResolution) error
}

DeliveryHook is the narrow durability capability supplied to one foreign loop actor. Implementations must scope every operation to the loop and request identifiers supplied in its value; they must not expose a Session, controller, journal, command sink, or other cross-loop authority. A successful QueueFallback return means its exact command payload is already durably recorded and may now be admitted through the normal actor path.

type DeliveryIntent

type DeliveryIntent struct {
	LoopID    uuid.UUID
	RequestID uuid.UUID
}

DeliveryIntent identifies one durable delivery request. It deliberately carries only loop/request identity; the session binds the exact command payload privately before actor admission, so session controllers, journals, and message payloads do not cross the foreign-loop boundary.

type DeliveryReservation

type DeliveryReservation = DeliveryIntent

DeliveryReservation identifies one reserved foreign delivery attempt.

type DeliveryResolution

type DeliveryResolution struct {
	LoopID    uuid.UUID
	RequestID uuid.UUID
	TurnID    uuid.UUID
	State     DeliveryResolutionState
}

DeliveryResolution identifies a durable delivery resolution. TurnID is optional for an ambiguous or untrackable attempt and is present when the actor has a host-owned injected fold to correlate.

type DeliveryResolutionState

type DeliveryResolutionState string

DeliveryResolutionState is the provider-neutral terminal classification for one foreign delivery attempt. A successful injected fold carries its turn identity; unknown and untrackable outcomes do not.

const (
	DeliveryResolutionInjected    DeliveryResolutionState = "injected"
	DeliveryResolutionUnknown     DeliveryResolutionState = "unknown"
	DeliveryResolutionUntrackable DeliveryResolutionState = "untrackable"
)

type EventPublisher

type EventPublisher interface {
	PublishEvent(context.Context, event.Event) error
	PublishEventChecked(context.Context, event.Event) error
}

EventPublisher is the foreign loop's narrow consumer of the session event fan-in. A session satisfies it via PublishEvent.

type RestoredBuilder

type RestoredBuilder func(
	loopCtx context.Context,
	sessionID, loopID uuid.UUID,
	parent loop.Provenance,
	pub EventPublisher,
	cfg loop.BoundDefinition,
	idGen func() (uuid.UUID, error),
	fac *event.Factory,
	seed RestoredForeign,
) (loop.Backend, error)

RestoredBuilder is the composition-root seam a session uses to reconstruct a foreign loop from journal-recovered state. It mirrors Builder but carries the RestoredForeign seed and returns no sid because the seed already holds it.

type RestoredForeign

type RestoredForeign struct {
	ForeignSID string
	// AgentSessionID is optional journaled agent state. It is empty for
	// legacy events and for foreign runtimes that do not expose one.
	AgentSessionID string
	TurnIndex      event.TurnIndex
	Msgs           content.AgenticMessages
}

RestoredForeign is the journal-recovered seed for a foreign loop: the recovered foreign session id, the committed turn count, and the committed conversation thread. A restored loop comes up idle, seeded with this state, and resumes (never re-creates) the recorded session on its next turn.

type Services

type Services struct {
	Broker   BrokerDescriptor
	Delivery DeliveryHook
}

Services is the immutable value supplied to a services-aware foreign builder. The zero value is the compatibility snapshot passed to legacy builders and carries no broker or delivery authority.

func NewServices

func NewServices(broker BrokerDescriptor, delivery DeliveryHook) Services

NewServices takes an independent snapshot of broker descriptor bytes while retaining the narrow delivery interface value.

func (Services) Clone

func (s Services) Clone() Services

Clone returns an independent services snapshot. Interface values are copied as values; the hook implementation remains responsible for its own concurrency and loop scoping.

func (Services) Format

func (s Services) Format(state fmt.State, verb rune)

Format keeps the descriptor and delivery authority out of diagnostics even when Services is formatted as a struct with %#v or %+v. Formatting options are intentionally ignored for the same fixed bounded output contract as BrokerDescriptor.Format.

type ServicesBuilder

type ServicesBuilder func(
	loopCtx context.Context,
	sessionID, loopID uuid.UUID,
	parent loop.Provenance,
	pub EventPublisher,
	cfg loop.BoundDefinition,
	idGen func() (uuid.UUID, error),
	fac *event.Factory,
	services Services,
) (loop.Backend, string, error)

ServicesBuilder is the additive foreign-loop construction seam. Services is passed last so existing builder argument order remains source-compatible.

type ServicesRestoredBuilder

type ServicesRestoredBuilder func(
	loopCtx context.Context,
	sessionID, loopID uuid.UUID,
	parent loop.Provenance,
	pub EventPublisher,
	cfg loop.BoundDefinition,
	idGen func() (uuid.UUID, error),
	fac *event.Factory,
	seed RestoredForeign,
	services Services,
) (loop.Backend, error)

ServicesRestoredBuilder is the additive restored-loop construction seam. It mirrors RestoredBuilder and receives the immutable Services snapshot last.

type UnknownProfileError

type UnknownProfileError struct{}

UnknownProfileError reports a profile that is not registered. Its message is intentionally bounded and does not include the requested profile or any construction detail.

func (*UnknownProfileError) Error

func (*UnknownProfileError) Error() string

Jump to

Keyboard shortcuts

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