Install Once, Drain Anywhere: A Worked DAL Challenge for Portable Travel Research

A distributed travel agent is easy to demo and surprisingly hard to install: the first machine knows where its files live, while the next operator knows only what the instructions say. The fix is to make installation produce a small, inspectable contract before research begins.

This article works through a concrete DAL challenge: build a travel-research worker that can be installed on a fresh operator machine, produce an itinerary draft, and stop safely when booking requires review.

The challenge: separate “can run” from “may book”

Imagine a travel agent with three responsibilities:

  1. 1. Gather candidate flights, lodging, and local transport.
  2. 2. Assemble those findings into an itinerary.
  3. 3. Book the selected options after the principal approves them.

A naive installer treats all three as one capability: configure credentials, launch the agent, and let it proceed. That is convenient but operationally unclear. A successful installation does not imply permission to spend money or make reservations.

The smaller, safer design gives installation its own acceptance test. After setup, the worker should be able to prove that it can read its configuration, write to its artifact directory, and execute a dry research pass. Booking remains a separate gate.

In DAL-shaped pseudocode, the boundary can be explicit:

```dal let install_check = { config_readable: true, artifact_root: "COO-FILESYSTEM/articles/drafts", research_mode: "dry_run", booking_enabled: false }

return install_check ```

The exact fields can differ in a real project. The important idea is that the result is data an operator can inspect, not merely a cheerful sentence from the installer.

Step one: make the operator install a contract

A portable worker should not assume that the current directory is its home. It should receive or resolve an explicit root and keep generated material underneath it. That makes the first successful run meaningful on a laptop, a server, or a test checkout.

A minimal configuration might look like this:

```dal let travel_config = { workspace: env::get("DAL_TRAVEL_WORKSPACE"), currency: "USD", home_airport: "JFK", booking_enabled: false, approval_required_for: ["flight", "lodging", "rail", "car"] }

if travel_config.workspace == "" { return { status: "blocked", reason: "DAL_TRAVEL_WORKSPACE is missing" } } ```

There are two useful properties here. First, the missing-input case is deliberate: the worker returns a blocker rather than silently writing into an unexpected directory. Second, the booking flag is visible in the same object that describes the installation. An operator can tell whether the machine is ready for research and whether it is authorized for external effects.

An install command should leave behind a small report, for example:

```markdown

# Travel worker install check

  • Status: ready_for_research
  • Workspace: /srv/travel-agent
  • Booking: disabled
  • Required next step: approve a specific itinerary before reservation

```

That report is not busywork. It is the handoff between setup and the first useful task.

Step two: research into artifacts, not ambient context

The research pass should write candidates to a durable file before asking the model to recommend anything. Search results, assumptions, and missing information otherwise disappear into a transcript that the next run cannot reliably inspect.

A compact candidate record could be assembled like this:

```dal let candidates = [ { kind: "flight", provider: "example-airline", departure: "2026-09-14T09:00:00-04:00", arrival: "2026-09-14T21:10:00+01:00", price: 742, currency: "USD", source_url: "https://example.invalid/flight/123", checked_at: now() } ]

fs::write( travel_config.workspace + "/research/flight-candidates.json", json::stringify(candidates) ) ```

A production connector would replace the example provider and URL. The structure still matters: each candidate carries its category, price, timestamp, and source. An itinerary writer can now explain what it selected and a reviewer can check whether the information is stale.

The same pattern works for lodging and ground transport. Keep raw-ish findings separate from the recommendation. If a price changes, update the candidate record and regenerate the recommendation; do not overwrite the evidence with the conclusion.

Step three: turn findings into a reviewable itinerary

The itinerary is where judgment enters. Preferences such as nonstop travel, a maximum nightly rate, walking distance, or flexible cancellation should be represented as constraints rather than hidden assumptions.

```dal let preferences = { nonstop: true, max_lodging_nightly: 220, cancellation: "flexible", arrival_before_local_time: "22:00" }

let itinerary = travel::recommend( candidates, preferences )

fs::write( travel_config.workspace + "/drafts/itinerary-2026-09-14.md", markdown::render(itinerary) ) ```

The resulting draft should include rejected alternatives and unresolved questions when they affect the decision. For example: “The cheapest flight arrives after the requested local cutoff,” or “The flexible room costs $38 more per night.” A recommendation is easier to approve when its trade-offs are visible.

This is also where installation pays off. Because every machine writes to the same configured workspace shape, an operator can find the draft without learning which process generated it. The agent is distributed; the review surface is stable.

Step four: make booking a narrow, explicit transition

Booking should consume an approved itinerary, not infer approval from the existence of a recommendation. One safe transition is to require a separate approval record containing the exact option identifiers and a freshness check.

```dal let approval = fs::read_json( travel_config.workspace + "/approvals/itinerary-2026-09-14.json" )

if approval.status != "approved" { return { status: "needs_review", artifact: "drafts/itinerary-2026-09-14.md" } }

if approval.itinerary_hash != hash(itinerary) { return { status: "blocked", reason: "approved itinerary changed" } }

if travel_config.booking_enabled != true { return { status: "blocked", reason: "booking is disabled on this installation" } }

return travel::book(approval.option_ids) ```

This code illustrates a useful operational distinction. The agent can finish research and produce a recommendation even when booking is unavailable. If approval exists but the installation is still research-only, the result is a clear blocker—not a partial, ambiguous attempt to reserve something.

The hash check is valuable because travel data moves quickly. A reviewer should approve the exact itinerary that the booking step will use, not a newer or silently modified version.

What the installer should prove

A practical acceptance checklist for a fresh machine is short:

  • The configured workspace exists and is writable.
  • Research connectors either pass a dry run or report a precise missing credential.
  • Candidate data is saved with timestamps and sources.
  • The itinerary draft is created in a predictable location.
  • Booking is disabled by default.
  • An approval record names the exact itinerary and options before any reservation call.
  • Completion reports distinguish ready, needs_review, blocked, and failed.

This checklist is more portable than a machine-specific setup tutorial because it tests outcomes. An operator can change the directory, provider, or deployment method while preserving the contract.

Conclusion: install the boundary before the agent

A travel agent becomes easier to distribute when installation does not promise more than it proves. First establish a workspace, validate research, and write inspectable artifacts. Then let a human approve a specific itinerary. Only afterward should a separately enabled booking capability create an external commitment.

The next step is to implement one dry-run installer and run it on a clean machine. Inspect the install report, open the candidate file, and confirm that a missing workspace or connector produces a blocker. If those checks are reliable, the worker is ready for broader research—not yet for unattended booking.

The portable unit is not just the agent process. It is the contract between installation, artifacts, review, and the next operator.