Core Concepts
Spectator models stories as structured data — not just text. Every story is composed of typed objects that you define and the engine generates.
World
A World defines the setting, genre, tone, and rules of the story universe.
import { World } from '@spectator-ai/core'
const world = World.create({
genre: 'cyberpunk',
setting: 'Neo-Tokyo, 2087. Megacorporations rule from gleaming towers.',
tone: 'gritty, neon-drenched, melancholic',
rules: [
'Cybernetic augmentation is common but addictive',
'AI is outlawed after the Collapse of 2071',
],
constraints: ['Keep scenes under 500 words'],
})All fields are optional. If you omit World entirely, the engine will generate without world context.
Worlds are immutable — extend() returns a new instance:
const darkWorld = world.extend({ tone: 'bleak and hopeless' })Character
Characters are defined with a name (required) and optional traits, backstory, goals, personality, and relationships.
import { Character } from '@spectator-ai/core'
const zero = Character.create({
name: 'Zero',
traits: ['resourceful', 'paranoid', 'loyal'],
backstory: 'A data runner who lost their memory after a botched Mesh dive',
goals: ['Recover lost memories', 'Expose the truth about the Collapse'],
personality: 'Guarded but fiercely loyal to the few they trust',
})Builder Methods
Characters are immutable. Builder methods return new instances:
// Add a relationship
const withRelation = zero.withRelationship({
target: 'Glass',
type: 'handler',
description: 'Provides jobs in exchange for loyalty',
})
// Add traits
const withMoreTraits = zero.withTraits('determined', 'resourceful')
// Override any fields
const extended = zero.extend({ backstory: 'A reformed hacker...' })Relationships
Relationships link characters together:
const glass = Character.create({
name: 'Glass',
traits: ['calculating', 'connected'],
}).withRelationship({
target: 'Zero',
type: 'handler',
description: 'Provides jobs and protection in exchange for loyalty',
})The type field is freeform — use whatever describes the relationship: 'nemesis', 'mentor', 'ally', 'handler', 'rival', etc.
Plot
A Plot defines the narrative structure through a sequence of beats — named story checkpoints that guide generation.
import { Plot } from '@spectator-ai/core'
const plot = Plot.create({
name: 'The Memory Job',
beats: [
{ name: 'The Job', type: 'inciting-incident', description: 'An offer that might unlock the past' },
{ name: 'Into the Mesh', type: 'rising-action' },
{ name: 'The Truth', type: 'climax' },
{ name: 'The Choice', type: 'resolution' },
],
})Each beat generates one scene. The type tells the engine what emotional role this beat plays in the narrative arc.
Beat Types
| Type | Role |
|---|---|
setup | Establish characters and world |
inciting-incident | The event that disrupts the status quo |
rising-action | Escalating conflict and complications |
midpoint | A major turning point |
crisis | The darkest moment |
climax | The final confrontation |
falling-action | The aftermath |
resolution | The new normal |
Plot Templates
Use Plot.template() to load a registered template (requires @spectator-ai/presets or custom registration):
import '@spectator-ai/presets'
const plot = Plot.template('hero-journey')Register your own templates:
Plot.registerTemplate('my-template', {
beats: [
{ name: 'Opening', type: 'setup' },
{ name: 'Conflict', type: 'climax' },
{ name: 'Ending', type: 'resolution' },
],
})Builder Methods
// Add a beat at the end
const withBeat = plot.withBeat({ name: 'Epilogue', type: 'resolution' })
// Insert a beat at a specific position
const withInserted = plot.withBeat({ name: 'Flashback', type: 'setup' }, 2)
// Remove a beat by index
const withoutBeat = plot.withoutBeat(0)Scene
A Scene is an individual narrative unit generated by the engine. Each beat in the plot produces one scene.
scene.id // Unique identifier
scene.text // The narrative text
scene.beat // The plot beat that generated this scene
scene.location // Where the scene takes place
scene.participants // Character names involved
scene.summary // Auto-generated summary
scene.characterStates // Character emotional/goal states after the sceneScenes are typically not created manually — they are returned by the engine.
Story
A Story is the complete output: a collection of scenes with metadata.
story.scenes // Scene[] — all scenes in order
story.text // All scene text joined with separators
story.title // Optional title
story.wordCount // Total word count
story.sceneCount // Number of scenes
story.characterStates // Character states from the final scene
story.toMarkdown() // Formatted markdown output
story.toJSON() // Serializable JSONStories can be serialized and restored:
const json = story.toJSON()
const restored = Story.fromJSON(json)Architecture
Spectator uses a multi-agent pipeline:
| Component | Role |
|---|---|
| Engine | Manages the generation pipeline, prompting, and scene analysis |
| Director | Maps world events to the emotional trajectory (roadmap) |
| Canvas | Visualizes timelines and threads (roadmap) |
Decoupled World vs. Camera (Fabula & Syuzhet)
Spectator separates the raw chronological events (the World State) from the narrative delivery (the Camera). This means you can generate a world history once, then render it through different lenses — flashbacks, interleaving timelines, or different character POVs.
Emotional Trajectory Mapping
Each beat type carries emotional weight. The engine uses beat types to shape pacing — setup is calm, crisis is tense, resolution brings closure.
Multi-Stream Timelines
World events can exist as independent concurrent streams that diverge (parallel character arcs) and converge (shared event nodes).
Explicit vs. Implicit Threading
Tag events as Explicit (what the viewer sees) or Implicit (what happens in the shadows). The engine maintains state consistency across both threads.