Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ links to the package's own README.
| [vibe-coding-mad-libs](challenges/vibe-coding-mad-libs/README.md) | Fill four blanks, and Pi builds the web app you described. |
| [birdsbirdbirds](challenges/birdsbirdbirds/README.md) | Draw a bird and watch it fly across the sky. |
| [glitch-invaders](challenges/glitch-invaders/README.md) | Shoot the bugs, then debug the three that fight back using Sentry evidence. |
| [launch-control](challenges/launch-control/README.md) | Upgrade a live AI agent's prompt, tools, and model with no redeploy. |

Challenges follow the contract in boothstrap.sh's challenge spec and
authoring guide; agents working here start from [AGENTS.md](AGENTS.md).
Expand Down
160 changes: 160 additions & 0 deletions challenges/launch-control/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Launch Control: Travel Desk

A 10-minute browser challenge built on LaunchDarkly AgentControl. The
participant chats with a travel booker, a small AI agent on a local page, and
asks it to plan the same trip at each of four rungs. Pi, the guide, upgrades
the booker's config one rung at a time, and the booker gets better with no
restart and no deploy. The lesson: you can change an AI agent's model, tools,
and instructions in production without redeploying.

| Rung | Variation | What it adds | Model |
| --- | --- | --- | --- |
| v1 | `bare` | A one-line prompt, no tools | `gemini-2.5-flash-lite` |
| v2 | `prompted` | A real travel-agent prompt | `gemini-2.5-flash-lite` |
| v3 | `tooled` | `search_flights`, `search_hotels`, `book_trip` | `gemini-2.5-flash-lite` |
| v4 | `upgraded` | A stronger model | `gemini-3.8-flash` |

The **ladder** is these four variations of one AgentControl config, in
[ladder.json](ladder.json); a **rung** is one of them. The **travel booker**
is the agent on the page; **the guide** is Pi in the terminal. Only the
booker is under LaunchDarkly's control.

## Files

| File | Purpose |
| --- | --- |
| `challenge.json`, `plugin.json` | Manifest and Agent Plugin metadata |
| `mcp.json`, `mcp/` | The `travel-desk` MCP server and its one tool, `switch_travel_agent` |
| `ladder.json` | The four variations: model, instructions, and tool schemas |
| `inventory.json` | The made-up cities, flights, and hotels the tools search |
| `launchdarkly.json` | Public settings for live mode: project, environment, and config keys |
| `provision.mjs` | Creates or updates the config in a LaunchDarkly project from `ladder.json` |
| `intro.md`, `icon.txt` | Intro screen text and tile icon |
| `skills/start-challenge/SKILL.md` | The guide Pi follows |
| `monitor/SKILL.md` | Headless browser monitor skill |
| `install` | Installs LaunchDarkly's SDKs into the package's `node_modules` |
| `setup`, `cleanup` | Start and stop the page server (`server/setup.mjs`, `server/cleanup.mjs`) |
| `server/` | Page server on `127.0.0.1:4374`, the booker's agent loop, and the demo stand-in |
| `page/` | The travel desk page: chat and Live config card |

## How it works

The page server resolves the AgentControl config for the context
`{ kind: "user", key: BOOTHSTRAP_SESSION_ID }` with `@launchdarkly/ai-server`
over a streaming SDK client, with analytics and diagnostic events off. It runs
the booker's agent loop on whatever the SDK returns: that model, through the
vault's `gemini` service, those instructions, and only those tools, against
the fake inventory. `book_trip` writes `itinerary.json` to the workspace and
nothing else. No real travel API, payment, or personal data is involved.

`switch_travel_agent({ rung })` changes only this session's individual target
on the config, with an `addTargets` and `removeTargets` semantic patch. It
accepts only the next rung, or `1` to roll back, and never touches the
default rule or another context key. The SDK sees the change in about a
second, and the page's config card updates in place.

`setup` removes any target this session left behind and confirms that the
session resolves to v1. `cleanup` removes the session's target; a session
with no target is left alone, so it is safe to run twice and after a crash.

## How steps advance

