Help us shape the future of Go! Take the 2026 Go Developer Survey

worker

package
v0.19.0 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: MIT Imports: 24 Imported by: 0

README

Orb on Durable Objects and Celld

The Worker host runs the full Orb runtime inside a Cloudflare Durable Object. The same bundle and the same wrangler.jsonc also run on a self-hosted Celld cell. It is a runtime, not a thin client: the agent loop, tools, session journal and provider calls all run in the object.

What runs where

client ──HTTPS/WSS──▶ Worker (dist/worker.mjs default export)
                        │  checks the bearer token, then routes /agents/<name>/…
                        ▼  env.ORB_AGENT.idFromName(<name>)
                      Durable Object OrbAgent (one per agent name)
                        │  first event: open its Orb in the isolate's runtime
                        ▼  (orb.wasm, cmd/orb-worker, instantiated once per isolate)
                      Go runtime: one platforms/worker.Instance per object
                        host.Host{FS, Store, Env}  ◀──▶  ctx.storage (SQLite-backed)
                        agent session + agent/rpc  ──▶  provider APIs via fetch
  • Bundle. make -f platforms/worker/worker.mk worker-build writes .tools/worker/dist/worker.mjs and .tools/worker/dist/orb.wasm. worker.mjs is Go's wasm_exec.js followed by deploy/worker.mjs, so the bundle is one JavaScript module plus a Wasm module. no_bundle: true means neither Wrangler nor Celld runs a bundler, and Celld does not need esbuild.
  • Objects. Each agent name gets its own Durable Object, with its own storage, session and Go instance. The name must match [A-Za-z0-9._~-]{1,128}. Use one name per conversation or per long-lived agent instance. idFromName is deterministic, so the same name always reaches the same object. On Celld, the id also depends on the Worker name: renaming the Worker there moves every name to a new, empty object.
  • Lifecycle. An object opens its Orb on its first event after a start, eviction or hibernation. Opening restores the files and documents from storage and resumes the current session. There is no in-memory state that storage does not also hold. The objects of an isolate share one Go runtime: only the first pays for instantiating it and its one-time setup, and each object adds a few megabytes rather than a runtime's worth. An object disposes of its Orb once it is collected, and a runtime that stops, as a panic does, stops its objects until their next event.

Capability profile

Port Worker host
FS A platforms/memory tree rooted at /, with the workspace at /workspace and the agent directory at /agent. Every mutation is written through to Durable Object storage before it returns (keys fs/…, 4 KiB chunks). A restart restores the tree, including modification times. The default budget is 32 MiB, held in memory.
Store settings.json, models.json and auth.json live in storage under doc/…, outside the tools' reach. ORB_SETTINGS and ORB_MODELS supply the defaults until an object writes its own.
Sessions JSONL journals live under /agent/sessions. The object resumes its current session. new_session and switch_session work; fork and clone return an error.
Env Worker secrets and vars, looked up by their standard names (ANTHROPIC_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, …). There are no credential files and no interactive OAuth logins.
Exec None. bash is omitted, and so are process MCP servers and JS extensions. grep needs ripgrep through Exec, so it fails if a setting enables it. read, write and edit are active; ls and find run over the FS port.
Net Outbound HTTP uses the global fetch through a transport of the Worker's own (fetch_js.go with the shim's fetch helpers), and SSE streams arrive through ReadableStream. Every wait on JavaScript, fetches and storage calls alike, settles through one callback per runtime. The object does not listen on any port; the Worker routes requests to it. nodejs_compat is not needed.

The object does not load context files, skills or prompt templates. The resource loader reads the process file system, and that is not yet served through the FS port.

Bridge peer

