Pattern 05. Web Research Integration — force_tool and HITL Gating
Learning Objectives
After completing this tutorial, you will be able to:
- Design a safe web search node using the combination of
force_tool: web.searchandmode: plan - Understand the HITL approval flow and correctly wire
when: "approvals_resolved"edges - Limit per-node web search calls with
guardrails.tool_call_budget - Configure
defaults.max_web_search_callsanddedupe_web_search - Prevent and resolve
BUDGET_INCONSISTENT,HITL_GATING_VIOLATION, andFORCE_TOOL_NO_BUDGETerrors
Prerequisites
04_judge_and_loop.mdcompletedblog_with_judge.yamlexists- euleragent agent created
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:
- Costs can increase unexpectedly (paid search APIs)
- Personal or internal information may be included in queries
- Malicious or biased search queries could be executed
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
9. Verifying dedupe_web_search Behavior
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.
Scope of dedupe_web_search
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.
- Next tutorial: 06_human_gate.md -- Design nodes that require mandatory human review using
force_tool: file.write - Multi-route: 07_multi_route.md -- Branch into 3 or more routes from a Judge