Сайт

Оркестраторы

Orchestrator превращает граф во что-то исполняемое и запускает его. Объявите его в contributes.orchestrators, затем зарегистрируйте реализацию:

register(id: string, orchestrator: IOrchestrator): IDisposable

Передайте тот же короткий id, что и в манифесте. Fragmenta добавит к нему id вашего расширения, так что полный id оркестратора имеет вид: <publisher>.<name>.<id>. Если манифест не объявляет оркестратор с таким id, реализация не подключается: register всё равно вернёт disposable, а ошибка появится только в логе хоста расширения.

Вызов dispose() для регистрации отключает реализацию. То же происходит при деактивации расширения.

Контракт IOrchestrator

interface IOrchestrator {
  start?(options?: OrchestratorStartOptions): Promise<OrchestratorStartResult>
  build?(options?: OrchestratorBuildOptions): Promise<OrchestratorBuildResult>
  run(options?: OrchestratorRunOptions): Promise<OrchestratorRunResult>
  dsl(options?: OrchestratorDslOptions): Promise<OrchestratorDslResult>
}

Fragmenta проверяет, какие методы реализует ваш объект, один раз, при вызове register — методы, добавленные в объект позже, не учитываются. Fragmenta вызывает эти методы, когда пользователь работает с оркестратором, и отслеживает статус для каждого оркестратора:

type OrchestratorStatus = 'idle' | 'starting' | 'ready' | 'building' | 'built' | 'running' | 'error'

| Метод | Обязателен | Во время выполнения | При успехе | | --- | --- | --- | --- | | start | нет | starting | ready — результат может вернуть config.orchestratorUrl, Fragmenta смёржит его в конфиг оркестратора. | | build | нет | building | built | | run | да | — | running | | dsl | да | — | статус не меняется — возвращает { dsl }. |

Каждый результат может также содержать status. Если он задан, он заменяет статус из колонки При успехе — и для dsl тоже. Возврат status: 'error' переводит оркестратор в состояние ошибки, а вызов считается неудачным; config, возвращённый из start, при этом всё равно объединяется.

Кнопки запуска

Кнопка запуска в правом верхнем углу редактора вызывает start, build и run — но не dsl. Метод dsl нужен инструментам, которым граф требуется в формате вашего оркестратора. Кнопка показывает одно действие за раз, выбранное из методов, реализованных вашим объектом:

| Статус | Предлагаемое действие | | --- | --- | | idle | Первый реализованный из start, build, run. | | error | Действие, которое упало; если его нет — как для idle. | | ready | Первый реализованный из build, run, start. | | built, running | Первый реализованный из run, build, start. | | starting, building | Нет — кнопка показывает загрузку. |

Пока вызов выполняется, кнопка превращается в кнопку остановки, которая его прерывает.

Опции и результаты

Все объекты опций содержат необязательный abortSignal; build дополнительно принимает force, а dsl может получить явные nodes / edges для построения вместо текущего состояния редактора.

| Поле | Где | Описание | | --- | --- | --- | | abortSignal?: AbortSignal | все опции | Срабатывает, когда пользователь останавливает вызов. Прекратите работу и отклоните промис. | | force?: boolean | OrchestratorBuildOptions | Запрос полной пересборки. Как его трактовать, решает ваш оркестратор; кнопка запуска его не передаёт. | | nodes?: Node[], edges?: Edge[] | OrchestratorDslOptions | Граф, из которого собирается DSL. Если не переданы, необходимо вручную читать состояние редактора. | | status?: OrchestratorStatus | все результаты | Заменяет статус при успехе (см. выше). | | config?: Partial<OrchestratorConfig> | OrchestratorStartResult | Поверхностно объединяется в конфиг оркестратора поверх url из манифеста. | | dsl: unknown | OrchestratorDslResult | Граф в формате вашего оркестратора. |