The page keeps a hidden line, `booker-state v1 rung=<n> tried=<rungs>
booked=<yes|no> mode=<live|demo>`, and the headless monitor reports
`meet-booker`, `add-prompt`, `add-tools`, and `add-model` from it, as
[monitor/SKILL.md](monitor/SKILL.md) describes. A rung counts as tried only
once the booker has answered a message sent while it was live, and `booked`
turns `yes` only when v4 books a trip, so a lucky booking on a lower rung
never completes `add-model`. Each rung starts the booker on a fresh model
conversation, so it answers the request on its own, while the page keeps the
whole chat. Once all four
are tried, Pi asks what changed and why nothing was redeployed, and reports
`debrief`. The page server also writes the same state to
`travel-desk-state.json` for a standalone run.

The server listens on port 4374. Set `LAUNCH_CONTROL_PORT` to use another
port, though the manifest and the monitor expect 4374.

## Demo and live modes

Live mode needs both LaunchDarkly keys and real settings. Otherwise the
challenge runs in demo mode: the identical flow against a local stand-in for
LaunchDarkly, mounted on the page server, that serves `ladder.json`. The page
then shows a small `demo` label. The booker calls Gemini in both modes.

The settings live in [launchdarkly.json](launchdarkly.json) as
`<placeholder>` values. `LAUNCHDARKLY_PROJECT_KEY`,
`LAUNCHDARKLY_ENVIRONMENT_KEY`, and `LAUNCHDARKLY_AI_CONFIG_KEY` in the
environment replace them, so an event can set them in its `challenge_env`.

## Event vault services

The event vault needs these three services. Each credential is the variable
the package reads; none starts with `LD_`.

| Service | Auth | Credential | Host | Read by |
| --- | --- | --- | --- | --- |
| `gemini` | `api-key`, header `x-goog-api-key` | `GEMINI_API_KEY` | `generativelanguage.googleapis.com` | The page server and Pi |
| `launchdarkly-sdk` | `api-key`, header `Authorization` | `LAUNCHDARKLY_SDK_API_KEY` | `stream.launchdarkly.com` | The page server only |
| `launchdarkly-api` | `api-key`, header `Authorization` | `LAUNCHDARKLY_API_AUTH_TOKEN` | `app.launchdarkly.com` | `setup`, `cleanup`, and the MCP tool |

- `launchdarkly-sdk` holds the environment's server-side SDK key (`sdk-…`).
With events off, the SDK needs only the streaming host.
- `launchdarkly-api` holds an access token that can write the config's
targeting. It is `api-key`, not `bearer`, because LaunchDarkly's REST API
takes the bare token in `Authorization` and refuses `Bearer api-…`.
- The page server never receives the API token: `setup` leaves it out of the
server's environment.

## Provision a LaunchDarkly project

Run this once per LaunchDarkly project, with an access token that can write AI
Configs, from the package directory:

```sh
LAUNCHDARKLY_API_AUTH_TOKEN=api-... \
LAUNCHDARKLY_PROJECT_KEY=<project> \
LAUNCHDARKLY_ENVIRONMENT_KEY=<environment> \
LAUNCHDARKLY_AI_CONFIG_KEY=launch-control-travel-booker \
node provision.mjs
```

It creates or updates the three tools, the agent-mode config, its four
variations, and a default rule serving `bare` in that environment. A second
run creates nothing twice. Then fill the same three keys into
`launchdarkly.json`.

## Run without boothstrap

Needs Node 26 and a `GEMINI_API_KEY`; for live mode, export the two
LaunchDarkly keys and settings too. Follow the standalone steps in
boothstrap.sh's `docs/authoring-challenges.md` to install Pi and
pi-mcp-adapter and load the package as an Agent Plugin, which mounts its
`travel-desk` MCP server. Then:

```sh
challenge=/absolute/path/to/launch-control
"$challenge"/install
mkdir /tmp/my-launch-control-run && cd /tmp/my-launch-control-run
"$challenge"/setup
```

Open `http://127.0.0.1:4374/`, then start Pi in the same folder:

```sh
pi --model google/gemini-3.8-flash \
--skill "$challenge"/skills/start-challenge/SKILL.md \
'/skill:start-challenge'
```

When done:

```sh
"$challenge"/cleanup
```