Each object is a full Bridge peer (platforms/worker/peer). Its identity (an Ed25519 key), grants, pairings and operation receipts live in Store documents under /bridge/, so they survive restarts. The object's own Orb is enrolled once, under an alias derived from the object name, and is attached as its orb.instance/1 service whenever the peer is open.

  • Inbound streams. /agents/<name>/bridge accepts a WebSocket that carries Bridge's pinned TLS 1.3 stream, as orb bridge run --web-listen does. The route needs no ORB_TOKEN: a remote Orb never has it, and Bridge authenticates the peer inside the stream. The socket is accepted, not hibernatable. The TLS session lives in the Go runtime, which hibernation would discard while the socket stayed open, and the open socket keeps the object resident. An object without a Bridge identity answers 404 without booting. A browser Origin must be listed in ORB_BRIDGE_ORIGINS.
  • Outbound calls. The object cannot dial (tailcat is native-only, and a Worker has no listener of its own). It reaches another peer over a channel that peer opened: a laptop Orb that joined keeps its channel, and the object's agent calls it with bridge_call. That tool is active only when plugins.bridge-agent-calls is on in the object's settings. Each call also needs the object's own instance grant for the destination, and a grant on the destination for the object's instance subject.
  • Memory (experimental). plugins.memtree in the object's settings turns on memtree (docs/plugins.md): every prompt starts from a fixed-size view of the whole conversation, and its summaries live in the session journal in object storage.
  • Admin. POST /agents/<name>/bridge/admin with {"method", "params"}, behind ORB_TOKEN, runs the owner methods of orb bridge: status, instances, peers, grants, invite, approve, block, grant, revoke, scope, group, assign, takeover, trust (a full controller grant) and remote. self returns the peer ID, instance ID and stream URL. invite advertises wss://<host>/agents/<name>/bridge, or the same path on ORB_PUBLIC_ORIGIN when a proxy fronts the Worker. join, publish and the service lifecycle are refused, because the object cannot dial.

Pairing a laptop Orb with an object:

curl -s -X POST https://<host>/agents/demo/bridge/admin -H "Authorization: Bearer $ORB_TOKEN" \
  -d '{"method":"invite"}' > invitation.json
orb bridge pair join < invitation.json          # prints the claim, with the laptop's peer ID
curl -s -X POST https://<host>/agents/demo/bridge/admin -H "Authorization: Bearer $ORB_TOKEN" \
  -d '{"method":"approve","params":{"invitation_id":"<id>","claimant":"<laptop peer ID>"}}'
orb bridge trust <object peer ID>               # optional: the object may list and call this laptop
orb bridge remote <object peer ID> instances.list < /dev/null

Deploy

Cloudflare, with Wrangler credentials from npx wrangler login or CLOUDFLARE_API_TOKEN:

make -f platforms/worker/worker.mk worker-deploy        # builds .tools/worker, then wrangler deploy

Celld, with a fleet bucket:

make -f platforms/worker/worker.mk worker-build && .tools/bin/celld deploy .tools/worker --bucket s3://my-cells-bucket

The installed cf CLI (0.4) delegates cf dev and cf deploy to Wrangler only through the experimental cloudflare.config.ts, which Celld cannot read. The targets therefore call the Wrangler version pinned in deploy/package.json, which shares wrangler.jsonc with Celld.

Local servers read .tools/worker/.dev.vars (dotenv format, never committed):

make -f platforms/worker/worker.mk worker-dev           # workerd via wrangler dev, http://127.0.0.1:8787
make -f platforms/worker/worker.mk worker-celld-dev     # Celld, http://127.0.0.1:9876 (installs celld into .tools)

Secrets and configuration

Name Kind Purpose
ORB_TOKEN secret, required Bearer token for every /agents/… route, at least 16 characters. Without it the Worker answers 503.
provider keys secret ANTHROPIC_API_KEY, OPENAI_API_KEY and the other standard names.
ORB_SETTINGS var Default settings.json in kernel format. The template ships with {"defaultProvider":"anthropic","defaultModel":"claude-sonnet-5"}.
ORB_MODELS var Default models.json, for custom providers.

On Cloudflare, set secrets with cd .tools/worker && npx wrangler secret put ORB_TOKEN and do the same for each provider key. Celld has no secret store: celld dev reads .dev.vars, and a fleet takes string vars from the deployed config. Keep that config out of git. A client can change the model of a session with the RPC set_model command.

Endpoint contract

Every /agents/<name>/… route except the Bridge stream requires Authorization: Bearer <ORB_TOKEN>. A missing or wrong token gets 401.