OrchestratorStartOptions
interface OrchestratorStartOptions {
  readonly abortSignal?: AbortSignal
}
OrchestratorBuildOptions
interface OrchestratorBuildOptions {
  readonly abortSignal?: AbortSignal
  readonly force?: boolean
}
OrchestratorRunOptions
interface OrchestratorRunOptions {
  readonly abortSignal?: AbortSignal
}
OrchestratorDslOptions
interface OrchestratorDslOptions {
  readonly abortSignal?: AbortSignal
  readonly nodes?: Node[]
  readonly edges?: Edge[]
}
OrchestratorStartResult
interface OrchestratorStartResult {
  readonly status?: OrchestratorStatus
  readonly config?: Partial<OrchestratorConfig>
}

interface OrchestratorConfig {
  orchestratorUrl?: Url
}
OrchestratorBuildResult
interface OrchestratorBuildResult {
  readonly status?: OrchestratorStatus
}
OrchestratorRunResult
interface OrchestratorRunResult {
  readonly status?: OrchestratorStatus
}
OrchestratorDslResult
interface OrchestratorDslResult {
  readonly status?: OrchestratorStatus
  readonly dsl: unknown
}

Ошибки и отмена

Неудачный вызов — ошибка, отклонённый промис илиstatus: 'error' в результате — переводит оркестратор в состояние ошибки.

Пример

Локальный оркестратор обычно реализует start, поднимая свой бэкенд, и возвращает URL. В свобю очередь hosted-вариант декларирует url в манифесте и обходится без start — именно так воркфлоу-расширение поставляет Workflow Cloud и Workflow Local.

export function activate(context: ExtensionContext) {
  context.subscriptions.push(
    sdk.orchestrator.register('workflow-local', {
      async start() {
        const orchestratorUrl = await bootLocalBackend()
        return { status: 'ready', config: { orchestratorUrl } }
      },
      async build() {
        await deployNodes()
        return {}
      },
      async run() {
        await execute()
        return {}
      },
      async dsl() {
        const nodes = await sdk.project.nodeEditor.getNormalizedNodes()
        const edges = await sdk.project.nodeEditor.getNormalizedEdges()
        return { dsl: buildDsl(nodes, edges) }
      },
    }),
  )
}

Во время выполнения сообщайте о прогрессе обратно в граф с помощью setNodeExecutionState передавайте логи в output-канал.

Выражения в DSL

Поле может содержать ссылку вместо литерала — {{Fetch Orders.summary.count}}. В проекте она хранится с именем ноды, потому что именно этим и оперируют пользователи. Среда выполнения должна получить id, потому что он переживает переименование. Превратить одно в другое — задача оркестратора при сборке DSL:

const nodes = await sdk.project.nodeEditor.getNormalizedNodes()
const byName = new Map(nodes.map((node) => [node.metadata.displayName, node.id]))

// "Fetch Orders.summary.count" → "{{c0ffee-…summary.count}}"

Где хранится текст: port.template для входного порта, parameter.template для параметра. В value и defaultValue хранится литерал, а не выражение — поле может содержать и то, и другое, и пользователь переключается между ними.

Обе формы DSL допустимы, и оркестратор обычно хочет явно указать, какую из них он выдаёт:

| Связывание | Что в поле | Кто разрешает | | --- | --- | --- | | позднее | {{nodeId.path}} | рантайм, на каждом прогоне | | раннее | значение прошлого прогона | никто, оно вморожено |

Выражение также является зависимостью: нода, на которую ссылается поле, должна выполниться раньше, даже если между ними нет связи. Собирайте dependsOn из связей и из выражений, иначе порядок будет неверным.

Сообщайте обратно, что произошло: setNodeOutputs — что выдала нода, setNodeInputsResolved — результат выражения, тогда редактор покажет значение под полем.

extensions/example-workflow делает всё это в одном месте: грамматику и подсказки, обе привязки DSL и запуск, который возвращает значения в поля.