Skip to content

Generator Runtime Contract V1

Goals

The Generator Runtime Contract defines how generators are embedded and run, ensuring that the same generator can run both as a full page and as an embedded module within a template page.

If you need the practical communication map between generator-workbench and the runtime, including routeMode, runtime events, and reverse commands, also read Workbench Runtime Communication.

It addresses:

  • Supporting both full and embed modes for the same generator.
  • Rendering only the central canvas and partial parameters in template pages.
  • Reading, setting, restoring, and exporting generator state by the host.
  • Tailoring parameter panels via unified schemas and filtering rules.

It does NOT address:

  • Implementation of platform capabilities like login, cloud save, history, credits, billing, and export.
  • CMS configuration, publishing orchestration, and approval flows.
  • Specific business UI forms like top navigation, login modals, and sharing dialogs.

These remain the responsibility of generator-sdk and generator-control-plane.

Relationship with Refactoring Delivery

In the context of refactoring existing generators, it's important to distinguish between these two types:

  • Compatibility Refactoring: Prioritizes non-regression and minimal changes, allowing for partial runtime or adapter implementation.
  • Standardization Refactoring: Ultimately converges the old generator into a standard generator structure.

The Generator Runtime Contract is a necessary condition for Standardization Refactoring, but not a sufficient one.

This means that the following scenarios cannot be equated to "Standardization Refactoring Completion":

  • Only adding window.__GENERATOR_RUNTIME__.
  • Only adding getState() / setState() / patchState().
  • Only integrating the SDK and wrapping it with a lightweight bridge.
  • Only implementing PanelSchema without full / embed support.

To claim "Standardization Refactoring Completion," the following must also be present:

  1. generator-sdk integrated with all required platform capabilities.
  2. Both full and embed entry points are functional.
  3. window.__GENERATOR_RUNTIME__ is exposed.
  4. getPanelSchema() + PanelFilter can be consumed by the host.
  5. Unified template protocol used for template scenarios.
  6. Compatibility, migration, or CMS documentation is complete.

Boundaries

generator-sdk

Responsible for platform capabilities:

  • Login
  • Cloud Save / Restore
  • History
  • Credits
  • Unified Billing
  • Export to Local / Open in Studio

Generator Runtime Contract

Responsible for the runtime protocol between the host and the generator:

  • Canvas mounting
  • Parameter panel mounting
  • State acquisition and setting
  • Parameter schema exposure
  • Parameter filtering
  • Export data provision
  • Runtime event subscription

Core Principles

  1. Single Source of Truth for State The generator must be driven by a serializable state. Both template and full pages use the same business state model.

  2. Decoupling Shell from Core Top navigation, sharing, login, and business overlays belong to the host shell; the canvas, parameters, and export provider belong to the generator runtime.

  3. Parameter Panels Derived from Schema Template pages should not hardcode form logic for a specific generator; they should consume the PanelSchema exposed by the runtime.

  4. Predictable Embedding The core canvas result for the same state must be consistent across full and embed modes.

  5. Incremental Compatibility Old bridges are allowed to map to this protocol via adapters, but new generators must implement this protocol directly.

  6. Completion Claims Restricted by Gates Even if the runtime is partially implemented, it should be described as a phased delivery unless all standardization criteria are met.

Mode Definitions

full

Full editor mode, typically including:

  • Top navigation
  • Left toolbar
  • Central canvas
  • Right-side full parameter panel

embed

Embedded mode, where the host page provides the business shell and the generator exposes only:

  • Canvas
  • Parameter panel or a subset of it
  • State synchronization capabilities
  • Export capabilities

Minimal Interface

V1 requires generators to expose at least the following interface:

ts
type RuntimeMode = 'full' | 'embed'
type MountTarget = 'full' | 'canvas' | 'panel'

interface GeneratorRuntime {
  version: string
  generatorId: string
  capabilities: RuntimeCapabilities

