RunMode¶
This is the core value proposition of the framework: a single agent can operate deterministically and autonomously, switching between modes at runtime. When a workflow step fails, the agent automatically falls back to LLM reasoning to fix the problem, then seamlessly resumes the workflow.
In this tutorial, we'll build a form automation agent — it normally fills forms step-by-step (Workflow mode), but when it encounters unexpected CAPTCHAs or popups, it switches to Agent mode to handle them, then resumes the workflow.
Initialize¶
First, let's set up the LLM and define the tools our form automation agent will use.
import os
model_name = os.environ.get("MODEL_NAME")
api_key = os.environ.get("API_KEY")
api_base = os.environ.get("BASE_URL")
from bridgic.llms.openai import OpenAILlm, OpenAIConfiguration
llm = OpenAILlm(
api_key=api_key,
api_base=api_base,
timeout=30,
configuration=OpenAIConfiguration(
model=model_name,
temperature=0.0,
max_tokens=16384,
),
)
from bridgic.core.agentic.tool_specs import FunctionToolSpec
attempt_counter = {"captcha_attempts": 0}
async def fill_field(field_name: str, value: str) -> str:
"""Fill a form field with the given value"""
return f"Filled '{field_name}' with '{value}'"
async def click_button(button_name: str) -> str:
"""Click a button on the page"""
if button_name == "submit" and attempt_counter["captcha_attempts"] == 0:
attempt_counter["captcha_attempts"] += 1
raise RuntimeError("CAPTCHA detected! Cannot submit directly.")
return f"Clicked '{button_name}' successfully"
async def solve_captcha() -> str:
"""Solve a CAPTCHA challenge on the page"""
return "CAPTCHA solved successfully"
async def check_page_status() -> str:
"""Check the current page status"""
if attempt_counter["captcha_attempts"] > 0:
return "Page status: CAPTCHA challenge displayed, form fields are filled"
return "Page status: Form ready for input"
fill_field_tool = FunctionToolSpec.from_raw(fill_field)
click_button_tool = FunctionToolSpec.from_raw(click_button)
solve_captcha_tool = FunctionToolSpec.from_raw(solve_captcha)
check_page_status_tool = FunctionToolSpec.from_raw(check_page_status)
We have four tools:
fill_field— fills a form field with a given value.click_button— clicks a button. On the first submit attempt, it simulates a CAPTCHA challenge by raising aRuntimeError.solve_captcha— solves the CAPTCHA challenge.check_page_status— reports the current state of the page.
The attempt_counter dictionary simulates a real-world scenario: the first time we try to submit the form, a CAPTCHA appears. After solving it, subsequent submissions succeed.
Part 1: The Four Run Modes¶
Bridgic Amphibious provides four RunMode options that control how your agent executes:
| Mode | Behavior |
|---|---|
RunMode.AGENT | Pure agent mode — only on_agent() runs. The LLM decides every step. |
RunMode.WORKFLOW | Pure workflow mode — only on_workflow() runs. If any step errors, the entire workflow fails. |
RunMode.AMPHIFLOW | Workflow first, automatic agent fallback on failure. The best of both worlds. |
RunMode.AUTO (default) | Auto-detects based on which methods the subclass overrides: only on_agent() → AGENT; only on_workflow() → WORKFLOW; both → AMPHIFLOW. |
Let's define the agent that we'll run in different modes.
from bridgic.amphibious import (
AmphibiousAutoma, CognitiveContext, CognitiveWorker,
think_unit, ActionCall, RunMode
)
class FormFiller(AmphibiousAutoma[CognitiveContext]):
fixer = think_unit(
CognitiveWorker.inline(
"Something went wrong during the form submission. "
"Check the page status, identify the problem, and fix it."
),
max_attempts=5,
)
async def on_agent(self, ctx: CognitiveContext):
await self.fixer
async def on_workflow(self, ctx: CognitiveContext):
yield ActionCall("fill_field", field_name="username", value="john_doe")
yield ActionCall("fill_field", field_name="password", value="s3cur3p@ss")
yield ActionCall("fill_field", field_name="email", value="john@example.com")
yield ActionCall("click_button", button_name="submit")
Our FormFiller agent has both orchestration methods:
on_workflow()defines a deterministic, step-by-step form-filling process: fill three fields, then click submit.on_agent()activates aCognitiveWorkerthat diagnoses and fixes problems using LLM reasoning.
The RunMode we choose determines which method (or both) gets used.
Pure Workflow Mode — Fails on Error¶
Let's first run in RunMode.WORKFLOW to see what happens when the CAPTCHA appears.
# Reset state
attempt_counter["captcha_attempts"] = 0
try:
agent = FormFiller(llm=llm, verbose=True)
result = await agent.arun(
goal="Fill out the registration form",
tools=[fill_field_tool, click_button_tool, solve_captcha_tool, check_page_status_tool],
mode=RunMode.WORKFLOW,
)
print(result)
except Exception as e:
print(f"Workflow failed: {e}")
[15:24:38.197] [Router] (_amphibious_automa.py:1538) Ferrying to WORKFLOW mode [15:24:38.198] [Observe] (_amphibious_automa.py:1144) workflow: None [15:24:38.198] [Think] (_amphibious_automa.py:1145) workflow: [15:24:38.199] [Act] (_amphibious_automa.py:1172) workflow: { "content": "", "result": { "results": [ { "tool_id": "call_0", "tool_name": "fill_field", "tool_arguments": { "field_name": "username", "value": "john_doe" }, "tool_result": "Filled 'username' with 'john_doe'", "success": true, "error": null } ] }, "metadata": {}, "status": null } [15:24:38.199] [Observe] (_amphibious_automa.py:1144) workflow: None [15:24:38.199] [Think] (_amphibious_automa.py:1145) workflow: [15:24:38.200] [Act] (_amphibious_automa.py:1172) workflow: { "content": "", "result": { "results": [ { "tool_id": "call_0", "tool_name": "fill_field", "tool_arguments": { "field_name": "password", "value": "s3cur3p@ss" }, "tool_result": "Filled 'password' with 's3cur3p@ss'", "success": true, "error": null } ] }, "metadata": {}, "status": null } [15:24:38.200] [Observe] (_amphibious_automa.py:1144) workflow: None [15:24:38.201] [Think] (_amphibious_automa.py:1145) workflow: [15:24:38.202] [Act] (_amphibious_automa.py:1172) workflow: { "content": "", "result": { "results": [ { "tool_id": "call_0", "tool_name": "fill_field", "tool_arguments": { "field_name": "email", "value": "john@example.com" }, "tool_result": "Filled 'email' with 'john@example.com'", "success": true, "error": null } ] }, "metadata": {}, "status": null } [15:24:38.202] [Observe] (_amphibious_automa.py:1144) workflow: None [15:24:38.202] [Think] (_amphibious_automa.py:1145) workflow: Workflow failed: Tool execution failed for: — click_button: CAPTCHA detected! Cannot submit directly.
In pure Workflow mode, when click_button("submit") hits the CAPTCHA error, the entire workflow fails. There is no recovery mechanism — the agent simply raises the exception and stops. The three fields were filled successfully, but the submission could not be completed.
This is the fundamental limitation of a purely deterministic workflow: it can only handle the happy path.
Part 2: Amphiflow Mode — Automatic Fallback & Recovery¶
Now let's see the magic of Amphiflow mode. Here is how the automatic fallback mechanism works:
- Workflow steps execute normally — each
yield ActionCall(...)runs in order. - When a step fails — the framework catches the error and activates
on_agent(). - The LLM analyzes the problem — it uses tools to diagnose and fix the issue.
- After fixing — the workflow resumes from the next step.
This is the "amphibious" design: the efficiency of deterministic workflows for the happy path, combined with the adaptability of LLM reasoning for unexpected situations.
# Reset state
attempt_counter["captcha_attempts"] = 0
agent = FormFiller(llm=llm, verbose=True)
result = await agent.arun(
goal="Fill out the registration form",
tools=[fill_field_tool, click_button_tool, solve_captcha_tool, check_page_status_tool],
mode=RunMode.AMPHIFLOW,
max_consecutive_fallbacks=2,
)
print(result)
Now when click_button("submit") fails, the framework automatically activates on_agent(). The LLM discovers the CAPTCHA, calls solve_captcha(), then re-attempts the submit. The workflow continues seamlessly.
The key parameter controlling the fallback behavior is max_consecutive_fallbacks=2 — a safety valve. If 2 consecutive steps fail and the agent cannot fix them, the framework abandons the workflow entirely and runs in pure Agent mode. This prevents infinite fallback loops.
Part 3: Comparing All Three Modes¶
Here is a summary of how the three modes compare:
| Mode | CAPTCHA Handling | Efficiency | Predictability |
|---|---|---|---|
| WORKFLOW | Fails immediately | High (no LLM overhead) | Highest (but fragile) |
| AGENT | LLM figures it out | Lower (LLM reasons every step) | Lower (may vary) |
| AMPHIFLOW | Fixed steps + LLM rescue | Best of both | High with flexibility |
- WORKFLOW is the fastest and most predictable, but it cannot handle anything unexpected. It is ideal for stable, well-understood processes with no surprises.
- AGENT can handle anything the LLM can reason about, but it is slower and less predictable because the LLM decides every single step.
- AMPHIFLOW gives you the best of both worlds: deterministic efficiency on the happy path, with automatic LLM-powered recovery when things go wrong.
What have we learnt?¶
In this tutorial, we saw the amphibious design in action:
- RunMode controls how the agent runs:
AGENT(LLM only),WORKFLOW(deterministic only),AMPHIFLOW(workflow + agent fallback), orAUTO(auto-detect from overridden methods). - Amphiflow mode is the framework's core innovation: workflow steps execute deterministically, but when a step fails, the framework automatically activates
on_agent()to let the LLM diagnose and fix the problem. max_consecutive_fallbackscontrols how many consecutive failures trigger a full switch to agent mode.- The amphibious design gives you the efficiency of workflows for the happy path and the adaptability of agents for unexpected situations.
Next, we'll explore Context and Exposure — how to control what information the LLM sees when making decisions.