Route Behavior
GET /health 200 ok, with no authentication.
GET /agents/<name>/rpc + Upgrade: websocket A hibernatable WebSocket. Each client message carries one or more pi RPC command frames, separated by LF. Each server message is one output frame: response, an agent event or extension_ui_request. The object sends its frames to every socket it has open.
POST /agents/<name>/rpc The body is NDJSON command frames. The reply is 200 application/x-ndjson and streams the object's frames while the request is open. It ends once every command has its response and any run a command started has sent agent_settled.
GET /agents/<name>/stats JSON: bootId, bootMs, busy and pending for the object, and wasmMemoryBytes and Go memory statistics for the isolate's runtime.
GET /agents/<name>/bridge + Upgrade: websocket A Bridge stream (binary messages, pinned TLS inside). No bearer token: Bridge authenticates the peer. 404 until the object has a Bridge identity.
POST /agents/<name>/bridge/admin {"method": "…", "params": {…}}: an owner method of orb bridge (see Bridge peer). Returns the method's JSON result, or 400 with {"error"}.

Frames are the kernel RPC frames of orb --mode rpc (agent/rpc), byte for byte. A transport failure, such as a runtime that failed to boot, arrives as {"type":"worker_error","error":"…"}. That frame is specific to this host and is not part of the kernel.

curl -N https://orb-agent.<account>.workers.dev/agents/demo/rpc \
  -H "Authorization: Bearer $ORB_TOKEN" --data-binary @- <<'EOF'
{"id":"1","type":"prompt","message":"Create notes/todo.md with three items"}
EOF

Limits

  • Size. With the Bridge peer, orb.wasm is 27.9 MB raw and 7.0 MB gzip, about 30% under the 10 MB compressed limit of the paid plan. The free plan allows 3 MB, so it cannot host Orb.
  • Memory. Cloudflare gives each isolate 128 MB. The isolate's objects share one runtime, whose Wasm linear memory is about 21 MB with one object after a tool-using turn and grows by about 1.7 MB per further object, mostly Go heap (89 MB for 40 objects under Node). Linear memory only grows, so this figure is also the peak.
  • Time. In local workerd a boot takes 0.3–1.7 s, and the first frame after a restart arrives in 0.25–0.75 s. Celld's first activation of a cell also compiles the module, which takes about 2.8 s in its isolate log. The Durable Object CPU limit is 30 s per event by default.
  • Timers. After each provider request, Go's js/wasm scheduler leaves a JavaScript timer pending for up to httpIdleTimeoutMs (5 minutes by default). A pending timer keeps the object out of hibernation until it fires.
  • Runs. One run per object at a time. A prompt sent while a run streams needs streamingBehavior, as in RPC mode.
  • Celld. performance.now() stays fixed while JavaScript runs, so bootMs reads 0. Celld's process global is partial, and the shim fills in the members Go needs.

Delete

  • Cloudflare. cd .tools/worker && npx wrangler delete --name orb-agent removes the Worker. To erase stored objects first, deploy a migration with "deleted_classes": ["OrbAgent"].
  • Celld. Locally, celld dev --clean discards .tools/worker/.celld/dev. For a fleet, remove the deployment's prefix from the bucket.

Tests and gates

Command Covers
go test ./platforms/worker/... ./cmd/orb-worker/... FS conformance (envtest), restart persistence and key hygiene, documents, session resume, new_session and switch_session. It also runs the real bundle in Node against a Map-backed Durable Object storage, restarts the object and asserts the frames.
GOOS=js GOARCH=wasm go test -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" ./platforms/worker/... The same suites through the syscall/js storage bridge, against an asynchronous fake ctx.storage.
make -f platforms/worker/worker.mk worker-e2e-workerd workerd (wrangler dev) with a local scripted OpenAI-compatible model: write then read over WebSocket, a dev-server restart, then history and file checks over HTTP NDJSON.
make -f platforms/worker/worker.mk worker-e2e-celld The same scenario under celld dev, including a node restart.
make -f platforms/worker/worker.mk worker-e2e-bridge-workerd (and -celld) Bridge in both directions with this tree's native orb, confined to .tools/worker-bridge-e2e/laptop (its own HOME, ORB_STATE_HOME, ORB_BRIDGE_HOME and PI_CODING_AGENT_DIR, profile e2e). The object invites, the laptop joins over the advertised URL, and the owner approves. The laptop lists, inspects and prompts the object and reads the conversation back with orb bridge remote. The object's agent then calls the laptop's Orb with bridge_call under its own instance grant. After a restart the laptop reconnects to the same identity.
make -f platforms/worker/worker.mk worker-e2e-bridge-deployed ORB_WORKER_URL=… The same against a deployed worker-e2e-build bundle. It redeploys .tools/worker-e2e once to restart the object.
worker-e2e-build, then worker-e2e-deployed A deployed check. worker-e2e-build adds e2e/fake-model.js, so the scripted model runs inside the Worker and no provider key is needed. node platforms/worker/e2e/e2e.mjs --configure-deploy .tools/worker-e2e --name orb-do-e2e writes its wrangler.json. Deploy that directory and set ORB_TOKEN. Then run WORKER_E2E_PHASE=write, redeploy (a new version restarts every object) and run WORKER_E2E_PHASE=verify, with ORB_WORKER_URL and ORB_TOKEN set.

