Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions docs/compose/spec/workflow-auto-dag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
feature: workflow-auto-dag
status: delivered
updated: 2026-07-29
branch: feat/workflow-auto-dag
commits: 57ff02ed5ea76b448893b7c80faaf3a8de4d19ff..559901bf45a2d95b0759c677c8d2eec1318885d6
---

# Workflow Auto DAG

## Report

**What was built** — `/workflow <task>` now expands into a primary-agent contract that generates and launches one inline JavaScript dependency DAG. Natural language remains the default input, while optional JSON can customize each node's role, prompt, model, tools, schema, isolation, and dependencies. The generated workflow runs an independent structured Verifier after implementation and permits two rework rounds by default, with a hard maximum of three.

The sandbox now provides `dag(nodes, run)`. It validates the complete graph before invoking any node, rejects malformed nodes, duplicate IDs or dependencies, self-dependencies, unknown references, and cycles, then executes each topological ready batch concurrently. Dependency outputs and final results retain deterministic ordering. Existing `agent()` semaphores and content journals continue to provide concurrency limits and resume behavior without a new database schema.

**Verification** — `bun test test/command/workflow-command.test.ts test/command/deep-research-command.test.ts test/workflow/sandbox.test.ts test/workflow/builtin.test.ts test/tool/describe-workflow.test.ts` — PASS, 51 tests / 258 assertions. `bun test test/workflow/runtime-nested.test.ts test/workflow/retry.test.ts test/workflow/persistence.test.ts` — PASS, 26 passed / 1 skipped / 78 assertions. Final changed-file rerun (`workflow-command` + `sandbox`) — PASS, 43 tests / 239 assertions. `bun typecheck` — PASS. `git diff --check` — PASS. `test/workflow/runtime.test.ts` remains PRE-EXISTING/ENVIRONMENTAL: isolated rerun had four existing 5-second timeouts; the focused sandbox and stable runtime/persistence bands pass.

**Journey log**

1. The existing runtime already had the difficult stateful pieces: process-wide agent throttling, script persistence, content-journal replay, and nested-workflow cycle checks. A deterministic guest scheduler was enough; a second DAG persistence model would have duplicated invariants.
2. The legacy `compose.js` Kahn scheduler silently drops unknown dependencies. The reusable primitive instead validates the whole graph before execution so malformed generated plans cannot launch partial work.
3. Verifier rejection is semantic feedback, not a transient transport error. The command contract keeps bounded rework separate from `agent({ retry })`.
4. Independent review found no critical, high, or medium findings and identified one stale primitive list in the primary-agent prompt; the list was updated before final verification.
5. The broader `runtime.test.ts` band has timing/process instability independent of this change; focused sandbox tests and stable runtime/persistence suites provide direct evidence for the modified boundary.

## [S1] Problem

The dynamic Workflow Runtime can run JavaScript agents concurrently and resume them from its persisted journal, but users must currently author the orchestration script themselves or choose a fixed built-in workflow. There is no `/workflow <task>` entry point that asks the primary agent to turn a natural-language task into a customizable, validated task DAG with an independent acceptance gate and bounded rework.

## [S2] Design

### Slash-command contract

Add the experimental `/workflow <task>` command beside `/deep-research`. Its expanded prompt instructs the current primary agent to:

1. interpret `$ARGUMENTS` as natural language, optionally containing a JSON object that overrides node roles, prompts, models, tools, dependencies, verifier behavior, or the rework bound;
2. generate one self-contained inline JavaScript workflow whose static `meta` describes the run;
3. express implementation work as nodes passed to the sandbox `dag()` primitive, with each node carrying a stable ID, role, prompt, model, tools, and `dependsOn` list as needed;
4. launch each node through `agent()`, passing dependency results into its prompt and preserving the node's explicit role/model/tool choices;
5. run a fresh verifier agent only after the implementation DAG settles; the verifier judges the original task and concrete outputs independently, returns structured acceptance evidence, and cannot be the same call as an implementer;
6. perform at most the requested number of rework rounds (default 2, hard prompt-level cap 3), re-running the verifier after every round; return an honest non-accepted result when the bound is exhausted;
7. invoke `workflow({ operation: "run", script, args })` and relay its terminal result and run ID without claiming acceptance unless the verifier accepted it.

The command and its required tool share `MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL`, which is enabled by default and accepts `false` or `0` as an explicit opt-out. Invalid or empty user input is handled by the primary agent through one concise clarification rather than launching a guessed workflow.

### Standard DAG primitive

Expose `dag(nodes, run)` in every Workflow Sandbox alongside `parallel()` and `pipeline()`.

