# Adaptive Agent

An agent that runs a multi-step plan, watches for live signals that contradict what it assumed, and safely changes its mind — built for the **[The Adaptive Agent](https://build.doo.ooo/challenges/adaptive-agent)** challenge: *can an AI system recognize that reality changed and safely change its mind?*

**Live demo:** https://AliJalil.github.io/adaptive-agent/ · **90-second version:** open the demo, leave the first scenario selected, hit **▶ Play**, and watch the two panels diverge — the adaptive agent reschedules the hotel/shuttle mid-run and lands on ✅; the baseline panel next to it finishes every step too, and lands on ❌ with the exact same plan.

No fixed-interval re-prompting anywhere. Re-planning fires only when an incoming event contradicts an assumption a *specific pending step* is relying on — everything else is logged and ignored.

## What this is

Three scenarios, each run twice in parallel from identical starting conditions and an identical scripted stream of world events — one run **adaptive** (it observes and revises), one **baseline** (it executes the original plan blindly, exactly what most agent demos do today). Both panels show every step, the live event/execution log, and a final **outcome check** that verifies the *real-world* result, not just "did every step run." That's what exposes the baseline's failure: it completes 100% of its steps and still gets the outcome wrong.

- **Scenario 1 — flight delay.** A 5-hour delay arrives mid-run. Adaptive reschedules the hotel check-in and shuttle pickup to the new arrival time. Baseline ships a shuttle to an empty gate.
- **Scenario 2 — phantom stock.** An inventory recount reveals Warehouse A never had the item. Adaptive reroutes pickup and the customer's ETA to Warehouse B. Baseline dispatches a courier to pick up nothing.
- **Scenario 3 — the failure test.** Two inventory recounts disagree within one tick of each other. See below.

## Architecture snapshot: plan → execute → observe → re-evaluate

```mermaid
flowchart LR
    P["<b>Plan</b><br/>buildPlan(world):<br/>ordered steps, each<br/>tagged with the world-facts<br/>it assumes"] --> E
    E["<b>Execute</b><br/>run the current<br/>pending step"] --> O
    O["<b>Observe</b><br/>apply the next scripted<br/>world event (streamed,<br/>not polled) to world state"] --> D{"contradicts a<br/>pending step's<br/>assumption?"}
    D -->|no| E
    D -->|yes, but cooldown active| S["⏸ Suppress —<br/>flag for human review<br/>(contain flapping)"]
    D -->|yes| R["<b>Re-evaluate</b><br/>scenario.replan():<br/>patch only the affected<br/>steps, log 'I changed my<br/>mind because…'"]
    R --> E
    S --> E
    E -->|plan complete| V["verifyOutcome():<br/>does the REAL result<br/>match reality?"]
```

`engine.js` owns the generic loop (plan/execute/observe/re-evaluate, cooldown containment) and knows nothing about flights or warehouses. `scenarios.js` owns all domain knowledge: what a step assumes, how to patch a broken plan, and what "actually worked" means for that domain. `runs.js` persists every run's full state to `localStorage` after each tick, so a page reload resumes exactly where playback stopped — a real property of the loop, not an in-memory-only demo.

## Why re-planning doesn't fire on every signal

A pending step declares its assumptions explicitly: `{ key: "flightDelayHours", expected: 0, description: "flight lands on time" }`. An incoming event only triggers `considerReplan` if it changes a key that some *still-pending* step actually depends on, and only if the new value actually contradicts what that step assumed. An event that doesn't touch any live assumption is logged as ignored, not acted on — this is what keeps "adaptation" from degenerating into "re-prompt on every event," which the brief explicitly disqualifies.

## The failure test: a signal that contradicts itself

Scenario 3 replays the warehouse scenario, but the inventory feed is noisy: recount #1 says Warehouse A is out of stock (the agent replans to Warehouse B), and recount #2, one tick later, says the opposite — recount #1 was a scanner error, A does have stock.

**What naive adaptation would do:** immediately reverse course back to Warehouse A. If a third recount arrived disagreeing again, it would reverse again. Every reversal is not free — it's a courier re-dispatched, a promise re-made to the customer. An agent that trusts whichever signal arrived most recently will flap on noisy input, and every flap has a real-world cost.

**Containment implemented:** each assumption key gets a short cooldown after it triggers a revision (`engine.js`, `COOLDOWN_TICKS`). A second contradicting signal on the same key inside that window doesn't trigger another reversal — it's logged as suppressed and the run is flagged `needsHumanReview`. The agent keeps its one considered decision (Warehouse B) instead of thrashing, and honestly reports that the situation is ambiguous rather than either flip-flopping or silently pretending recount #2 never happened. You can watch this exact sequence in the log: `replan@t1` immediately followed by `suppressed@t2`.

**Where this containment itself has a limit, honestly:** the cooldown is a fixed tick count, not a measure of signal confidence or source reliability. A truly urgent, high-confidence correction arriving inside the cooldown window would also be suppressed and merely flagged — the system trades a small amount of responsiveness for protection against flapping. That's a deliberate, disclosed trade-off, not an oversight; a production version would likely weight signals by source trust rather than using a blind timer for the cooldown itself.

## Bonus signals implemented

- **Side-by-side non-adaptive baseline** — every scenario runs both modes in parallel from identical inputs, not as a separate mode you have to imagine.
- **Streaming, event-driven inputs** — the event timeline is delivered tick by tick during playback (`▶ Play` / `⏭ Step`), not handed to the agent as one static snapshot to reason over once.

## Running it locally

```bash
git clone https://github.com/AliJalil/adaptive-agent.git
cd adaptive-agent
python3 -m http.server 8000   # or any static file server
# open http://localhost:8000
```

No build step, no dependencies, no `.env`.

## Two-year thesis: adaptive planning in production agents (≤300 words)

Most agent demos today conflate two things that should be separate: making a plan, and staying married to it. The interesting engineering problem for the next two years isn't planning better — models are already decent planners — it's building the OBSERVE step as a first-class citizen: a live, event-driven check of "does the world still match what I assumed," wired to specific assumptions on specific steps, not a periodic re-prompt that burns tokens and can hallucinate a new plan when nothing actually changed.

Production adaptive agents need three things this build makes concrete. First, assumptions as data, not prose buried in a prompt: a named fact, a key, a source, testable against a live signal, so "did reality change" is a comparison, not a judgment call a model re-derives from scratch each time. Second, re-planning that's scoped, not global: when one assumption breaks, only the steps depending on it should move — re-deriving the whole plan on every signal is how a harmless notification turns into an unnecessary, possibly worse plan. Third, and almost nobody builds this today: hysteresis. Without a cooldown or confirmation requirement on a just-revised assumption, an agent facing a noisy or adversarial signal source will flap — burning real-world cost (a re-dispatched courier, a rebooked flight) on every contradiction, including the ones that were themselves noise.

Agents that get deployed, not just demoed, will be judged less on how cleverly they adapt and more on how disciplined they are about *not* adapting — recognizing a signal too ambiguous to act on and escalating it, rather than confidently thrashing between two plans. Containment, not cleverness, is the unsolved part, and it's the part that doesn't show up in a highlight reel.

## Notes

**AI tools used:** Built with Claude (Anthropic), which wrote the engine, all three scenarios (including catching and fixing a real bug during development — the cooldown logic initially failed to recognize a signal reversing back to its original value as a fresh contradiction, since each step's assumption was compared against its original expected value rather than what the plan currently believes; fixed by realigning assumptions to the newly observed value on every revision, verified with a small Node script before touching the UI), the UI, and this document, in one session.

**Key decisions:** (1) Assumptions live on steps as structured data (`{key, expected, description}`), not embedded in prose, so "did this event matter" is a plain equality check, not something re-derived by a model. (2) The engine (`engine.js`) is fully domain-agnostic — it never mentions flights or warehouses — so the same loop, cooldown, and persistence logic could run a genuinely different scenario by swapping `scenarios.js` alone. (3) A side-by-side baseline panel instead of a toggle, because the disqualifying failure mode (completing every step while the real-world outcome silently breaks) is only convincing when you can watch it happen next to the adaptive run reacting to the exact same signal.

**Out of scope (by design):**
- **World events are scripted, not truly external.** Each scenario ships a fixed timeline (`eventsAtTick`); a production version would subscribe to a real event bus (webhooks, a message queue) rather than reading from an array, but the engine's contract — "here's an event, here's the current world" — doesn't change.
- **Cooldown is a fixed tick count, not confidence-weighted** — see the failure-test section above for the honest trade-off this implies.
- **No LLM anywhere.** Detecting a contradiction is an equality check, and choosing how to patch a broken plan (`replan()`) is scenario-authored logic, not a model call. This keeps every decision auditable, at the cost of the plans and repairs being hand-authored rather than generated for novel situations outside the three scenarios shipped here.
- **Single-browser persistence.** Run state lives in `localStorage`; a production system would persist to a durable store other services could observe mid-run.

## License

MIT
