@lovelace_lol/polymer
v0.0.7
Published
Pure ClojureScript character agencies for Character Loom.
Readme
Polymer
Clean ClojureScript character agency package for Character Loom.
Production releases publish to npm as @lovelace_lol/polymer on every push to main. Hosts should depend on the npm package (latest or a semver range). PR staging may still use git SHA pins or LoomLarge Depends-on links.
Polymer is intentionally separate from Latticework and Polyester. It is built as a Society of Mind agency system: small CLJS agencies collaborate through streams, plan locally, schedule their own work, and keep side effects inside the agencies responsible for performing them.
See docs/agency-architecture.md for the agency collaboration, scheduler, side-effect, stream, and runtime-target rules.
Agency API
The package exposes an agency system:
const agencies = createCharacterAgencies();
agencies.dispatch({ agency: "blink", command: { type: "enable" } });
agencies.events.subscribe((message) => {});
agencies.snapshot();Agencies communicate through incoming and outgoing streams of plain data. Messages may be facts, goals, requests, constraints, priorities, status, or diagnostics. Requests that imply side effects are handled by the receiving agency's local planner, scheduler, and effector path.
Host applications may dispatch external commands, observe outgoing streams, and read snapshots for diagnostics or configuration. Host applications should not serve as the normal message bus between Polymer agencies.
The character profile may provide an agencySociety document that controls
which existing agencies and message routes are enabled. A host-side graph editor
can apply route admission changes without recreating agency-local state:
const nextSociety = agencies.updateSocietyRouting(profile.agencySociety);The update is deliberately limited to routing. Agency-local configure and
priors values are applied when createCharacterAgencies creates the agencies;
the graph is not a central planner and cannot bypass their planners or
schedulers.
Current Agencies
Polymer currently includes early Blink, Animation, Gaze, Eye/Head Tracking, TTS, LipSync, Prosodic Expression, and Gesture agencies, plus first slices for Conversation, Camera Context, and Transcription. These are implementation milestones, not the limits of the architecture. Each agency should follow the same local GOAP/planner, scheduler, stream, transform, and effector pattern as the package grows.
The text-only LipSync pronunciation fallback and its accuracy limits are
documented in docs/lipsync-pronunciation-fallback.md.
Runtime-specific work belongs at the edge. The current animation path can target the existing web runtime, but Polymer core should remain able to support other runtime targets such as Babylon, Unity, Godot, robotics, or future animation libraries through separate runtime-specific boundaries.
Hair appearance and physics are model/runtime capabilities rather than character-behavior agencies. Polymer therefore does not expose a Hair agency, Hair planner, or Hair scheduler. It continues to re-export Embody's hair color, material-normalization, and Wasm physics APIs so hosts can configure the character runtime without importing Embody directly.
Gesture Agency
Gesture is the arm/hand gesticulation agency. It accepts LoomLarge-authored gesture snapshots as plain data and turns them into typed bone-channel animation snippets. It does not call scene bones, host UI state, storage, or the animation runtime. Gesture snapshots are compatible with the Gestures tab profile shape: id/name, optional description or text representation, emoji trigger, left/right/both/custom scope, captured source metadata, duration, priority, affected bones, static bone targets, and optional time-based keyframes.
Gesture commands can still name an exact gesture or emoji, but the planner also accepts gesture goals. A goal asks Gesture to choose from the authored library by intent, tags, scope, required or avoided effectors, cooldown, active conflicts, and capacity before it emits Animation requests.
Gesture GOAP Planning
Gesture GOAP plans for one local world-state transition: a suitable authored arm/hand gesture is selected and safely scheduled through Animation, or the agency records why that goal cannot be satisfied. It is not a second public playback API.
The planner uses the incoming command, the gesture library, active snippets, and Gesture config to decide:
- which gesture best satisfies an explicit gesture id, emoji mapping, intent, tags, text hints, scope, and bone constraints
- whether the selected gesture is valid motion and is out of cooldown
- whether active snippets conflict on affected bones
- whether capacity is available under
maxActive - whether to ignore, remove only conflicting snippets, remove the oldest snippets needed for capacity, build a snippet, schedule Animation, and record the result
Host integrations should prefer the character agency network so Gesture's animation requests are routed to Animation inside Polymer:
const agencies = createCharacterAgencies({
animation: { engine },
gesture: {
gestures: profile.characterGestures,
emojiMappings: profile.gestureEmojiMappings,
},
});
agencies.dispatch({ agency: "gesture", command: { type: "playEmoji", emoji: "👋" } });
agencies.dispatch({
agency: "gesture",
command: {
type: "gesture.goal",
goal: {
intent: "greeting",
tags: ["wave"],
scope: "right",
avoidBones: ["HAND_L"],
},
},
});The host supplies the gesture library and may observe streams for diagnostics, but Gesture's own scheduler is responsible for replacement, cancellation, and completion cleanup before Animation owns the runtime side effect.
Character host (Embody scene + model loading)
Polymer re-exports Embody's character host so LoomLarge and other hosts can set up a character without depending on Embody directly.
Default path — pass a container element and a character URL:
import {
createCharacterHost,
createCharacterAgencies,
} from '@lovelace_lol/polymer';
const host = await createCharacterHost({
container: document.getElementById('viewport'),
character: {
modelUrl: '/characters/jonathan.glb',
presetType: 'cc4',
},
});
const agencies = createCharacterAgencies({
animation: { engine: host.engine },
});Named scene types (studio, showcase, inspection, void) bundle
background, lighting preset, and shadow-plane defaults; enumerate them via
CHARACTER_SCENE_TYPES / CHARACTER_SCENE_TYPE_IDS:
const host = await createCharacterHost({
container,
character: { modelUrl: '/characters/jonathan.glb' },
scene: { type: 'showcase' },
});Advanced path — inject an existing Three.js scene:
const host = await createCharacterHost({
container: mountEl,
character: { modelUrl: '/characters/jonathan.glb', presetType: 'cc4' },
external: { scene, renderer, camera },
});Polymer stays agency-oriented: it does not own the render loop. Use
createCharacterHost / loadCharacterModel for scene+model setup, then inject
host.engine into createCharacterAgencies.
For an already-managed Three.js scene, Polymer also exposes a CLJS-owned host
that talks directly to Embody's Rust/Wasm RuntimeCore:
import {
createRustEmbodyHostUnbound,
initEmbodyCore,
supportsRustEmbodyPreset,
} from '@lovelace_lol/polymer';
await initEmbodyCore();
if (supportsRustEmbodyPreset('cc4')) {
const engine = createRustEmbodyHostUnbound({
profileMode: 'preset',
presetId: 'cc4',
profile: characterProfileOverrides,
});
engine.onReady({ model, meshes });
engine.start();
}The CLJS host owns the JavaScript API and render-loop bridge. Rust owns preset lookup, profile merge and intake, compiled bindings, transitions, and packed frame evaluation. Unsupported presets must stay on the compatibility host until they are explicitly embedded; they are never treated as CC4 aliases.
Git Install Contract
Polymer Git-SHA consumers should import checked-in JavaScript artifacts from
dist. They should not need Java, Shadow CLJS, or a local Polymer build during
application install.
The package intentionally has no prepare, install, or postinstall build
lifecycle. Polymer CI runs the CLJS build, verifies that dist exposes the
browser ESM API that Git-SHA consumers import, and prepublishOnly repeats the
build/test/API check before an npm publish.
Worker Protocol Artifact
The package also ships @lovelace_lol/polymer/worker/agency-worker.js as an
opt-in transport around the complete character agency API. It accepts
createCharacterAgencies, dispatch, snapshot, and dispose messages and
returns character events, snapshots, and lifecycle messages as plain data. The
Worker does not add another planner or route between agencies; Polymer's
character network keeps that responsibility.
CI executes the compiled artifact in a real Node worker thread. LoomLarge does not use this artifact yet, and this PR does not decide which agencies should be placed in workers. Selective placement, profiling, transferables, and worker pool decisions remain tracked in issue #62.
