Jack Chen 01b63594fb feat(core): add Black Forest Labs image adapter (#614)
* feat(core): add Black Forest Labs image adapter

Black Forest Labs (FLUX) generates asynchronously: a submit returns a polling_url, the task is polled until ready, and the image is downloaded from a result URL. BlackForestLabsImageAdapter runs that flow with fetch, sends input images as input_image, input_image_2, and so on, and maps a WIDTHxHEIGHT size to width and height.

A moderated task shows up only in the poll status. The adapter reports Request Moderated and Content Moderated as a non-retryable content_policy failure as soon as it sees them, so runImage moves to the next model instead of polling until the attempt deadline and retrying. Failed and lost tasks are retryable.

The API key goes only to the API origin and HTTPS hosts under bfl.ai, keyed requests refuse redirects, and the pre-signed image download carries no key. providerOptions follows the same rule as the other adapters: fields that carry the prompt or input images are rejected at construction, and width and height are defaults that request.size overrides.

Follow-up to #613.

* fix(core): report detected image media type and the BFL dimensions sent

The built-in image adapters declared a fixed media type, which was wrong whenever providerOptions chose another output format, for example output_format png on Black Forest Labs or jpeg on OpenAI. All four now report the type read from the returned bytes and fall back to the old default only when the bytes are not a recognized image.

The Black Forest Labs attempt parameters kept default width and height from providerOptions even when request.size replaced them in the request body. They now record the same width and height the request sent.

* fix(core): keep a submitted BFL task through transient poll failures

A transient failure while polling or downloading, such as a 429, a 5xx, or a network error, escaped the adapter as retryable, so runImage retried generate() and submitted another billable task while the first kept running. The Black Forest Labs adapter now retries those failures in place until the attempt deadline, honoring Retry-After. Non-retryable errors still throw at once, and so does a 4xx on the download, since an expired delivery link cannot recover without a new task.

The adapter contract now says what it meant: an adapter must not resubmit a generation internally, while retrying a status poll or download of an already-submitted task is allowed.

* fix(core): type rate-limited and timed-out image downloads

downloadImage turned every non-2xx status into api_error, so a 429 or 408 on a pre-signed result URL did not count as transient for the Black Forest Labs in-place retry, escaped to runImage, and resubmitted a billable task. A download 429 is now rate_limit with Retry-After and a 408 is timeout, so both are retried in place against the existing task.

* fix(core): scope BFL credentials and keep a submitted task final at the deadline

The Black Forest Labs adapter sent its key to any HTTPS host under bfl.ai. Behind a proxy that key belongs to the proxy, and a forwarded BFL polling URL would receive it. Sibling BFL hosts now qualify only when baseURL is itself a BFL host; otherwise the key goes to the configured origin alone.

A transient outage that lasted past the attempt deadline still led runImage to retry generate() and submit another billable task. Once a task is submitted, the adapter now reports a deadline as a non-retryable timeout, and runImage keeps a non-retryable adapter verdict at the deadline instead of replacing it with a retryable timeout, so the chain moves to the next model. A deadline before the submit completes stays retryable.

* fix(core): tell deadlines from cancellation and end BFL turns on permanent download failures

runImage now aborts an attempt's signal with a DOMException named TimeoutError when the deadline fires, the same convention as AbortSignal.timeout(), and with the caller's own reason on cancellation. The Black Forest Labs adapter turns only a deadline after the submit into a final timeout; a caller that cancels a direct generate() call gets its own abort reason back.

A permanent failure downloading a submitted task, such as an expired or missing result link, was still retryable, so runImage submitted and paid for another task. Once the task exists, a download failure that is not transient now ends that model's turn as non-retryable.

* fix(core): end a submitted BFL task's turn on any failure and keep its cost

Several failures after a Black Forest Labs task was submitted were still retryable: a Ready status without result.sample, an Error or Failed task, and a lost task. runImage retried them by submitting and paying for another task. Every failure after the submit now ends that model's turn as non-retryable.

The adapter's in-place retry of transient poll and download failures slept through any Retry-After, so a header above runImage's maxRetryAfterMs stalled the attempt until its deadline. runImage now passes its cap to adapters as ImageCallOptions.maxRetryAfterMs, and a longer wait ends the turn at once.

A failure after the submit also dropped the task ID and the cost BFL reported, because adapter params were only returned on success. ImageModelError now carries optional params, runImage copies them into the failed attempt record, and the BFL adapter attaches the task and its cost to every post-submit failure.

* fix(core): close the last BFL paths that resubmit a created task

Two failures after a successful Black Forest Labs submit still reached runImage as retryable and led it to submit and pay for another task: a submit response without polling_url, and a result download that answers 200 with bytes that are not an image. Both now end that model's turn as non-retryable invalid_output and carry the task ID and cost into the failed attempt record. The adapter checks the downloaded bytes itself instead of leaving it to runImage, whose retry of an invalid output would resubmit.
2026-09-24 03:59:11 +08:00


OMA

Agents your organization can own, approve, and audit.
OMA (Open Multi-Agent) is a self-hosted TypeScript agent runtime: consequential actions wait for durable, tamper-evident approvals, and every run leaves a record you can verify offline, byte for byte.

npm version Node.js version CI Supply chain audit codecov MIT License

Website · Docs · Examples · npm

English · 中文


No telemetry. No hosted control plane. Your keys, your models — cloud, local (Ollama, vLLM, llama-server), or Chinese providers — your environment. Nothing stops working when the people who built it leave.

Get started

Requires Node.js 20 or newer. For production, use a currently maintained Node.js LTS release. Node.js 20 is upstream-EOL and retained only as a migration compatibility window; OMA will remove it in the next major release, no earlier than 2026-10-31.

Scaffold a PR review agent, security analysis agent, or teaching DAG:

npm create oma-app@latest my-oma

In an interactive terminal, that one command selects a starter and runtime, installs dependencies, and runs a deterministic local demo. The demo needs no API key and makes no model request: scripted model responses drive the real OMA scheduler, result aggregation, and offline dashboard.

Or add OMA to an existing backend:

npm install @open-multi-agent/core
import { FileStore, OpenMultiAgent } from '@open-multi-agent/core'

// Your keys and your endpoint: a hosted provider, or a local server through baseURL.
const oma = new OpenMultiAgent({
  defaultProvider: 'openai',
  defaultModel: 'gpt-5.4',
  // Consequential tool calls (file writes, shell) pause for a human decision.
  onToolCall: ({ consequential }) => (consequential ? { action: 'suspend' } : { action: 'allow' }),
})

const team = oma.createTeam('ops', {
  name: 'ops',
  agents: [{ name: 'operator', systemPrompt: 'Reconcile overdue invoices.', toolPreset: 'readwrite' }],
})

// The checkpoint store keeps the run and its pending approvals durable.
const result = await oma.runTeam(team, 'Find overdue invoices and draft the reminders.', {
  checkpoint: { store: new FileStore('./.oma/run.json') },
})

// result.status?.code === 'suspended' until a reviewer decides result.pendingApprovals,
// each bound to a hash of exactly what the reviewer was shown.

Set OPENAI_API_KEY to run this example. Providers covers other hosted models, local servers, OpenAI-compatible endpoints, and AI SDK providers.

runAgent() runs a single agent, runTasks() executes an explicit pipeline, and runTeam() plans from a goal. The Core package guide walks through all three modes, provider and credential setup, and the production checklist. The example index lists every runnable example across basics, cookbook workflows, patterns, providers, and integrations.

Durable approvals

A plan, task dispatch, or tool-call gate can return suspend. The request is stored beside the checkpoint, bound to a SHA-256 hash of exactly what the reviewer saw, and the run resumes from that content after a restart. A decision is atomic and first-wins; a tampered request or a store without compare-and-set fails closed.

approval/durable.ts · durable-approval.test.ts (16 cases) · durable-approval-validation.test.ts (7 cases) · Guide

Verifiable journal

Attach a journal backend and the run records every block the model saw, every tool call and result, and every context rewrite. verifyRun() reads it back cold, offline, and checks that each block's named source event still reproduces it byte for byte; an evicted window is reported as inconclusive, not as a failure. It proves lineage and content, not that the file was never edited.

journal/verify.ts · journal/hash.ts · verify-run.test.ts (11 cases) · Guide

Governance floor

Declare governanceIntent: 'required' with requiredRoles, and the run is judged on an execution receipt: which roles ran, in what order, with which dependency edges, and whether an independent review happened. The evaluator never sees agent output text, and a run can succeed and still report unsatisfied.

orchestrator/governance.ts · observability/execution-receipt.ts · governance-floor.test.ts (16 cases) · Guide · Receipts

Runs where you run

  • No telemetry, no hosted control plane. A library with no OMA backend or account, and none planned. It makes no analytics, license, update, or phone-home request. Self-hosting
  • Your keys, your models. Built-in adapters for Anthropic, OpenAI, Azure OpenAI, Bedrock, Gemini, Grok, and Copilot, and for DeepSeek, Doubao, Hunyuan, MiniMax, MiMo, and Qiniu; Ollama, vLLM, and llama-server through baseURL; any OpenAI-compatible endpoint and Vercel AI SDK providers. Providers
  • Egress policy. offline or allowlist, checked before a built-in adapter connects. A child policy can only tighten its parent, an unenforceable transport fails closed, and process and ACP backends sit outside it. LLM egress policy

Built with OMA

open-multi-agent launched 2026-04-01 under MIT. Known users and integrations to date:

  • temodar-agent by Ali Sünbül. WordPress security analysis platform running OMA's built-in tools (bash, file_*, grep) inside a Docker runtime. Confirmed production use. (~60 stars)
  • Mark Galyan runs OMA fully offline on local quantized models, using the coordinator and context compaction to keep autonomous agent loops alive under tight VRAM limits. Contributor since the framework's first month.
  • Engram: "Git for AI memory." Syncs knowledge across agents instantly and flags conflicts. (repo, ~80 stars)
More users and integrations

Users

  • PR-Copilot by kidoom. AI pull-request review assistant running an OMA review team, with defineTool repo-context tools and a custom ContextStrategy for token-aware diff compression.
  • StuFlow by znc15. Terminal AI coding assistant on OMA's orchestration core, driving runAgent / runTasks / runTeam with a custom coordinator, paired with DeepSeek.
  • Reports to Charts Studio. Turns documents and research tables into slide-ready charts, using a five-role extraction council with structured outputs and deterministic validation.

Integrations

  • @agentsonar/oma: Sidecar detecting cross-run delegation cycles, repetition, and rate bursts.
  • CodingScaffold: Agentic-coding scaffold that lists OMA as an optional orchestration backend, with a runTeam workflow template.
  • baize-oma: HTTP adapter exposing OMA runAgent() and runTeam() as Baize slot capabilities.

We build customer-owned systems on OMA for organizations that need one. Email jack@yuanasi.com.

Sponsors

Paid sponsors supporting open-multi-agent. Sponsorship does not affect technical decisions or model recommendations.

Providers

  • Atlas Cloud: Full-modal AI inference platform giving one API for video, image, and LLM across 300+ curated models. $5 credit vouchers for OMA users, first come first served. See the Atlas Cloud setup guide.

Optional coordinator

runTeam() decomposes a goal into a task graph across agents. One model call turns the goal into task specs with assignees and dependencies, a deterministic scheduler executes them, and a second call writes the final answer from the completed task outputs. The coordinator is never consulted mid-run, and the finished run is data you can read back. Use runAgent() or runTasks() when you already know the work.

import { OpenMultiAgent } from '@open-multi-agent/core'

const oma = new OpenMultiAgent({ defaultProvider: 'openai', defaultModel: 'gpt-5.4' })

const team = oma.createTeam('research-team', {
  name: 'research-team',
  agents: [
    { name: 'researcher', systemPrompt: 'Find the relevant facts.' },
    { name: 'analyst', systemPrompt: 'Compare evidence and identify tradeoffs.' },
  ],
  sharedMemory: true,
})

const result = await oma.runTeam(team, 'Compare three approaches and recommend one.')

// Nothing above declares a task graph. The coordinator planned one at runtime,
// and the finished run is data you can read back.
for (const task of result.tasks ?? []) {
  console.log(`[${task.status}] ${task.title} → ${task.assignee ?? 'unassigned'}`, task.dependsOn)
}

console.log(result.agentResults.get('coordinator')?.output)
console.log(result.totalTokenUsage)

OMA Run Viewer replaying a real run: task DAG and span waterfall views with per-task status, assignee, tokens, and tool calls

The offline Run Viewer replaying a real run from the trace store: task DAG, span waterfall, and per-task evidence, with no hosted service involved.

Coordinator covers what it decides and what it is allowed to see. Plan replay freezes an approved plan, Consensus verifies outputs with independent judges, and External agents puts Claude Code, Gemini CLI, and Codex on the same task graph through process and ACP backends.

Packages

  • @open-multi-agent/core: Runtime, tools, memory, checkpoints, approvals, journal, traces, CLI, and offline Run Viewer.
  • @open-multi-agent/otel: Optional OpenTelemetry adapter for teams with a centralized OpenTelemetry stack.
  • create-oma-app: Scaffolder behind npm create oma-app; starter templates with a no-key local demo.

Core users can store traces locally and inspect them with the offline Run Viewer. Install the OTel package only when OMA traces should appear in the same monitoring system as the rest of your application.

Documentation

Goal Start here
Install and run All docs · Core package guide · Examples · CLI · Glossary · Production checklist
Configure models and tools Providers · LLM egress policy · Tools · Sandbox and shell · MCP · Structured input · External agents
Operate reliably Observability · Run Viewer · Run journal · Evaluation · Checkpoint and resume · Run store and leases · Durable approvals · Adaptive recovery · Context management · Errors
Control orchestration Coordinator · Consensus · Execution routing · Model routing · Task scheduling · Plan replay · Shared memory · Streaming · Budgets and limits

Contributing

Issues and pull requests are welcome. See CONTRIBUTING.md for workspace boundaries, validation, and submission guidance.

Contributor credits by area are in CONTRIBUTORS.md.

License

MIT

Maintained by YuanASI (Shenzhen YuanASI Technology Co., Ltd.).

S
Description
From a goal to a task DAG, automatically. TypeScript-native multi-agent orchestration with MCP and live tracing. Three runtime dependencies.
Readme MIT
21 MiB
0 Stars 1 Watchers 0 Forks
Latest
2026-07-11 01:46:24 -05:00
Languages
TypeScript 95.6%
JavaScript 2.7%
HTML 1.7%