Home > EulerAgent > Tutorials > Pattern > Pattern 05. Web Research Integration — force_tool and ...

Pattern 05. Web Research Integration — force_tool and HITL Gating

Learning Objectives

After completing this tutorial, you will be able to:

Prerequisites

euleragent agent list
euleragent pattern validate blog_with_judge.yaml

1. Why Is HITL Necessary?

Web search is a tool that connects to external networks. If an agent runs dozens of search queries on its own:

euleragent follows a deny-all default policy. Web searches are not executed without explicit permission.

The combination of force_tool: web.search + mode: plan implements this safeguard.


2. Core Concept: force_tool + mode: plan

What mode: plan Means

mode: execute (기본)
  LLM → 도구 직접 실행 → 결과

mode: plan (HITL 모드)
  LLM → 도구 실행 "제안" → ⏸ PAUSE
  사람이 제안을 검토 → ✓ 승인 또는 ✗ 거절
  승인된 도구만 실행 → 결과

What force_tool: web.search Means

force_tool forces the node to use this specific tool. When combined with mode: plan, it means "any proposal to use this tool must receive HITL approval."

runner:
  mode: plan              # 계획만, 실행은 HITL 승인 후
  force_tool: web.search  # web.search를 반드시 제안해야 함
  min_proposals: 3        # 최소 3개의 검색 쿼리를 제안해야 통과

HITL_GATING_VIOLATION

If force_tool is set but mode: execute is used, a HITL_GATING_VIOLATION error is raised because this is an unsafe configuration.

# 잘못된 예 — HITL_GATING_VIOLATION
runner:
  mode: execute           # execute + force_tool = 위반!
  force_tool: web.search  # HITL 없이 바로 실행?? 안 됨

# 올바른 예
runner:
  mode: plan              # 반드시 plan
  force_tool: web.search

3. Budget Configuration: guardrails and defaults

Per-Node Budget (guardrails.tool_call_budget)

nodes:
  - id: research
    kind: llm
    runner:
      mode: plan
      force_tool: web.search
    guardrails:
      tool_call_budget:
        web.search: 5    # 이 노드에서 web.search는 최대 5회

Global Budget (defaults)

defaults:
  max_web_search_calls: 10    # 전체 실행에서 web.search 최대 10회
  max_total_tool_calls: 30    # 전체 실행에서 모든 도구 합산 최대 30회
  dedupe_web_search: true     # 동일한 쿼리 중복 실행 방지

BUDGET_INCONSISTENT Error

This error occurs when a per-node budget exceeds the global budget.

# 잘못된 예 — BUDGET_INCONSISTENT
defaults:
  max_web_search_calls: 5     # 전역 최대 5회

nodes:
  - id: research
    guardrails:
      tool_call_budget:
        web.search: 8         # 노드 예산이 전역 예산(5)보다 큼! 에러

# 올바른 예
defaults:
  max_web_search_calls: 10    # 전역 10회

nodes:
  - id: research
    guardrails:
      tool_call_budget:
        web.search: 5         # 노드 5회 ≤ 전역 10회 ✓

FORCE_TOOL_NO_BUDGET

This occurs when force_tool is set but the budget for that tool is 0 or not configured:

# 잘못된 예 — FORCE_TOOL_NO_BUDGET
runner:
  force_tool: web.search

guardrails:
  tool_call_budget:
    web.search: 0    # 예산 0인데 force? 에러

4. The approvals_resolved Edge

A web search node enters a paused state. To proceed to the next node after approval is complete, use the when: "approvals_resolved" condition.

edges:
  - from: research        # 웹 검색 노드 (pause 가능)
    to: write             # 다음 노드
    when: "approvals_resolved"   # 모든 승인이 완료됐을 때

# 잘못된 예
edges:
  - from: research
    to: write
    when: "true"    # 웹 검색 노드에서 true를 쓰면 pause 없이 바로 넘어감!

5. Pattern Design