- Each node is an object with a non-empty, unique string `id` and optional `dependsOn: string[]`.
- Validate the complete graph before invoking `run`: reject malformed nodes, duplicate IDs, duplicate dependencies, self-dependencies, unknown dependency IDs, and cycles with actionable `dag:` errors.
- Use Kahn topological sorting to produce deterministic ready batches in declaration order.
- Execute every ready batch concurrently with `Promise.all`; begin a node only after all dependencies completed.
- Invoke `run(node, dependencies)`, where `dependencies` is an ordered array of `{ id, result }` following the node's `dependsOn` order.
- Resolve to an array of `{ id, result }` in original node declaration order. The primitive does not interpret `null` or domain-specific verifier output; workflow scripts own those policies.

Concurrency remains bounded by the Runtime's existing process-wide and per-run semaphores because DAG nodes call the existing `agent()` host hook. Persistence and recovery remain content-journal based: completed `agent()` calls replay on `resume`, while unfinished DAG nodes execute again. No new database schema is required.

### Safety and failure behavior

Graph validation runs entirely before node execution, so an invalid DAG launches zero agents. Script logic errors, including DAG structural errors, fail the workflow loudly through the existing sandbox error path. Agent transport failures retain the existing `agent() => null` and bounded transport retry contract; generated scripts must treat a required node's `null` result as a failed deliverable and must not report verifier acceptance.

The existing nested-workflow lineage guard remains unchanged. It detects recursion between workflow scripts; `dag()` separately detects cycles between task nodes within one script.

## [S3] Out of Scope

- A visual DAG editor or new TUI detail layout.
- A new persisted DAG/node database schema; recovery continues to use the existing script and agent-result journal.
- Executing arbitrary JavaScript supplied directly by the user without primary-agent mediation.
- Automatically merging branches, opening pull requests, or approving destructive operations.
- Replacing `/compose-next` or the legacy built-in `compose` workflow.
- Treating a verifier verdict as a security boundary; it is an independent quality gate within the workflow.

## Tasks

- [x] T1: add and test the sandbox `dag(nodes, run)` primitive — acceptance: valid dependency graphs execute in deterministic concurrent batches and return declaration-ordered results; every malformed-reference and cycle case rejects before any callback runs (covers: S2)
- [x] T2: register and test `/workflow <task>` behind the workflow feature flag — acceptance: command autocomplete/listing exposes `workflow` only when the tool is enabled, and its expanded prompt requires inline DAG generation, role/prompt/model/dependency customization, independent verifier evidence, and bounded rework (covers: S2)
- [x] T3: update model-facing workflow documentation — acceptance: the workflow tool contract names `dag()` and explains how scheduling, persistence recovery, task-cycle detection, verifier separation, and rework bounds compose (covers: S2)
- [x] T4: run focused tests, workflow regression tests, typecheck, and diff checks — acceptance: all relevant commands pass from `packages/opencode` (covers: S2)
3 changes: 2 additions & 1 deletion packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -590,11 +590,12 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
},
{
title: t("tui.command.workflow.list.title"),
description: t("tui.command.workflow.list.description"),
value: "workflow.list",
category: "session",
enabled: Flag.MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL,
slash: {
name: "workflows",
name: "worklist",
},
onSelect: () => {
dialog.replace(() => <DialogWorkflows />)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { useDialog } from "@tui/ui/dialog"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { useLanguage } from "@tui/context/language"
import { createMemo, onCleanup, onMount } from "solid-js"

export function DialogWorkflows() {
const dialog = useDialog()
const route = useRoute()
const sync = useSync()
const { t } = useLanguage()

const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))

Expand Down Expand Up @@ -47,5 +49,5 @@ export function DialogWorkflows() {
}))
})