  mount(options: MountOptions): MountedInstance | Promise<MountedInstance>
  getState(): GeneratorState
  setState(nextState: GeneratorState, options?: SetStateOptions): void | Promise<void>
  patchState(patch: StatePatch, options?: SetStateOptions): void | Promise<void>
  getPanelSchema(options?: GetPanelSchemaOptions): PanelSchema
  export(action: ExportAction, options?: ExportOptions): Promise<ExportResult | null>
  subscribe(listener: RuntimeEventListener): () => void
  dispatchWorkbenchCommand?(command: WorkbenchCommand): void | Promise<void>
}

This set of interfaces defines the minimal protocol surface for the runtime; it is not the same as the full delivery threshold. For existing generators, it often corresponds to "Phase 2: Completing Runtime Interfaces," not the end of all refactoring work.

mount()

Responsible for mounting the generator into the container provided by the host.

ts
interface MountOptions {
  mode: RuntimeMode
  routeMode?: RuntimeMode
  target: MountTarget
  container: HTMLElement
  state?: GeneratorState
  panelFilter?: PanelFilter
  readonly?: boolean
  hostContext?: HostContext
}

Requirements:

  • Allow rendering the full editor when mode: 'full'.
  • Support canvas or panel mounting when mode: 'embed'.
  • routeMode reflects the actual route ?mode= so the runtime can distinguish runtime mount mode from page capability mode.
  • In generator-workbench shell mode, the runtime can still receive mode: 'full' together with routeMode: 'embed'.
  • Support repeated mounting and unmounting.
  • Respond to container size changes.

getState() / setState() / patchState()

Responsible for state synchronization between the host and the generator.

  • getState() returns the current full business state.
  • setState() is used for full state replacement.
  • patchState() is used for partial updates, ideal for template pages controlling a few parameters.

getPanelSchema()

Returns the parameter panel description protocol, allowing the host to render all or partial parameters.

export()

Provides the export result; the host or generator-sdk decides how to download, open in Studio, or handle billing.

subscribe()

Subscribes to runtime events such as state changes, parameter schema changes, and errors.

dispatchWorkbenchCommand()

Receives workbench-side commands such as shell actions, route sync, or future host orchestration requests.

ts
interface WorkbenchCommand {
  type: string
  data?: unknown
}

This method is optional, but it is the recommended standard entry when generator-workbench or another host needs workbench -> runtime communication while keeping the runtime contract public and extensible.

To ensure compatibility across plain HTML, iframe, and script scenarios, the following convention is recommended:

ts
declare global {
  interface Window {
    __GENERATOR_RUNTIME__?: GeneratorRuntime
    __registerGeneratorRuntime__?: (runtime: GeneratorRuntime) => void
  }
}

State Model Recommendations

Generators are not required to share the same rendering engine, but they are required to share a unified protocol shape.

ts
interface GeneratorState {
  meta: {
    schemaVersion: string
    generatorId: string
    title?: string
    updatedAt?: string
  }
  document: {
    width?: number
    height?: number
    unit?: 'mm' | 'px' | 'in'
    layers?: Array<Record<string, unknown>>
  }
  params: Record<string, unknown>
  selection?: {
    activeLayerId?: string | null
    activeTool?: string | null
  }
  view?: {
    zoom?: number
    panX?: number
    panY?: number
    panelTab?: string
    expandedGroups?: string[]
  }
  assets?: Array<Record<string, unknown>>
}
  • meta and document should be stable and usable for cloud save, restore, and template page rendering.
  • params is used for parameter panel field binding.
  • selection and view are allowed to be editor-state data and are not required to be fully consumed by template pages.
  • state must be JSON-serializable.

PanelSchema

Template pages should not hardcode form logic for each generator; they should consume the parameter schema returned by the generator.

ts
interface PanelSchema {
  version: string
  generatorId: string
  groups: PanelGroup[]
}

interface PanelGroup {
  id: string
  title: string
  description?: string
  order?: number
  collapsible?: boolean
  defaultExpanded?: boolean
  fields: PanelField[]
}

interface BaseField {
  id: string
  label: string
  type: string
  bind: {
    path: string
  }
  helpText?: string
  readonly?: boolean
}

