Skip to content

Commit dda35d0

Browse files
authored
Merge pull request #12 from reoring/feat/subworkflow-completion-check
feat(workflow): subworkflow completion_check loop
2 parents 0a9c96c + bddab65 commit dda35d0

30 files changed

Lines changed: 1700 additions & 157 deletions

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on Keep a Changelog, and this project follows SemVer where practical.
66

7+
## [0.1.5] - 2026-02-25
8+
9+
### Added
10+
11+
- Allow subworkflow steps (`workflow:`) to use `completion_check` loops (`max_iterations`) to rerun child workflows until complete.
12+
- Add subworkflow event bubbling (`bubble_subworkflow_events`, `subworkflow_event_prefix`) and deterministic export copying (`exports_mode: merge|replace`).
13+
- Improve workflow TUI: ANSI-aware wrapping in tabs, and `y` to copy the visible detail tab to the clipboard (system clipboard commands or OSC52 fallback).
14+
- Add a runnable subworkflow loop example (`examples/subworkflow-loop.yaml`, `examples/subworkflow-loop-child.yaml`).
15+
16+
### Changed
17+
18+
- Completion checks accept decisions from `decision_file` JSON or text markers (`PASS`/`FAIL`/`COMPLETE`/`INCOMPLETE`), and can fall back to parsing worker stdout (JSON or marker).
19+
- Completion check instructions interpolate `$ROBOPPI_COMPLETION_CHECK_ID` / `${ROBOPPI_COMPLETION_CHECK_ID}` to the active check id for robustness.
20+
- `scripts/agent-pr-loop/todo-check.sh` now respects `ROBOPPI_TODO_PATH` / `ROBOPPI_LOOP_DIR` and prefers legacy `.agentcore-loop` when present.
21+
722
## [0.1.4] - 2026-02-22
823

924
### Fixed

‎docs/workflow-design.md‎

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ steps:
4444
4545
### 2.2 StepDefinition
4646
47+
Each step is one of:
48+
49+
- **Worker step**: runs a worker (`worker` + `instructions` + `capabilities`)
50+
- **Subworkflow step**: runs a child workflow (`workflow`) and optionally exports child artifacts (`exports`)
51+
52+
`worker` and `workflow` are mutually exclusive.
53+
4754
```yaml
4855
steps:
4956
<step_id>:
@@ -59,6 +66,16 @@ steps:
5966
capabilities: # allowed operations
6067
- enum # READ | EDIT | RUN_TESTS | RUN_COMMANDS
6168
69+
# ---- Subworkflow config (alternative to Worker config) ----
70+
workflow?: string # child workflow YAML path (mutually exclusive with worker)
71+
exports?: # optional: copy child artifacts into this step's context
72+
- from: string # child step id
73+
artifact: string # child artifact name
74+
as?: string # name in parent context (default: artifact)
75+
exports_mode?: enum # merge | replace
76+
bubble_subworkflow_events?: boolean # default: false (blackbox) / true (show child steps)
77+
subworkflow_event_prefix?: string # default: auto (= parent step id)
78+
6279
# ---- DAG dependencies ----
6380
depends_on?: string[] # list of prerequisite step ids
6481
@@ -111,9 +128,12 @@ completion_check:
111128
```
112129

113130
The checker worker evaluates artifact state and returns **complete / incomplete**.
114-
If incomplete, the main worker is rerun (up to `max_iterations`).
131+
If incomplete, the step is rerun (up to `max_iterations`).
115132

116-
Because the checker and main worker run in the same `workspace`, file state naturally carries over across iterations.
133+
- For worker steps: rerun the worker
134+
- For subworkflow steps: rerun the child workflow
135+
136+
Because the checker and the step execution run in the same `workspace`, file state naturally carries over across iterations.
117137

118138
### 2.6 Full type definition (TypeScript representation)
119139