Documentation

Overview

Package worker is the Worker host (DECISIONS.md P2, P10): the ports an Orb runtime needs inside a Cloudflare Durable Object or a self-hosted Celld cell, backed by the object's key-value storage, plus the assembly that resumes one session per object and serves it over kernel RPC frames.

The package is portable; only durable_js.go binds a live Durable Object through syscall/js. Everything else runs, and is tested, on every target.

Index

Constants

View Source
const (
	AgentDir  = "/agent"
	Workspace = "/workspace"
	// DefaultMaxBytes bounds the persisted tree, which the object holds in
	// memory, well inside a 128 MB isolate.
	DefaultMaxBytes = 32 << 20
)

The object's path namespace: tools work in Workspace; settings, model catalogs and session journals live under AgentDir, as ~/.pi/agent does.

Variables

This section is empty.

Functions

func DocumentKey

func DocumentKey(name string) string

DocumentKey is the storage key that holds document name's metadata. Its presence tells the JavaScript shim, without booting the runtime, that the object has written that document.

Types

type Env

type Env func(name string) (string, bool)

Env is the Env port over the Worker's bindings: a lookup of secrets and vars by name. There are no credential files.

func (Env) Env

func (lookup Env) Env(_ context.Context, name string) (string, bool)

func (Env) FileExists

func (Env) FileExists(context.Context, string) bool

type FileSystem

type FileSystem struct {
	*memory.FileSystem
	// contains filtered or unexported fields
}

FileSystem is the persisted FS port: a platforms/memory working copy whose every mutation is written through to KV before it returns, so an evicted or restarted object reopens the same tree. Reads never touch storage.

func OpenFileSystem

func OpenFileSystem(ctx context.Context, kv KV, namespace string, options memory.Options) (*FileSystem, error)

OpenFileSystem restores the tree stored under namespace. options configure the working copy; its clock is kept for new writes.

func (*FileSystem) AppendFile

func (fsys *FileSystem) AppendFile(ctx context.Context, name string, content []byte) error

func (*FileSystem) CreateDir

func (fsys *FileSystem) CreateDir(ctx context.Context, name string, recursive bool) error

func (*FileSystem) CreateTempDir

func (fsys *FileSystem) CreateTempDir(ctx context.Context, prefix string) (string, error)

func (*FileSystem) CreateTempFile

func (fsys *FileSystem) CreateTempFile(ctx context.Context, prefix, suffix string) (string, error)

func (*FileSystem) Remove

func (fsys *FileSystem) Remove(ctx context.Context, name string, recursive, force bool) error

func (*FileSystem) RenameFile

func (fsys *FileSystem) RenameFile(ctx context.Context, from, to string) error

func (*FileSystem) WriteFile

func (fsys *FileSystem) WriteFile(ctx context.Context, name string, content []byte) error

type Instance

type Instance struct {
	Host  *host.Host
	Files *FileSystem
	// contains filtered or unexported fields
}

Instance is one Durable Object's Orb: a Host over the object's storage and the session it resumes. It is the RPC session host: new_session and switch_session replace the session; fork and clone are not offered.

func Open

func Open(ctx context.Context, options Options) (*Instance, error)