return <DialogSelect title="Workflows" options={options()} />
return <DialogSelect title={t("tui.command.workflow.list.title")} options={options()} />
}
6 changes: 5 additions & 1 deletion packages/opencode/src/cli/cmd/tui/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ export const dict: Record<string, string> = {
"find repeated workflows in recent work and package them into skills, subagents, or commands",
"tui.slash.goal.description":
"set a stop-condition goal; runs until a judge says it's met. /goal clear to abort",
"tui.slash.workflow.description":
"generate and run a customizable verified multi-agent DAG for a task",
"tui.slash.deep-research.description":
"deep multi-source, fact-checked research report (runs the deep-research workflow)",

Expand Down Expand Up @@ -260,7 +262,9 @@ export const dict: Record<string, string> = {
// App-level commands
"tui.command.session.list.title": "Switch session",
"tui.command.session.new.title": "New session",
"tui.command.workflow.list.title": "Workflows",
"tui.command.workflow.list.title": "Workflow run list",
"tui.command.workflow.list.description":
"Browse and open workflow runs in this session (not /workflow)",
"tui.command.model.list.title": "Switch model",
"tui.command.model.cycle_recent.title": "Model cycle",
"tui.command.model.cycle_recent_reverse.title": "Model cycle reverse",
Expand Down
6 changes: 5 additions & 1 deletion packages/opencode/src/cli/cmd/tui/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ export const dict = {
"encontrar flujos repetidos en el trabajo reciente y empaquetarlos en skills, subagentes o comandos",
"tui.slash.goal.description":
"definir un objetivo con condición de parada; se ejecuta hasta que un juez confirme. /goal clear para abortar",
"tui.slash.workflow.description":
"generar y ejecutar un DAG multiagente personalizable y verificado para una tarea",
"tui.slash.deep-research.description":
"informe de investigación profunda multi-fuente y verificado (ejecuta el workflow deep-research)",

Expand Down Expand Up @@ -335,7 +337,9 @@ export const dict = {
// App-level commands
"tui.command.session.list.title": "Cambiar sesión",
"tui.command.session.new.title": "Nueva sesión",
"tui.command.workflow.list.title": "Flujos de trabajo",
"tui.command.workflow.list.title": "Lista de ejecuciones de workflow",
"tui.command.workflow.list.description":
"Explorar y abrir ejecuciones de workflow de esta sesión (no es /workflow)",
"tui.command.model.list.title": "Cambiar modelo",
"tui.command.model.cycle_recent.title": "Ciclo de modelos",
"tui.command.model.cycle_recent_reverse.title": "Ciclo de modelos (inverso)",
Expand Down
6 changes: 5 additions & 1 deletion packages/opencode/src/cli/cmd/tui/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ export const dict = {
"trouver les workflows répétés dans le travail récent et les empaqueter en skills, sous-agents ou commandes",
"tui.slash.goal.description":
"définir un objectif avec condition d'arrêt ; s'exécute jusqu'à ce qu'un juge confirme. /goal clear pour annuler",
"tui.slash.workflow.description":
"générer et exécuter un DAG multi-agents personnalisable et vérifié pour une tâche",
"tui.slash.deep-research.description":
"rapport de recherche approfondi multi-sources et vérifié (exécute le workflow deep-research)",

Expand Down Expand Up @@ -323,7 +325,9 @@ export const dict = {
// App-level commands
"tui.command.session.list.title": "Changer de session",
"tui.command.session.new.title": "Nouvelle session",
"tui.command.workflow.list.title": "Workflows",
"tui.command.workflow.list.title": "Liste des exécutions de workflow",
"tui.command.workflow.list.description":
"Parcourir et ouvrir les exécutions de workflow de cette session (pas /workflow)",
"tui.command.model.list.title": "Changer de modèle",
"tui.command.model.cycle_recent.title": "Modèles récents",
"tui.command.model.cycle_recent_reverse.title": "Modèles récents (inverse)",
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/cli/cmd/tui/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export const dict = {
"tui.slash.dream.description": "memory ファイルと生の軌跡からプロジェクトメモリを手動で統合",
"tui.slash.distill.description": "最近の作業から繰り返しワークフローを見つけ、skill・サブエージェント・コマンドにパッケージ化",
"tui.slash.goal.description": "停止条件付きゴールを設定;判定が達成と言うまで実行。/goal clear で中止",
"tui.slash.workflow.description": "タスクから検証付きのカスタム複数エージェント DAG を生成して実行",
"tui.slash.deep-research.description": "深い多ソース・ファクトチェック済み調査レポート(deep-research ワークフローを実行)",

// Built-in bundled skill descriptions (user-facing, decoupled from SKILL.md description which targets the LLM)
Expand Down Expand Up @@ -267,7 +268,8 @@ export const dict = {
// App-level commands
"tui.command.session.list.title": "セッションを切り替え",
"tui.command.session.new.title": "新規セッション",
"tui.command.workflow.list.title": "ワークフロー",
"tui.command.workflow.list.title": "ワークフロー実行一覧",
"tui.command.workflow.list.description": "このセッションのワークフロー実行を閲覧・開く(/workflow ではない)",
"tui.command.model.list.title": "モデルを切り替え",
"tui.command.model.cycle_recent.title": "モデルを循環",
"tui.command.model.cycle_recent_reverse.title": "モデルを逆循環",
Expand Down
6 changes: 5 additions & 1 deletion packages/opencode/src/cli/cmd/tui/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ export const dict = {
"найти повторяющиеся workflow в недавней работе и упаковать их в skills, субагентов или команды",
"tui.slash.goal.description":
"задать цель с условием остановки; выполняется, пока судья не подтвердит. /goal clear для отмены",
"tui.slash.workflow.description":
"сгенерировать и запустить настраиваемый проверенный multi-agent DAG для задачи",
"tui.slash.deep-research.description":
"глубокий многоисточниковый проверенный отчёт (запускает workflow deep-research)",

Expand Down Expand Up @@ -338,7 +340,9 @@ export const dict = {
// App-level commands
"tui.command.session.list.title": "Сменить сессию",
"tui.command.session.new.title": "Новая сессия",
"tui.command.workflow.list.title": "Рабочие процессы",
"tui.command.workflow.list.title": "Список запусков workflow",
"tui.command.workflow.list.description":
"Просмотреть и открыть запуски workflow в этой сессии (не /workflow)",
"tui.command.model.list.title": "Сменить модель",
"tui.command.model.cycle_recent.title": "Цикл моделей",
"tui.command.model.cycle_recent_reverse.title": "Цикл моделей (в обратном порядке)",
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/tui/i18n/slash-command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const BUILTIN = new Set(["init", "review", "dream", "distill", "goal", "deep-research"])
const BUILTIN = new Set(["init", "review", "dream", "distill", "goal", "workflow", "deep-research"])

export function slashCommandDescription(
t: (key: string) => string,
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/cli/cmd/tui/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export const dict = {
"tui.slash.dream.description": "从 memory 文件与原始轨迹中手动整合项目记忆",
"tui.slash.distill.description": "在最近工作中发现重复流程,打包为 skill、子智能体或命令",
"tui.slash.goal.description": "设置终止条件目标;运行直到判定达成。使用 /goal clear 中止",
"tui.slash.workflow.description": "根据任务生成并运行可自定义的、带验证的多智能体 DAG",
"tui.slash.deep-research.description": "深度多来源、事实核查的研究报告(运行 deep-research 工作流)",

// Built-in bundled skill descriptions (user-facing, decoupled from SKILL.md description which targets the LLM)
Expand Down Expand Up @@ -283,7 +284,8 @@ export const dict = {
// App-level commands
"tui.command.session.list.title": "切换会话",
"tui.command.session.new.title": "新建会话",
"tui.command.workflow.list.title": "工作流",
"tui.command.workflow.list.title": "工作流运行列表",
"tui.command.workflow.list.description": "浏览并打开本会话中的工作流运行(不是 /workflow)",
"tui.command.model.list.title": "切换模型",
"tui.command.model.cycle_recent.title": "循环切换模型",
"tui.command.model.cycle_recent_reverse.title": "反向循环切换模型",
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/cli/cmd/tui/i18n/zht.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export const dict = {
"tui.slash.dream.description": "從 memory 檔案與原始軌跡中手動整合專案記憶",
"tui.slash.distill.description": "在最近工作中發現重複流程,打包為 skill、子智慧代理或命令",
"tui.slash.goal.description": "設定終止條件目標;執行直到判定達成。使用 /goal clear 中止",
"tui.slash.workflow.description": "根據任務產生並執行可自訂、帶驗證的多智能體 DAG",
"tui.slash.deep-research.description": "深度多來源、事實核查的研究報告(執行 deep-research 工作流程)",

// Built-in bundled skill descriptions (user-facing, decoupled from SKILL.md description which targets the LLM)
Expand Down Expand Up @@ -283,7 +284,8 @@ export const dict = {
// App-level commands
"tui.command.session.list.title": "切換工作階段",
"tui.command.session.new.title": "新增工作階段",
"tui.command.workflow.list.title": "工作流程",
"tui.command.workflow.list.title": "工作流程執行清單",
"tui.command.workflow.list.description": "瀏覽並開啟本工作階段中的工作流程執行(不是 /workflow)",
"tui.command.model.list.title": "切換模型",
"tui.command.model.cycle_recent.title": "循環切換模型",
"tui.command.model.cycle_recent_reverse.title": "反向循環切換模型",
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2435,7 +2435,7 @@ function Workflow(props: ToolProps<typeof WorkflowTool>) {

// Counters/phase prefer the live metadata streamed by the tool's 250ms flush
// loop, falling back to the bus run row. The bus row only learns counters via
// loadWorkflows polling (which only the /workflows dialog runs), so during a run
// loadWorkflows polling (which only the /worklist dialog runs), so during a run
// the inline panel would otherwise sit at 0✓ 0✗ 0⟳ — the streamed metadata is
// the authoritative live source for this in-conversation view.
const counters = createMemo(() => {
Expand Down
Loading
Loading