Home > EulerAgent > Tutorials > Graph > Graph 08. Advanced Parallel Patterns — 3+ Branches, Complex...

Graph 08. Advanced Parallel Patterns — 3+ Branches, Complex State

Learning Objectives

After completing this tutorial, you will be able to:


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.

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

← Prev Back to List Next →