Open restores the object's files and documents and resumes its current session, or its newest, or starts one.

func (*Instance) Dispose

func (instance *Instance) Dispose()

func (*Instance) EnableControl

func (instance *Instance) EnableControl() (*agent.SessionControl, error)

EnableControl admits non-owning controllers (a Bridge attachment) to the object's sessions; every replacement is then bracketed as a transition.

func (*Instance) Fork

func (instance *Instance) Fork(string, bool) (string, bool, error)

func (*Instance) NewSession

func (instance *Instance) NewSession(parentSession string) (bool, error)

func (*Instance) NewSessionContext

func (instance *Instance) NewSessionContext(ctx context.Context, parentSession string) (bool, error)

NewSessionContext is NewSession under ctx's control target.

func (*Instance) ObserveSessions

func (instance *Instance) ObserveSessions(observe func(*agent.AgentSession)) func()

ObserveSessions calls observe with the current session and after every replacement, until the returned function is called.

func (*Instance) Serve

func (instance *Instance) Serve(ctx context.Context, input io.Reader, output, diagnostics io.Writer) int

Serve speaks kernel RPC frames (agent/rpc) until input ends or ctx is done, then disposes the session.

func (*Instance) Session

func (instance *Instance) Session() *agent.SessionRuntime

func (*Instance) SwitchSession

func (instance *Instance) SwitchSession(sessionPath string) (bool, error)

func (*Instance) SwitchSessionContext

func (instance *Instance) SwitchSessionContext(ctx context.Context, sessionPath string) (bool, error)

SwitchSessionContext is SwitchSession under ctx's control target.

type KV

type KV interface {
	// Get returns the present keys among keys; absent keys are omitted.
	Get(ctx context.Context, keys []string) (map[string][]byte, error)
	// List returns every entry whose key starts with prefix.
	List(ctx context.Context, prefix string) (map[string][]byte, error)
	// Write stores put and removes del as one atomic write where the backend
	// allows it (a Durable Object coalesces writes issued without an await).
	Write(ctx context.Context, put map[string][]byte, del []string) error
}

KV is the storage port: the subset of the Durable Object key-value API (ctx.storage get/put/delete/list) that the Worker host persists through. Values are opaque bytes; keys are at most 2 KiB.

type Options

type Options struct {
	KV KV
	// Env resolves provider credentials (Worker secrets) and ORB_* vars.
	Env Env
	// Settings and Models serve settings.json and models.json until the
	// object stores its own (Worker vars ORB_SETTINGS and ORB_MODELS).
	Settings, Models []byte
	// MaxBytes and MaxFiles bound the persisted tree; zero MaxBytes is
	// DefaultMaxBytes.
	MaxBytes int64
	MaxFiles int
	// Model pins the model instead of restoring it from the session or
	// settings; StreamFn replaces the default provider dispatcher.
	Model    *ai.Model
	StreamFn engine.StreamFn
	// Tools adds tools to every session the object starts.
	Tools ToolsFunc
	// Extensions picks the extensions every session the object starts loads, by name, from its
	// settings; the assembly supplies them, so the host links no capability.
	Extensions func(*config.SettingsManager) map[string]extensions.Factory
}

type Store

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

Store is the Store port over a persisted tree of its own, out of reach of the file tools, so settings, credentials and model catalogs survive restarts without being readable by the model.

func NewStore

func NewStore(files *FileSystem, defaults map[string][]byte) *Store

func (*Store) Document

func (s *Store) Document(name string) host.Document

type ToolsFunc

ToolsFunc supplies extra tools for each session the object starts, decided from the object's settings (a capability's plugin switch, for example).

Directories

Path Synopsis
Package peer makes a Worker object a Bridge peer (DECISIONS.md P11): a persisted identity in the object's Store, the object's own Orb attached as its orb.instance/1 service, pinned-TLS streams over the object's WebSockets, and the owner's admin surface.
Package peer makes a Worker object a Bridge peer (DECISIONS.md P11): a persisted identity in the object's Store, the object's own Orb attached as its orb.instance/1 service, pinned-TLS streams over the object's WebSockets, and the owner's admin surface.

Jump to

Keyboard shortcuts

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