Graph 08. Advanced Parallel Patterns — 3+ Branches, Complex State
Learning Objectives
After completing this tutorial, you will be able to:
- Design and implement a 3-branch parallel research pattern.
- Compare designs where each branch writes to independent state keys versus shared keys.
- Implement a pattern that sums each branch's result count using
source_count(integer+sum_int). - Demonstrate the
PARALLEL_BRANCH_LIMIT_EXCEEDEDerror firsthand. - Understand the constraints of nested parallel_groups and design alternatives.
- Explain the
PARALLEL_SIDE_EFFECT_FORBIDDENerror and the reasoning behind it.
Prerequisites
# 07_parallel_basics.md 완료 확인
euleragent graph validate examples/graphs/parallel/my_first_parallel.yaml
mkdir -p examples/graphs/parallel
1. 3-Branch Research Pattern
Topology
plan ──┬──→ web_search ──┬──→ merge → analyze → finalize
├──→ local_search ──┤
└──→ database_search─┘
This pattern collects information concurrently from three independent data sources.
web_search: Internet search (execute mode — requires HITL ifweb.searchis specified as force_tool)local_search: Local document search (execute mode)database_search: Database query (execute mode)
Step 1: Independent State Key Design
This is a design where each branch writes to separate state keys. In this case, last_write
is used only by a single branch, so it is safe.
state_schema:
web_results:
type: list
merge: last_write # web_search 브랜치만 씀 → 안전
local_results:
type: list
merge: last_write # local_search 브랜치만 씀 → 안전
db_results:
type: list
merge: last_write # database_search 브랜치만 씀 → 안전
source_count:
type: integer
merge: sum_int # 3개 브랜치 모두 씀 → sum_int로 합산
Pros: Results from each source can be accessed independently Cons: state_schema keys proliferate, and the join node must read multiple keys
Step 2: Shared Key Design (append_list)
This is a design where each branch writes to the same state key.
state_schema:
all_results:
type: list
merge: append_list # 3개 브랜치 모두 씀 → append_list로 결정론적 합산
source_count:
type: integer
merge: sum_int # 3개 브랜치의 결과 수 합산
Pros: Simpler schema; the join node reads only a single key Cons: Difficult to trace which source a result came from
Step 3: Writing the Complete 3-Branch YAML
# examples/graphs/parallel/three_branch_research.yaml
id: graph.three_branch_research
version: 1
category: research
description: 3개 소스 병렬 리서치 — web + local + database → merge → analyze
state_schema:
# 독립 키 전략: 각 소스의 결과를 별도로 보존
web_results:
type: list
merge: last_write # web_search만 씀
local_results:
type: list
merge: last_write # local_search만 씀
db_results:
type: list
merge: last_write # database_search만 씀
# 공유 합산 키: 전체 결과 수
source_count:
type: integer
merge: sum_int # 3개 브랜치 모두 씀
# 조인 노드가 씀
final_analysis:
type: string
merge: last_write # merge 노드만 씀
defaults:
max_iterations: 3
max_total_tool_calls: 50
max_web_search_calls: 10
parallel_groups:
- id: research_group
branches: [web_search, local_search, database_search]
join: merge_findings
nodes:
- id: plan
kind: llm
runner:
mode: execute
- id: web_search
kind: llm
runner:
mode: execute
exclude_tools: [shell.exec, file.write] # 병렬 브랜치 안전 설정
writes_state: [web_results, source_count]
- id: local_search
kind: llm
runner:
mode: execute
exclude_tools: [shell.exec, file.write]
writes_state: [local_results, source_count]
- id: database_search
kind: llm
runner:
mode: execute
exclude_tools: [shell.exec, file.write]
writes_state: [db_results, source_count]
- id: merge_findings
kind: llm
runner:
mode: execute
reads_state: [web_results, local_results, db_results, source_count]
writes_state: [final_analysis]
artifacts:
primary: analysis_report.md
- id: evaluate
kind: judge
judge:
schema: evaluator_v1
route_values: [finalize, revise]
- id: revise
kind: llm
runner:
mode: execute
artifacts:
primary: analysis_report.md
edges:
# plan → 팬아웃
- from: plan
to: web_search
when: "true"
- from: plan
to: local_search
when: "true"
- from: plan
to: database_search
when: "true"
# 팬인 → merge_findings
- from: web_search
to: merge_findings
when: "true"
- from: local_search
to: merge_findings
when: "true"
- from: database_search
to: merge_findings
when: "true"
# 하류 순차
- from: merge_findings
to: evaluate
when: "true"
- from: evaluate
to: finalize
when: "judge.route == finalize"
- from: evaluate
to: revise
when: "judge.route == revise"
- from: revise
to: evaluate
when: "true"
finalize:
artifact: analysis_report.md
euleragent graph validate examples/graphs/parallel/three_branch_research.yaml
Expected output:
단계 3/3: Graph 추가 검증...
[✓] state_schema 존재 (parallel_groups 사용 시 필수)
[✓] state_schema 타입+merge 호환성:
web_results: list + last_write ✓
local_results: list + last_write ✓
db_results: list + last_write ✓
source_count: integer + sum_int ✓
final_analysis: string + last_write ✓
[✓] parallel_groups 검증:
research_group:
branches: [web_search, local_search, database_search] (3개)
join: merge_findings ✓
[✓] 각 브랜치 writes_state:
web_search: [web_results, source_count] ✓
local_search: [local_results, source_count] ✓
database_search: [db_results, source_count] ✓
[✓] source_count (sum_int): 3개 브랜치가 씀 → 결정론적 ✓
[✓] 팬아웃 엣지: plan → web_search, local_search, database_search ✓
[✓] 팬인 엣지: 모든 브랜치 → merge_findings ✓
완료
결과: 유효 ✓ (오류 없음, 경고 없음)
2. Shared Key append_list Strategy Comparison
Here we redesign the same graph using the append_list shared strategy.
# examples/graphs/parallel/three_branch_shared.yaml
id: graph.three_branch_shared
version: 1
category: research
description: 3개 브랜치 — 공유 append_list 키 전략
state_schema:
all_findings:
type: list
merge: append_list # 3개 브랜치 모두 씀 → append_list (결정론적)
source_count:
type: integer
merge: sum_int # 3개 브랜치 모두 씀 → sum_int (결정론적)
defaults:
max_iterations: 2
max_total_tool_calls: 40
parallel_groups:
- id: research_group
branches: [web_search, local_search, database_search]
join: merge_findings
nodes:
- id: plan
kind: llm
runner:
mode: execute
- id: web_search
kind: llm
runner:
mode: execute
writes_state: [all_findings, source_count]
- id: local_search
kind: llm
runner:
mode: execute
writes_state: [all_findings, source_count]
- id: database_search
kind: llm
runner:
mode: execute
writes_state: [all_findings, source_count]
- id: merge_findings
kind: llm
runner:
mode: execute
reads_state: [all_findings, source_count]
artifacts:
primary: report.md
edges:
- from: plan
to: web_search
when: "true"
- from: plan
to: local_search
when: "true"
- from: plan
to: database_search
when: "true"
- from: web_search
to: merge_findings
when: "true"
- from: local_search
to: merge_findings
when: "true"
- from: database_search
to: merge_findings
when: "true"
- from: merge_findings
to: finalize
when: "true"
finalize:
artifact: report.md
euleragent graph validate examples/graphs/parallel/three_branch_shared.yaml
# 결과: 유효 ✓ (all_findings: append_list → 경고 없음)
3. PARALLEL_BRANCH_LIMIT_EXCEEDED Demonstration (9 Branches)
Let's see what happens when the maximum branch count (8) is exceeded.
# examples/graphs/parallel/too_many_branches.yaml (에러 학습용)
id: graph.too_many_branches
version: 1
category: demo
state_schema:
results:
type: list
merge: append_list
parallel_groups:
- id: giant_group
branches:
- branch_1
- branch_2
- branch_3
- branch_4
- branch_5
- branch_6
- branch_7
- branch_8
- branch_9 # ← 9개! 최대 8개 초과
join: join_node
nodes:
- id: branch_1
kind: llm
runner: {mode: execute}
writes_state: [results]
- id: branch_2
kind: llm
runner: {mode: execute}
writes_state: [results]
- id: branch_3
kind: llm
runner: {mode: execute}
writes_state: [results]
- id: branch_4
kind: llm
runner: {mode: execute}
writes_state: [results]
- id: branch_5
kind: llm
runner: {mode: execute}
writes_state: [results]
- id: branch_6
kind: llm
runner: {mode: execute}
writes_state: [results]
- id: branch_7
kind: llm
runner: {mode: execute}
writes_state: [results]
- id: branch_8
kind: llm
runner: {mode: execute}
writes_state: [results]
- id: branch_9
kind: llm
runner: {mode: execute}
writes_state: [results]
- id: join_node
kind: llm
runner: {mode: execute}
reads_state: [results]
edges:
- {from: branch_1, to: join_node, when: "true"}
- {from: branch_2, to: join_node, when: "true"}
- {from: branch_3, to: join_node, when: "true"}
- {from: branch_4, to: join_node, when: "true"}
- {from: branch_5, to: join_node, when: "true"}
- {from: branch_6, to: join_node, when: "true"}
- {from: branch_7, to: join_node, when: "true"}
- {from: branch_8, to: join_node, when: "true"}
- {from: branch_9, to: join_node, when: "true"}
- {from: join_node, to: finalize, when: "true"}
finalize:
artifact: output.md
euleragent graph validate examples/graphs/parallel/too_many_branches.yaml
Expected output:
오류: PARALLEL_BRANCH_LIMIT_EXCEEDED
parallel_groups 'giant_group'에 9개의 브랜치가 있습니다.
최대 허용 브랜치 수는 8개입니다.
현재 브랜치: [branch_1, branch_2, ..., branch_9] (9개)
허용 최대: 8개
해결 방법:
1. 브랜치 수를 8개 이하로 줄이세요.
2. 여러 그룹으로 분리하고 중간 집계 노드를 추가하세요:
grp_a: [branch_1..4] → merge_a
grp_b: [branch_5..8] → merge_b
(branch_9는 sequential하게 merge_b 다음에)
4. Nested parallel_groups Design Principles
The euleragent Graph currently does not support nested parallel_groups. Only flat structures are allowed.
# 지원하지 않는 중첩 구조 (개념적)
parallel_groups:
- id: outer_group
branches: [inner_group_1, inner_group_2] # ← inner 그룹이 브랜치?
join: outer_join
- id: inner_group_1 # ← 이것이 outer의 브랜치? 미지원
branches: [a, b]
join: inner_join_1
오류: PARALLEL_NESTED_GROUP_FORBIDDEN
parallel_groups는 중첩을 지원하지 않습니다.
'inner_group_1'이 'outer_group'의 브랜치로 참조됩니다.
Alternative Designs for Nesting
When nesting is needed, you can resolve it by splitting into multiple sequential groups.
필요한 구조 (중첩):
[A1, A2] → join_A ─┬─→ B1 ─→ final
└─→ B2 ─┘
대안 구조 1 (순차 병렬 그룹):
[A1, A2] → join_A (1번째 그룹)
→ [B1, B2] → final (2번째 그룹)
대안 구조 2 (병렬 확장):
[A1, A2, B1, B2] → join (전부 한 번에)
join이 A 결과와 B 결과를 모두 처리
# 대안 구조 1: 두 개의 순차 parallel_groups
parallel_groups:
- id: group_a
branches: [a1, a2]
join: join_a
- id: group_b
branches: [b1, b2]
join: join_b
# 실행 순서: [a1, a2] 동시 → join_a → [b1, b2] 동시 → join_b
edges:
- {from: a1, to: join_a, when: "true"}
- {from: a2, to: join_a, when: "true"}
- {from: join_a, to: b1, when: "true"} # join_a가 b 그룹의 팬아웃
- {from: join_a, to: b2, when: "true"}
- {from: b1, to: join_b, when: "true"}
- {from: b2, to: join_b, when: "true"}
5. PARALLEL_SIDE_EFFECT_FORBIDDEN Demonstration
This error occurs when shell.exec or file.write is used as a force_tool in a parallel branch.
# 위험한 예: 병렬 브랜치에서 file.write force_tool 사용
nodes:
- id: branch_a
kind: llm
runner:
mode: plan
force_tool: file.write # ← 병렬 브랜치에서 file.write 금지!
writes_state: [results]
# 이 YAML을 검증하면:
euleragent graph validate examples/graphs/parallel/side_effect_test.yaml
Expected output:
오류: PARALLEL_SIDE_EFFECT_FORBIDDEN
병렬 브랜치 'branch_a'에서 force_tool: file.write가 사용되었습니다.
병렬 브랜치에서 다음 도구는 금지됩니다:
- file.write: 동일 파일 동시 쓰기로 데이터 손상 위험
- shell.exec: 병렬 셸 실행으로 파일 시스템 충돌 위험
해결 방법:
1. force_tool을 제거하고 브랜치는 상태(state)만 사용하세요.
2. 파일 쓰기가 필요하면 조인 노드 이후 순차 노드에서 수행하세요.
Correct design:
# 병렬 브랜치: state에만 씀
- id: branch_a
kind: llm
runner:
mode: execute # force_tool 없음
writes_state: [results]
# 조인 이후 순차 노드: 파일 쓰기 가능
- id: finalize_writer
kind: llm
runner:
mode: plan
force_tool: file.write # 여기서는 OK (병렬 아님)
Expected Output Summary
| Command | Expected Result |
|---|---|
graph validate three_branch_research.yaml |
Valid, 3 branches, source_count sum_int confirmed |
graph validate three_branch_shared.yaml |
Valid, append_list shared strategy confirmed |
graph validate too_many_branches.yaml |
PARALLEL_BRANCH_LIMIT_EXCEEDED (9 branches) |
graph validate side_effect_test.yaml |
PARALLEL_SIDE_EFFECT_FORBIDDEN (file.write) |
Independent Key vs Shared Key Strategy Comparison
| Aspect | Independent Key Strategy | Shared Key Strategy |
|---|---|---|
| state_schema complexity | High (many keys) | Low (few keys) |
| Source traceability | Possible (separate key per source) | Not possible (results are mixed) |
| Merge strategy risk | last_write (single branch) | append_list (always safe) |
| Join node complexity | High (reads multiple keys) | Low (reads fewer keys) |
| Recommended scenario | Differentiated processing per source needed | Simple aggregation |
Key Concepts Summary
| Concept | Description |
|---|---|
| Independent key strategy | Each branch writes to a separate key -- last_write is used by a single branch |
| Shared key strategy | Multiple branches write to the same key -- append_list/sum_int is required |
| source_count | A pattern that sums each branch's result count using sum_int |
| PARALLEL_BRANCH_LIMIT_EXCEEDED | Raised when there are 9 or more branches |
| Nested parallel_groups | Not supported -- only flat structures are allowed |
| PARALLEL_SIDE_EFFECT_FORBIDDEN | Forbids file.write/shell.exec as force_tool in parallel branches |
| exclude_tools | A safety setting that explicitly excludes specific tools from branches |
Common Errors
Error 1: Join node not in parallel_groups and missing writes_state
# join_node는 parallel_groups.join이므로 branche가 아님
# 그러나 writes_state는 선언 가능 (조인 노드도 상태를 씀)
- id: merge_findings
kind: llm
reads_state: [all_findings]
writes_state: [final_analysis] # OK: 조인 노드의 쓰기는 허용
The join node is not a branch, so there is no error when writes_state is not declared. However,
explicit declaration is recommended for documentation purposes.
Error 2: Two parallel_groups sharing the same join node
parallel_groups:
- id: group_a
branches: [a1, a2]
join: shared_join # ← 두 그룹이 같은 join을?
- id: group_b
branches: [b1, b2]
join: shared_join # ← 동일한 join 노드
오류: PARALLEL_JOIN_MULTIPLE
'shared_join' 노드가 2개의 parallel_groups에서 join으로 참조됩니다:
group_a, group_b
각 parallel_groups는 고유한 join 노드를 가져야 합니다.
Hands-On Exercise
Exercise: Market Research 4-Branch Parallel Graph
Create a 4-branch market research graph with the following structure.
plan → [trend_research | competitor_research | customer_research | tech_research]
→ synthesize → evaluate ⇄ revise → finalize
state_schema requirements:
- all_insights: list + append_list (shared by all 4 branches)
- insight_count: integer + sum_int (number of insights found by each branch)
- trend_summary: string + last_write (written only by trend_research)
- synthesis_report: string + last_write (written by the synthesize node)
euleragent graph validate examples/graphs/parallel/market_research_4branch.yaml
euleragent graph compile examples/graphs/parallel/market_research_4branch.yaml \
--out examples/graphs/parallel/market_research_4branch_compiled.json
Previous: 07_parallel_basics.md | Next: 09_parallel_with_quality.md