`cleanup` stops the server, removes the session's target in live mode, and
removes `.launch-control/`, `travel-desk-state.json`, and `itinerary.json`.
Without boothstrap there is no `report_step` or `session_info`, so no monitor
runs: the skill reads `travel-desk-state.json` and acknowledges finished steps
in plain words.
61 changes: 61 additions & 0 deletions challenges/launch-control/challenge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"id": "launch-control",
"title": "Launch Control: Travel Desk",
"tagline": "Upgrade a live AI agent's prompt, tools, and model with no redeploy.",
"icon": "icon.txt",
"type": "browser",
"agent": "pi",
"model": "google/gemini-3.8-flash",
"vault_services": [
"gemini",
"launchdarkly-sdk",
"launchdarkly-api"
],
"points": 150,
"estimated_minutes": 10,
"learning_objectives": [
"configuring an AI agent at runtime",
"changing a model, prompt, and tools with no deploy",
"rolling an agent back instantly"
],
"featured_tech": [
"LaunchDarkly"
],
"intro": "intro.md",
"steps": [
{
"id": "meet-booker",
"title": "Meet the travel booker",
"hint": "Tap the suggested trip, or write your own, and send it to the booker.",
"done_when": "The page's accessibility snapshot holds the line \"booker-state v1 …\" whose tried= list contains 1."
},
{
"id": "add-prompt",
"title": "Give it a real prompt",
"hint": "Let Pi switch the booker to v2, then send your trip again.",
"done_when": "The page's accessibility snapshot holds the line \"booker-state v1 …\" whose tried= list contains 2."
},
{
"id": "add-tools",
"title": "Give it tools",
"hint": "Let Pi switch the booker to v3, then send your trip again.",
"done_when": "The page's accessibility snapshot holds the line \"booker-state v1 …\" whose tried= list contains 3."
},
{
"id": "add-model",
"title": "Upgrade its model",
"hint": "Let Pi switch the booker to v4, then send your trip again until it's booked.",
"done_when": "The page's accessibility snapshot holds the line \"booker-state v1 …\" whose tried= list contains 4 and which contains \"booked=yes\"."
},
{
"id": "debrief",
"title": "Tell your guide what changed",
"hint": "Say what changed at each rung, and why nothing was redeployed.",
"done_when": "All four rungs are tried and the participant has told the guide, in their own words, what changed and why no deploy was needed."
}
],
"browser": {
"start_url": "http://127.0.0.1:4374/",
"monitor_skill": "monitor/SKILL.md"
}
}
5 changes: 5 additions & 0 deletions challenges/launch-control/cleanup
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
# Cleanup hook: stop the page server, remove this session's target from the
# booker's config, and remove this attempt's files. Safe to run twice.
set -eu
exec node "${BOOTHSTRAP_PACKAGE_DIR:-$(dirname "$0")}/server/cleanup.mjs"
4 changes: 4 additions & 0 deletions challenges/launch-control/icon.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
 ▲
▐█▌
▄▐█▌▄
 ▀▀▀
8 changes: 8 additions & 0 deletions challenges/launch-control/install
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/sh
# Installs LaunchDarkly's AI SDK and server SDK, which the page server uses to
# read the booker's config, into this package's node_modules.
set -eu

# pwd -P resolves symlinks: npm ci fails on a --prefix that isn't a real path.
package_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
npm ci --ignore-scripts --no-audit --no-fund --prefix "$package_dir"
18 changes: 18 additions & 0 deletions challenges/launch-control/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
You run the travel desk, and its booking agent needs work. Ask it to plan
a trip, then watch it get better, live, while it keeps running.

The booker reads its whole setup from LaunchDarkly AgentControl: which
model it runs, what its instructions say, and which tools it may use. Pi,
your guide on the right, changes that setup one rung at a time:

- v1: a small model and a one-line prompt
- v2: a real travel-agent prompt
- v3: tools to search flights and hotels, and to book
- v4: a stronger model

After each switch, send your trip again and compare. The Live config card
shows what LaunchDarkly serves the booker right now. Nothing restarts and
nothing is redeployed.

