The loop
Perception → Reasoning → Planning → Action → Feedback and learning. Solid arrows show the main flow of information. Dashed arrows show how outcomes flow back to improve perception, reasoning, or planning. Select a stage to see what goes in and what comes out.
Information flow, stage by stage
| Stage | Input | Output | Typical techniques |
|---|---|---|---|
| Perception | Messages, images, sensor data, tool results | Structured observation | Parsing, vision and speech models, validation |
| Reasoning | Observation, goal, memory, rules | Interpretation or decision | LLM reasoning, decision APIs, retrieval |
| Planning | Decision and constraints | Ordered steps | Task decomposition, dependency graphs, solvers |
| Action | Next step | Change in the environment | Tool calls, APIs, MCP servers, robot commands |
| Feedback and learning | Outcomes and goals | Updated memory, parameters, prompts, or plans | Evaluation, reflection, human feedback, fine-tuning (offline) |
Inside one task, feedback changes the next step (retry, replan, update memory). Across many tasks, collected feedback can improve the system itself: better prompts, tools, rules, or a fine-tuned model. The second kind should go through evaluation before deployment.
End-to-end example: a campus shuttle dispatcher
This program runs the whole loop for five time steps. Each stage is a small function so you can match the code to the diagram.
Inputs
- Riders waiting at three stops, for five time steps (simulated sensors)
- A target: nobody waits more than 8 minutes
Outputs
- A dispatch plan for each step
- Measured wait times
- The learned congestion threshold
Execution steps
perceive()cleans the readingsreason()finds congested stopsplan()orders and limits dispatchesact()simulates the outcomelearn()updates the threshold
Run it and watch the threshold drop from 20 to 12 after the agent misses its target twice. That is feedback changing future reasoning.
The rider numbers and wait-time formula are invented for teaching. In a real deployment, the reasoning step could be an LLM or a decision API, and the action step would call a real dispatch system.
"""End-to-end agentic workflow: a campus shuttle dispatcher
This example runs the full loop from the Agentic AI Workflows module:
Perception -> Reasoning -> Planning -> Action -> Feedback and learning
^ |
+------------------ updated memory ---------------+
Inputs: simulated stop sensors (people waiting) for 5 time steps
Outputs: a dispatch decision per step, plus a learned congestion threshold
Feedback: measured wait times; when riders wait too long, the agent lowers
its threshold so it dispatches extra shuttles earlier next time.
Illustrative: the "world" is a small deterministic simulation, and reasoning
uses simple rules so every step is easy to trace.
"""
from dataclasses import dataclass, field
# ---- Environment (simulated) ---------------------------------------------
WAITING_BY_STEP = [
{"Dorms": 12, "Library": 5, "North gate": 3},
{"Dorms": 18, "Library": 9, "North gate": 4},
{"Dorms": 25, "Library": 14, "North gate": 6},
{"Dorms": 22, "Library": 17, "North gate": 9},
{"Dorms": 15, "Library": 19, "North gate": 11},
]
def simulate_wait(waiting: int, extra_shuttle: bool) -> float:
"""Minutes riders wait: more people means longer waits; an extra shuttle helps."""
return round(waiting * (0.35 if extra_shuttle else 0.6), 1)
# ---- Agent memory ---------------------------------------------------------
@dataclass
class Memory:
threshold: int = 20 # dispatch an extra shuttle above this many riders
max_wait_target: float = 8.0 # minutes
history: list = field(default_factory=list)
# ---- The five stages ------------------------------------------------------
def perceive(raw: dict) -> dict:
"""Clean the sensor feed and compute a simple summary."""
clean = {stop: max(0, int(n)) for stop, n in raw.items()}
return {"waiting": clean, "busiest": max(clean, key=clean.get)}
def reason(obs: dict, mem: Memory) -> list[str]:
"""Decide which stops are congested given the current threshold."""
return [s for s, n in obs["waiting"].items() if n > mem.threshold]
def plan(congested: list[str], obs: dict) -> list[dict]:
"""Build an ordered action plan: busiest congested stops first, max 2 extra shuttles."""
ordered = sorted(congested, key=lambda s: -obs["waiting"][s])[:2]
return [{"action": "dispatch_extra", "stop": s} for s in ordered]
def act(actions: list[dict], obs: dict) -> dict:
"""Execute the plan in the (simulated) world and measure the outcome."""
served = {a["stop"] for a in actions}
return {s: simulate_wait(n, s in served) for s, n in obs["waiting"].items()}
def learn(waits: dict, mem: Memory) -> str:
"""Feedback: adjust the threshold when the target is missed or easily met."""
worst = max(waits.values())
if worst > mem.max_wait_target and mem.threshold > 8:
mem.threshold -= 4
return f"worst wait {worst} min > target, lower threshold to {mem.threshold}"
if worst < mem.max_wait_target / 2 and mem.threshold < 30:
mem.threshold += 2
return f"worst wait {worst} min is low, raise threshold to {mem.threshold}"
return f"worst wait {worst} min, keep threshold at {mem.threshold}"
# ---- Run the loop ---------------------------------------------------------
mem = Memory()
for t, raw in enumerate(WAITING_BY_STEP, 1):
obs = perceive(raw)
threshold_used = mem.threshold
congested = reason(obs, mem)
actions = plan(congested, obs)
waits = act(actions, obs)
note = learn(waits, mem)
mem.history.append({"t": t, "actions": actions, "waits": waits})
print(f"t={t} | perceive {obs['waiting']}")
print(f" reason congested (> {threshold_used} riders): {congested or 'none'}")
print(f" plan {[a['stop'] for a in actions] or 'no extra shuttles'}")
print(f" act waits {waits}")
print(f" learn {note}\n")
print("Learned threshold after 5 steps:", mem.threshold)
Try these changes
- Set
max_wait_targetto 6.0. How many cycles does it take to adapt? - Allow three extra shuttles in
plan(). What happens to the worst wait? - Add a sensor reading of
-5. Doesperceive()handle it?