V1 recommends prioritizing these basic fields:

  • text
  • number
  • slider
  • select
  • toggle
  • color
  • image
  • custom

If a generator has complex custom controls, use custom, but still expose its placeholder and binding information via the unified schema.

PanelFilter

When a template page renders only partial parameters, it must be clipped via PanelFilter rather than re-hardcoding logic.

ts
interface PanelFilter {
  includeGroups?: string[]
  excludeGroups?: string[]
  includeFields?: string[]
  excludeFields?: string[]
  readonlyFields?: string[]
  hiddenFields?: string[]
  orderOverrides?: Array<{ id: string; order: number }>
}

Filtering Rules

  • If a group is excluded, all its fields are invisible.
  • If a group has no visible fields after filtering, it is automatically hidden.
  • includeFields / excludeFields / readonlyFields should prioritize using field.bind.path for matching.
  • readonlyFields only changes interaction permissions, not field visibility.
  • The host must not directly modify the generator's internal field definitions; it can only clip them via the filter.

Template Definition Protocol

When a generator needs to support "Publish Template / Import Template," it is recommended to use the template module of generator-sdk rather than designing a private JSON structure for each generator.

ts
interface GeneratorTemplateDefinition {
  type: 'generator-template'
  version: '1.0.0'
  generatorId: string
  appKey?: string
  templateMeta?: Record<string, unknown>
  defaults: Record<string, unknown>
  panelFilter: PanelFilter
  adjustableFields: Array<{
    groupId: string
    groupTitle?: string
    fieldId: string
    fieldLabel?: string
    path: string
  }>
  metadata?: Record<string, unknown>
}

Conventions:

  1. defaults is the default state snapshot of the template.
  2. panelFilter is the unique standard for the template consumption side; hosts should consume it directly rather than re-deriving field rules.
  3. adjustableFields is only for template authoring tool display and does not replace panelFilter.
  4. After importing a template, the host should restore template capabilities via runtime.setState() / runtime.patchState() + panelFilter.

If the runtime already integrates the template protocol, it can optionally expose a capability flag:

ts
capabilities: {
  supportsTemplates?: true
}

embed Mode Constraints

New generators in embed mode must follow these rules:

  1. Do not render top navigation.
  2. Do not render platform shell entries like login, sharing, and credits.
  3. Do not trigger billing independently; it should be triggered by the host in combination with generator-sdk.
  4. Do not rely on global body layout for core rendering.
  5. Do not hardcode page dimensions; must adapt to container size.
  6. Must support read-only mode.
  7. Core canvas results must be consistent between full and embed modes for the same state.

If an old generator can only run partially in embed mode but does not yet have an independent full entry point, it should still not be claimed as "Standardization Refactoring Complete."

Event Protocol

ts
type RuntimeEvent =
  | { type: 'ready' }
  | { type: 'state-change'; state: GeneratorState; patch?: StatePatch; source?: string }
  | { type: 'params_change'; data: { field: string; value: unknown; params?: Record<string, unknown> } }
  | { type: 'panel-schema-change'; schema: PanelSchema }
  | { type: 'select_template'; data: { name: string; category?: string } }
  | { type: 'warning'; code: string; message: string }
  | { type: 'error'; code: string; message: string }

V1 Minimal Requirements:

  • Trigger ready once after the initial mount.
  • Trigger state-change after parameter modification.
  • If the runtime needs field-level parameter feedback, trigger params_change after a panel parameter changes.
  • Trigger panel-schema-change when schema visibility changes with state.

If the runtime needs to notify the host about additional business-side interactions, it can additionally emit:

  • params_change
  • select_template

Rules:

  • params_change.data.field is required and represents the changed parameter field name.
  • params_change.data.value is required and represents the new field value.
  • params_change.data.params is optional and can contain the current full params object.
  • data.name is required and represents the selected template name.
  • data.category is optional and represents the template category.
  • When the runtime is hosted by generator-workbench and the page route is ?mode=embed, the workbench forwards this runtime event to the parent page bridge as generator_toSelectTemplate.