We add real web search to the blog writing pattern.

[research] ──HITL PAUSE──► 승인 후 실행 → [draft] → [evaluate] → finalize
(web.search,                                              │
 mode: plan)                                              └── revise → [evaluate]

Full flow:

┌─────────────────────────────────────────────────────────────────┐
│ blog.web_research 패턴 흐름도                                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  [research]  ◄─── HITL PAUSE                                    │
│     │ 웹 검색 제안 (llm/plan, force_tool=web.search)              │
│     │ min_proposals: 3  budget: web.search ≤ 5                  │
│     │ when: approvals_resolved                                  │
│     ▼                                                           │
│  [draft]                                                        │
│     │ 수집된 정보로 초안 작성 (llm/execute)                        │
│     │ when: true                                                │
│     ▼                                                           │
│  [evaluate] ──── when: judge.route == finalize ─────────────────┐
│     │ 품질 평가 (judge/evaluator_v1)                             │
│     │ when: judge.route == revise                               │
│     ▼                                                           │
│  [revise]                                                       │
│     │ 초안 개선 (llm/execute)                                    │
│     └────────────────────────► [evaluate] (최대 3회)             │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ [FINALIZE]  blog_post.md 저장                            │◄──┘
│  └──────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

6. Writing the YAML

Create the blog_web_research.yaml file.

id: blog.web_research
version: 1
category: writing
description: "웹 검색 + Judge 루프가 결합된 고품질 블로그 작성 패턴"

defaults:
  max_iterations: 3
  max_total_tool_calls: 30

  # 전체 실행에서 웹 검색 최대 횟수
  # 노드별 예산의 합보다 크거나 같아야 함
  max_web_search_calls: 10

  # 중복 쿼리 방지 — 동일한 쿼리가 다시 제안되면 자동으로 제외
  dedupe_web_search: true

  pass_threshold: 0.85

nodes:
  # ── 노드 1: research (웹 검색, HITL) ──
  - id: research
    kind: llm
    runner:
      # mode: plan은 필수! execute이면 HITL_GATING_VIOLATION
      mode: plan
      force_tool: web.search    # 반드시 web.search를 제안해야 함
      min_proposals: 3          # 최소 3개의 검색 쿼리 제안 필수
    prompt:
      system_append: |
        당신은 기술 리서처입니다.
        주어진 주제에 대해 최신 정보를 찾기 위한 웹 검색 쿼리를 제안하세요.

        제안 요구사항:
        - 최소 3개, 최대 5개의 검색 쿼리
        - 각 쿼리는 서로 다른 측면을 다뤄야 함
        - 영어 쿼리를 사용 (검색 결과 품질 향상)
        - 너무 광범위하거나 너무 좁지 않은 쿼리

        예시 쿼리 형식:
        - "[기술명] tutorial best practices [현재연도]"
        - "[기술명] vs [대안기술] comparison"
        - "[기술명] real world use case example"

    # 노드별 웹 검색 예산
    # max_web_search_calls(10) 이하여야 함 — BUDGET_INCONSISTENT 방지
    guardrails:
      tool_call_budget:
        web.search: 5    # 이 노드에서 최대 5회

    artifacts:
      primary: research_results.md

  # ── 노드 2: draft ──
  - id: draft
    kind: llm
    runner:
      mode: execute
      # 초안 작성 시 추가 검색 금지 — 수집된 정보만 사용
      exclude_tools: [web.search, web.fetch, shell.exec]
    prompt:
      system_append: |
        당신은 기술 블로그 작가입니다.
        웹 검색으로 수집된 최신 정보를 바탕으로 블로그 포스트를 작성하세요.

        요구사항:
        - 길이: 1000-1500 단어
        - 최신 정보를 활용 (검색 결과에서 날짜, 버전 등 인용)
        - 독자: 현업 개발자
        - 실용적 예시와 코드 포함
        - 마크다운 포맷
    artifacts:
      primary: blog_post.md

  # ── 노드 3: evaluate (Judge) ──
  - id: evaluate
    kind: judge
    judge:
      schema: evaluator_v1
      route_values: [finalize, revise]
    prompt:
      system_append: |
        블로그 포스트의 품질을 다음 기준으로 평가하세요:

        1. 최신성 (25%): 검색으로 수집한 최신 정보가 잘 반영됐는가?
        2. 기술 정확성 (30%): 기술 정보가 정확한가?
        3. 실용성 (25%): 독자가 바로 적용할 수 있는가?
        4. 가독성 (20%): 구성, 흐름, 명확성

        score >= 0.85 → finalize
        score < 0.85 → revise

  # ── 노드 4: revise ──
  - id: revise
    kind: llm
    runner:
      mode: execute
      exclude_tools: [web.search, web.fetch, shell.exec]
    prompt:
      system_append: |
        편집장의 피드백을 반영하여 블로그 포스트를 개선하세요.
        기존 검색 결과를 최대한 활용하되, 누락된 정보가 있으면 명시하세요.
    artifacts:
      primary: blog_post.md