@@ -128,25 +148,71 @@ interface WorkflowDefinition {
128148
steps: Record<string, StepDefinition>;
129149
}
130150
131-
interface StepDefinition {
151+
type StepDefinition = WorkerStepDefinition | SubworkflowStepDefinition;
152+
153+
interface WorkerStepDefinition {
132154
description?: string;
133155
agent?: string; // optional agent profile id
134156
worker: "CODEX_CLI" | "CLAUDE_CODE" | "OPENCODE" | "CUSTOM";
157+
model?: string;
135158
workspace?: string;
136159
instructions: string;
137160
capabilities: ("READ" | "EDIT" | "RUN_TESTS" | "RUN_COMMANDS")[];
161+
138162
depends_on?: string[];
139163
inputs?: InputRef[];
140164
outputs?: OutputDef[];
141165
timeout?: DurationString;
142166
max_retries?: number;
143167
max_steps?: number;
144168
max_command_time?: DurationString;
169+
145170
completion_check?: CompletionCheckDef;
146171
convergence?: ConvergenceDef; // optional; opt-in convergence control for loops
147172
max_iterations?: number; // default: 1 (no loop)
148173
on_iterations_exhausted?: "abort" | "continue";
149174
on_failure?: "retry" | "continue" | "abort";
175+
176+
// Disallow subworkflow-only fields
177+
workflow?: never;
178+
exports?: never;
179+
}
180+
181+
interface ExportRef {
182+
from: string;
183+
artifact: string;
184+
as?: string;
185+
}
186+
187+
interface SubworkflowStepDefinition {
188+
description?: string;
189+
workflow: string;
190+
exports?: ExportRef[];
191+
exports_mode?: "merge" | "replace";
192+
bubble_subworkflow_events?: boolean;
193+
subworkflow_event_prefix?: string;
194+
195+
depends_on?: string[];
196+
inputs?: InputRef[];
197+
timeout?: DurationString;
198+
max_retries?: number;
199+
200+
completion_check?: CompletionCheckDef;
201+
convergence?: ConvergenceDef;
202+
max_iterations?: number;
203+
on_iterations_exhausted?: "abort" | "continue";
204+
on_failure?: "retry" | "continue" | "abort";
205+
206+
// Disallow worker-only fields
207+
agent?: never;
208+
worker?: never;
209+
model?: never;
210+
workspace?: never;
211+
instructions?: never;
212+
capabilities?: never;
213+
outputs?: never;
214+
max_steps?: never;
215+
max_command_time?: never;
150216
}
151217
152218
interface ConvergenceStageDef {

‎examples/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ timeout 10s ./roboppi daemon examples/daemon/simple-cron.yaml --verbose
4040
- `examples/build-test-report.yaml`: build -> test (parallel) -> report
4141
- `examples/failure-recovery.yaml`: failure handling / retry patterns
4242
- `examples/todo-loop.yaml`: simple iterative loop pattern
43+
- `examples/subworkflow-loop.yaml`: subworkflow step + completion_check loop (see `examples/subworkflow-loop-child.yaml`)
4344
- `examples/agent-pr-loop.yaml`: larger multi-step agent loop (see demo under `examples/agent-pr-loop-demo/`)
4445
- `examples/appthrust-dashboard/workflow.yaml`: production workflow for a team project (design -> todo -> implement -> validate)
4546

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Child workflow used by examples/subworkflow-loop.yaml
2+
3+
name: subworkflow-loop-child
4+
version: "1"
5+
timeout: "1m"
6+
7+
steps:
8+
produce:
9+
worker: CUSTOM
10+
instructions: |
11+
set -euo pipefail
12+
mkdir -p .roboppi-loop
13+
14+
n=0
15+
if [ -f .roboppi-loop/counter ]; then
16+
n="$(cat .roboppi-loop/counter)"
17+
fi
18+
n=$((n+1))
19+
echo "$n" > .roboppi-loop/counter
20+
21+
echo "run=$n" > report.txt
22+
if [ "$n" -ge 3 ]; then
23+
echo "OK" >> report.txt
24+
fi
25+
cat report.txt
26+
capabilities: [READ, EDIT, RUN_COMMANDS]
27+
outputs:
28+
- name: report
29+
path: report.txt

‎examples/subworkflow-loop.yaml‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Subworkflow loop example
2+
#
3+
# Runs a child workflow via a `workflow:` step and uses a CUSTOM completion_check
4+
# to repeat the subworkflow until it becomes COMPLETE.
5+
#
6+
# Run:
7+
# ./roboppi workflow examples/subworkflow-loop.yaml --verbose
8+
# # (dev)
9+
# bun run src/workflow/run.ts examples/subworkflow-loop.yaml --verbose
10+
11+
name: subworkflow-loop
12+
version: "1"
13+
timeout: "5m"
14+
15+
steps:
16+
cycle:
17+
workflow: "./subworkflow-loop-child.yaml"
18+
19+
# Show child steps in the UI as `cycle/<child-step-id>`.
20+
bubble_subworkflow_events: true
21+
22+
# Copy exported artifacts deterministically across iterations.
23+
exports_mode: replace
24+
exports:
25+
- from: produce
26+
artifact: report
27+
as: child-report
28+
29+
# exit 0 = COMPLETE, exit 1 = INCOMPLETE
30+
completion_check:
31+
worker: CUSTOM
32+
instructions: |
33+
set -euo pipefail
34+
if [ -f report.txt ] && grep -q '^OK$' report.txt; then
35+
exit 0
36+
fi
37+
exit 1
38+
capabilities: [READ, RUN_COMMANDS]
39+
40+
max_iterations: 5
41+
on_iterations_exhausted: abort
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# completion_check: decision_file の check_id 注入が brittle($ENV がそのまま書かれる)
2+
3+
ステータス: Open
4+
5+
関連:
6+
7+
- docs/issues/completion-check-decision-stability.ja.md
8+
9+
## 問題
10+
11+
completion_check の `decision_file` が JSON であっても、`check_id` が現在のチェック ID と一致しない場合、runner が以下のように扱って workflow が落ちる。
12+
13+
- `stale decision_file check_id mismatch`
14+
- `Completion check infrastructure failure repeated`
15+
16+
現場の典型パターン:
17+
18+
- worker に「`check_id` は `$AGENTCORE_COMPLETION_CHECK_ID` を使え」と指示する
19+
- worker が **環境変数を展開できず**、文字列 `$AGENTCORE_COMPLETION_CHECK_ID` をそのままファイルに書く
20+
- runner は mismatch を stale とみなして FAIL
21+
22+
これは「プロンプト調整の問題」ではなく、**runner と worker のインターフェイス設計**の問題になっている。
23+
24+
## 再現(最小)
25+
26+
1. workflow で completion_check に `decision_file: .agentcore-loop/review.verdict` を指定
27+
2. completion_check の instructions に次を要求
28+
29+
```json
30+
{"decision":"incomplete","check_id":"$AGENTCORE_COMPLETION_CHECK_ID","reason":"not-ready"}
31+
```
32+
33+
3. 実行すると runner が `check_id_match=false` で停止する
34+
35+
## 期待(理想)
36+
37+
- worker が shell を実行できない/しない場合でも `check_id` を正しく書ける
38+
- `check_id` mismatch が起きても、少なくとも「直し方」が明確で、即 hard-fail にならない
39+
40+
## 根本解決案(候補)
41+
42+
### 案A: runner が check_id を “文字列として” prompt に注入する
43+
44+
worker は env var を読めないことがあるため、runner が instructions の先頭に **実値**を埋め込む。
45+
46+
例:
47+
48+
- `CompletionCheckID: <uuid>` を固定フォーマットで提示
49+
- workflow DSL に `{{completion_check_id}}` のようなテンプレートを導入し、runner が展開
50+
51+
Pros:
52+
53+
- LLM の出力だけで `check_id` が成立
54+
- 最も小さい変更で効果が大きい
55+
56+
Cons:
57+
58+
- workflow DSL のテンプレート導入(または自動注入)の設計が必要
59+
60+
### 案B: decision_file の path を check_id で一意化(content で持たない)
61+
62+
`decision_file` を固定パスではなく、check_id を含むファイル名にする。
63+
64+
例:
65+
66+
- `.agentcore-loop/review.verdict.<uuid>.json`
67+
68+
runner は **その check_id のファイルだけ**を読む。
69+
70+
Pros:
71+
72+
- mismatch の概念が消える
73+
74+
Cons:
75+
76+
- artifact/cleanup/outputs の扱いが変わる
77+
78+
### 案C: runner が JSON wrapper を生成し、worker には marker だけ要求
79+
80+
worker 出力(または別ファイル)から `COMPLETE/INCOMPLETE` / `PASS/FAIL` を受け取り、runner が check_id を付与して JSON 化する。
81+
82+
Pros:
83+
84+
- LLM に strict な JSON ファイル書き込みを要求しない
85+
86+
Cons:
87+
88+
- runner 側の仕様追加が必要(decision_source の優先順位も含む)
89+
90+
### 案D: mismatch を “infra failure” ではなく “INCOMPLETE + actionable” にする
91+
92+
`check_id` mismatch は、実運用では「worker が書き方を間違えた」ケースが多い。
93+
94+
- hard-fail ではなく INCOMPLETE として次 iteration で上書き可能にする
95+
- エラーメッセージに `expected=<uuid> got=<value>` を必ず含める
96+
- 可能なら `suggested_fix: rm -f <decision_file>` のようなガイドを出す
97+
98+
## 受け入れ条件
99+
100+
- `decision_file` が JSON で `decision` を含む場合、`check_id` を worker が確実に正しく書ける(少なくとも案A/B/C のいずれかで達成)
101+
- `check_id` mismatch が発生しても、workflow が “インフラ故障” として即死しない(INCOMPLETE で継続できる or 明確な修正指示が出る)
102+
- unit test で以下をカバー
103+
- `check_id` 正常一致
104+
- `check_id` 不一致(stale/誤記/プレースホルダ)
105+
- JSON 形式エラー
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# completion_check: decision_file を LLM に書かせない(runner/スクリプトで生成する)
2+
3+
ステータス: Open
4+
5+
関連:
6+
7+
- docs/issues/completion-check-decision-stability.ja.md
8+
- issues/completion-check-check-id-templating.ja.md
9+
10+
## 問題
11+
12+
completion_check の “正規経路” が `decision_file` の structured JSON になったことで、workflow 側は次を worker に要求しがちになる。
13+
14+
- JSON 形式が正しい
15+
- 必須キー(decision/check_id/reason 等)が揃う
16+
- `check_id` が現行の completion check id と一致する
17+
18+
しかし実際には、LLM worker に strict JSON のファイル書き込みを要求するのは brittle で、次のような事故が起きる。
19+
20+
- JSON ではなく `PASS/FAIL` を書いてしまう
21+
- JSON だが `check_id` がプレースホルダ(`$AGENTCORE_COMPLETION_CHECK_ID`)のまま
22+
- 余計なコードフェンス/説明が混ざる
23+
24+
結果として `Completion check infrastructure failure` のような “中身の改善とは無関係な失敗” が発生し、ループが止まる。
25+
26+
## 根本方針
27+
28+
decision_file は **LLM ではなく runner か deterministic なスクリプト**が生成する。
29+
30+
LLM は以下のどれか “緩い出力” だけを提供する。
31+
32+
- 最終行 marker: `COMPLETE` / `INCOMPLETE`
33+
- `PASS` / `FAIL`
34+
- `verdict.txt` に `complete|incomplete`
35+
- `verdict.json` に `{decision: ..., reasons: ...}`(ただし check_id は runner が付与)
36+
37+
## 提案(実装案)
38+
39+
### 案1: completion_check を CUSTOM に寄せる(JSON wrapper を生成)
40+
41+
workflow DSL:
42+
43+
- worker(LMM) は `.agentcore-loop/review.marker` に `COMPLETE/INCOMPLETE` を書く
44+
- completion_check(CUSTOM script) が:
45+
- marker を読む
46+
- `AGENTCORE_COMPLETION_CHECK_ID` を env から取得
47+
- `decision_file` に JSON を確実に書く
48+
49+
この場合、LLM に check_id を触らせない。
50+
51+
### 案2: runner が “marker->JSON” を内蔵する
52+
53+
runner は `decision_file` を読む前に:
54+
55+
- worker 出力 marker(互換)を読み
56+
- JSON に正規化して `decision_file` を自動生成(またはメモリ上で解釈)
57+
58+
### 案3: runner が decision_file の check_id を自動付与する
59+
60+
`decision_file` が JSON で `decision` を含むが `check_id` が欠ける場合:
61+
62+
- runner が現行 check_id を付与する(stale 防止は path/mtime 併用で担保)
63+
64+
## 受け入れ条件
65+
66+
- worker が shell を実行できない環境でも安定して completion_check が回る
67+
- structured decision が必須でも `check_id` mismatch で落ちない
68+
- 既存 workflow(PASS/FAIL, COMPLETE/INCOMPLETE)の後方互換を維持

0 commit comments

Comments
 (0)