Recommended pattern:

ts
const listeners = new Set<(event: RuntimeEvent) => void>()

function emit(event: RuntimeEvent) {
  listeners.forEach((listener) => listener(event))
}

function onTemplateCardClick(template: { name: string; category?: string }) {
  emit({
    type: 'select_template',
    data: {
      name: template.name,
      category: template.category,
    },
  })
}

function onPanelFieldChange(field: string, value: unknown, params: Record<string, unknown>) {
  emit({
    type: 'params_change',
    data: {
      field,
      value,
      params,
    },
  })
}

Workbench To Runtime Command Convention

For bidirectional communication, the recommended split is:

  • subscribe(listener) for runtime -> workbench
  • dispatchWorkbenchCommand(command) for workbench -> runtime

Example:

ts
runtime.dispatchWorkbenchCommand = async (command) => {
  if (command.type === 'open-login') {
    openLoginPanel(command.data)
  }
}

When this contract is used inside generator-workbench, the host-facing method name is workbench.dispatchRuntimeCommand(...). It is an adapter around the same direction: workbench -> runtime.

Export Protocol

ts
type ExportAction =
  | 'cover'
  | 'download-svg'
  | 'download-png'
  | 'open-in-studio'

interface ExportResult {
  dataUrl: string
  mimeType: string
  ext: string
  width?: number
  height?: number
  metadata?: Record<string, unknown>
}

Division of Responsibility:

  • Runtime is responsible for generating export data.
  • generator-sdk is responsible for downloading to local, opening in Studio, and combining with billing.
  • The host decides when to trigger the export.

Host Integration Example

Typical integration of left-side canvas + right-side partial parameters in a template page:

ts
const runtime = window.__GENERATOR_RUNTIME__
const sdk = GeneratorSDK.init({ appKey: 'your_app_key', env: 'prod' })
const importedTemplate = sdk.template.parse(templateText)
const snapshot = sdk.template.toRuntimeSnapshot(importedTemplate)

await runtime.mount({
  mode: 'embed',
  target: 'canvas',
  container: canvasContainer,
  state: snapshot.state,
})

const panelSchema = runtime.getPanelSchema({
  panelFilter: {
    ...snapshot.panelFilter,
  },
})

await runtime.mount({
  mode: 'embed',
  target: 'panel',
  container: panelContainer,
  state: snapshot.state,
  panelFilter: snapshot.panelFilter,
})

Relationship with Old Bridge

Existing generators commonly use window.__GENERATOR_BRIDGE__. V1 allows compatibility via adapters, but it is not recommended for new generators to implement only the bridge.

Recommended mapping:

  • getSnapshot() -> getState()
  • applySnapshot() -> setState()
  • getExportData() -> export()
  • _sync() -> subscribe() + state-change

If only the adapter mapping from bridge to runtime is completed, it is recommended to describe it as "Compatibility Refactoring Complete" or "Phase 1/2 Complete" in external reports. Do not describe it as "Standard Generator Refactoring Complete."

V1 Non-Goals

The following are not goals of V1:

  • A universal JSON canvas rendering engine.
  • Forcing all generators to share the same layer schema.
  • All parameter controls being rendered uniformly by the platform.
  • One-time migration of all old generators.

V1 only defines a minimal protocol that can be consumed by Skills, MCP, Starters, and template pages.

New generators are recommended to be organized as follows:

txt
src/
  core/
    state.ts
    renderer.ts
    panel-schema.ts
    export.ts
  runtime/
    index.ts
    mount-canvas.ts
    mount-panel.ts
  pages/
    full.ts
    embed.ts
  platform/
    sdk.ts

One-Sentence Conclusion

The essence of the Generator Runtime Contract is:

Standardizing how generators are embedded and run across different hosts, rather than transforming all generators into the same rendering engine.

A final note for existing generator refactoring:

The runtime contract is important, but it is only one part of standardization refactoring and should not replace the final completion gates.

MIT Licensed