edges:
  # research 완료 후 승인이 완료됐을 때만 draft로 이동
  - from: research
    to: draft
    when: "approvals_resolved"    # 반드시 approvals_resolved 사용!

  - from: draft
    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: blog_post.md

7. Validation

euleragent pattern validate blog_web_research.yaml

Expected output:

Validating pattern: blog_web_research.yaml

  Stage 1 (Schema)      PASS
  Stage 2 (Structural)  PASS  force_tool + mode:plan combination valid ✓
  Stage 3 (IR Analysis) PASS
    HITL gates: research (web.search, min_proposals=3) ✓
    Budget check: research.web.search(5) ≤ max_web_search_calls(10) ✓
    dedupe_web_search: enabled ✓
    Cycle bounded: max_iterations=3 ✓

Validation complete: 0 errors, 0 warnings

8. Execution and Approval Flow

Execution

cp blog_web_research.yaml .euleragent/patterns/

euleragent pattern run blog.web_research my-agent \
  --task "Kubernetes Operator 패턴 완전 가이드 — 언제, 왜, 어떻게 사용하는가" \
  --project default

Expected output (paused at research):

[run:h8f4c2b6] Starting pattern: blog.web_research

  ⏸ research     PAUSED — Waiting for HITL approval (web.search × 4)

Checking the Approval List

euleragent approve list --run-id h8f4c2b6

Output:

Pending Approvals for run: h8f4c2b6
─────────────────────────────────────────────────
Node: research (budget: web.search ≤ 5, proposed: 4)

  #1  web.search  "Kubernetes Operator pattern tutorial 2025"
  #2  web.search  "Kubernetes custom resource definition operator development"
  #3  web.search  "Kubernetes Operator vs Helm chart when to use"
  #4  web.search  "Kubernetes Operator real world examples production"

4 tool calls pending. Budget remaining: 1/5.

Options:
  Accept all:  euleragent approve accept-all --run-id h8f4c2b6 --execute
  Accept some: euleragent approve accept --run-id h8f4c2b6 --item 1 --item 2 --item 3 --execute
  Reject all:  euleragent approve reject-all --run-id h8f4c2b6

Rejecting Individual Queries

# #3번 쿼리는 거절 (범위가 너무 넓음)
euleragent approve reject --run-id h8f4c2b6 --item 3 \
  --reason "Operator vs Helm 비교는 나중에 별도로"

# 나머지 승인 후 실행
euleragent approve accept --run-id h8f4c2b6 --item 1 --item 2 --item 4 --execute

Resuming

euleragent pattern resume h8f4c2b6 --execute

Expected output:

[run:h8f4c2b6] Resuming from: research

  web.search "Kubernetes Operator pattern tutorial 2025"        OK (15 results)
  web.search "Kubernetes custom resource definition..."          OK (11 results)
  web.search "Kubernetes Operator real world examples..."        OK (9 results)
  (item #3 rejected — skipped)

  ✓ research     Completed — 3 searches, research_results.md generated
  ✓ draft        Completed — 1,247 words
  ✓ evaluate     Completed — score: 0.91 → route: finalize
  ✓ finalize     Completed

Artifact: .euleragent/runs/h8f4c2b6/artifacts/blog_post.md

Let's see how dedupe_web_search: true works in practice.

If the Judge decides on revise and after revision the evaluate pass succeeds, but the pattern were designed to loop back to research, duplicate queries would be automatically filtered. You can verify this in the event stream:

cat .euleragent/runs/h8f4c2b6/pattern_events.jsonl | grep "dedupe"
{"ts":"2026-02-23T14:33:12Z","event":"tool.dedupe","tool":"web.search","query":"Kubernetes Operator pattern tutorial 2025","reason":"already_executed_in_run","skipped":true}

10. Intentionally Triggering a BUDGET_INCONSISTENT Error

As a hands-on exercise, let's deliberately cause an error.

In blog_web_research.yaml, set the research node's web.search budget higher than the global budget.

defaults:
  max_web_search_calls: 5    # 전역 5회로 낮춤

nodes:
  - id: research
    guardrails:
      tool_call_budget:
        web.search: 8        # 노드 예산(8) > 전역 예산(5) → 에러

Validation:

euleragent pattern validate blog_web_research_broken.yaml

Expected output:

Validating pattern: blog_web_research_broken.yaml

  Stage 1 (Schema)      PASS
  Stage 2 (Structural)  PASS
  Stage 3 (IR Analysis) FAIL

  ERROR [BUDGET_INCONSISTENT]
    Node 'research': tool_call_budget.web.search (8) exceeds
    defaults.max_web_search_calls (5).
    The node budget cannot exceed the global cap.

    Fix options:
    1. Reduce node budget: guardrails.tool_call_budget.web.search ≤ 5
    2. Increase global cap: defaults.max_web_search_calls ≥ 8

Validation complete: 1 error, 0 warnings

Fix: Either reduce the node budget (to 5 or less) or increase the global budget (to 8 or more).


11. Key Concepts Explained

The Role of min_proposals

min_proposals: 3 means the LLM must propose at least 3 search queries. If the LLM proposes only 1, the runtime requests more proposals. This prevents overly narrow research.

Plan Nodes Without force_tool

A node with mode: plan but no force_tool is also valid. In this case, the LLM autonomously selects and proposes tools. HITL approval still occurs.

Multiple force_tool Values

Specifying multiple force_tool values on a single node is not supported. To force multiple tools, you must split them across separate nodes.

dedupe_web_search operates only within a single run. Queries from other runs are not detected as duplicates.


12. Exercise: Intentionally Trigger and Resolve a BUDGET_INCONSISTENT Error

Try the following scenarios.

Scenario 1: Aggregated Budgets Across Multiple Research Nodes

defaults:
  max_web_search_calls: 8    # 전역 8회

nodes:
  - id: broad_research
    guardrails:
      tool_call_budget:
        web.search: 5

  - id: narrow_research
    guardrails:
      tool_call_budget:
        web.search: 5        # 두 노드 합산(10) > 전역(8)
                             # → BUDGET_INCONSISTENT?

Run validation and check what error actually appears. (Hint: consider the relationship between individual node budgets vs. the global budget.)

Scenario 2: FORCE_TOOL_NO_BUDGET

nodes:
  - id: research
    runner:
      mode: plan
      force_tool: web.search
    # guardrails 섹션 없음
    # → FORCE_TOOL_NO_BUDGET?

Check whether a default budget is automatically applied or an error is raised.

euleragent pattern validate scenario2.yaml --format json

Next Steps

You have learned how to safely integrate web search. Now let's build more powerful human intervention -- gates where a person directly reviews and edits content.

← Prev Back to List Next →