The travel desk is in Chrome on the left. Its flights and hotels are made
up, and nothing is ever charged.
33 changes: 33 additions & 0 deletions challenges/launch-control/inventory.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$comment": "The travel desk's made-up inventory: no real airline, hotel, or price. Dates are month-day only, so a search matches them in any year. One trip fits the suggested request (JFK to Lisbon, March 12 to 16, waterfront, under $900): NV310, NV311, and Alfama Riverside Guesthouse, $818 for four nights.",
"airports": {
"JFK": "New York",
"LIS": "Lisbon",
"OPO": "Porto",
"MAD": "Madrid"
},
"flights": [
{ "id": "NV308", "airline": "Novair", "origin": "JFK", "destination": "LIS", "date": "03-11", "departs": "07:10", "arrives": "18:55", "stops": 0, "price_usd": 179 },
{ "id": "AT112", "airline": "Atlantica", "origin": "JFK", "destination": "LIS", "date": "03-12", "departs": "08:05", "arrives": "19:50", "stops": 0, "price_usd": 305 },
{ "id": "SK240", "airline": "Skyline", "origin": "JFK", "destination": "LIS", "date": "03-12", "departs": "06:40", "arrives": "22:15", "stops": 1, "price_usd": 268 },
{ "id": "NV310", "airline": "Novair", "origin": "JFK", "destination": "LIS", "date": "03-12", "departs": "07:10", "arrives": "18:55", "stops": 0, "price_usd": 214 },
{ "id": "AT114", "airline": "Atlantica", "origin": "JFK", "destination": "LIS", "date": "03-13", "departs": "08:05", "arrives": "19:50", "stops": 0, "price_usd": 288 },
{ "id": "AT113", "airline": "Atlantica", "origin": "LIS", "destination": "JFK", "date": "03-16", "departs": "11:30", "arrives": "14:20", "stops": 0, "price_usd": 322 },
{ "id": "SK241", "airline": "Skyline", "origin": "LIS", "destination": "JFK", "date": "03-16", "departs": "09:15", "arrives": "17:40", "stops": 1, "price_usd": 281 },
{ "id": "NV311", "airline": "Novair", "origin": "LIS", "destination": "JFK", "date": "03-16", "departs": "13:05", "arrives": "15:50", "stops": 0, "price_usd": 236 },
{ "id": "NV313", "airline": "Novair", "origin": "LIS", "destination": "JFK", "date": "03-17", "departs": "13:05", "arrives": "15:50", "stops": 0, "price_usd": 165 },
{ "id": "AT220", "airline": "Atlantica", "origin": "JFK", "destination": "OPO", "date": "03-12", "departs": "09:00", "arrives": "20:30", "stops": 0, "price_usd": 331 },
{ "id": "AT221", "airline": "Atlantica", "origin": "OPO", "destination": "JFK", "date": "03-16", "departs": "12:00", "arrives": "15:10", "stops": 0, "price_usd": 344 },
{ "id": "SK330", "airline": "Skyline", "origin": "JFK", "destination": "MAD", "date": "03-12", "departs": "18:20", "arrives": "23:55", "stops": 1, "price_usd": 296 },
{ "id": "SK331", "airline": "Skyline", "origin": "MAD", "destination": "JFK", "date": "03-16", "departs": "10:45", "arrives": "16:30", "stops": 1, "price_usd": 301 }
],
"hotels": [
{ "id": "LIS-ALFAMA", "name": "Alfama Riverside Guesthouse", "city": "Lisbon", "neighborhood": "Alfama", "waterfront": true, "nightly_usd": 92 },
{ "id": "LIS-CAIS", "name": "Cais do Sodré Harbor Hotel", "city": "Lisbon", "neighborhood": "Cais do Sodré", "waterfront": true, "nightly_usd": 139 },
{ "id": "LIS-BELEM", "name": "Belém Marina Suites", "city": "Lisbon", "neighborhood": "Belém", "waterfront": true, "nightly_usd": 210 },
{ "id": "LIS-BAIRRO", "name": "Bairro Alto Budget Rooms", "city": "Lisbon", "neighborhood": "Bairro Alto", "waterfront": false, "nightly_usd": 58 },
{ "id": "LIS-AVENIDA", "name": "Avenida Business Inn", "city": "Lisbon", "neighborhood": "Avenidas Novas", "waterfront": false, "nightly_usd": 110 },
{ "id": "OPO-RIBEIRA", "name": "Ribeira Quay Hotel", "city": "Porto", "neighborhood": "Ribeira", "waterfront": true, "nightly_usd": 118 },
{ "id": "MAD-SOL", "name": "Puerta del Sol Rooms", "city": "Madrid", "neighborhood": "Sol", "waterfront": false, "nightly_usd": 97 }
]
}
Loading
Loading