Day 1 · the practical session

What an agent is

Day 1 — the practical session

Matias Zaldarriaga

Ondas Gravitacionales e Investigación Asistida por IA

Use ← and →, or the dots at the top. p switches between reading size and presenting size.

Before I say anything

Open your terminal.

Start an agent and ask it to reproduce one of the two papers.

I am deliberately not telling you what to type.

The setup

Where to work, and things one could type

1. Get the papers.

cd <the course repo>
ls day1/papers/

  PhysRev.136.B1224.pdf   Peters 1964
  1608.01940v4.pdf        The basic physics of GW150914

2. Pick one.

Peters 1964 — analytic. Orbital decay of a binary from the quadrupole formula. Four pages, from 1964, and your agent may or may not have read it.

1608.01940 — observational. GW150914's masses from the strain data, with a ruler.

3. Make a working directory.

mkdir -p ~/gw-day1
cd ~/gw-day1
cp <repo>/day1/papers/*.pdf .

4. Start an agent, there.

claude
# or
codex

Work in an empty directory you own. The agent will read and write files in it. Never start one in your home directory or in a repository you care about.

Things one could type

Examples, not instructions. Type your own — the differences between what everyone asks for are the experiment.

Terse. “Reproduce this paper.”

Perfectly legitimate. Watch what it decides “reproduce” means.

Specific. “Read PhysRev.136.B1224.pdf. Derive equations (5.6) and (5.7) yourself and check them with sympy. Then reproduce Figure 1 exactly.”

Names the artefact, names the check — sympy is symbolic algebra in python: exact expressions, so “check” means an identity, not a float.

With a contract. “…and leave me: a script I can run, a figure, and a list of every claim you verified and how. Say explicitly which results you derived and which you recalled.”

The last sentence is the one that pays.

Also worth trying

  • Don't give it the PDF at all. Just name the paper. (What happens?)
  • Ask it to do the calculation before reading the paper, then compare.
  • Ask it what it is unsure about.

Set the dials before you start

Whatever you type, write down which model and which reasoning effort you used. Next stage. We need it for the comparison.

The two dials

The model, and how hard it thinks

Claude Code
claude                      # interactive
claude -p "…"               # headless, one shot

/model      →  Opus · Sonnet · Haiku
/effort     →  low … high … max

claude --dangerously-skip-permissions
Codex CLI
codex                       # interactive
codex exec "…"              # headless, one shot

/model      →  model + reasoning effort

codex --full-auto
codex --dangerously-bypass-approvals-and-sandbox

Model. A bigger model is more expensive per token and usually better at holding a long argument together. It is not uniformly better.

Reasoning effort. How much thinking it does before acting. Costs time and tokens. For a derivation this matters more than the model.

These flags change every few months. claude --help, codex --help. Do not memorise them; memorise that the two dials exist, because everything else in this session depends on them.

The week, in one table

What a scientist actually does

The organising idea for the whole week. Not “can AI do science” — but which of these specific jobs can it hold, and what does it need in order to hold them?

The jobWhat it needs from a toolWhere it shows up this week
Read the literaturethe source, not a summary of ittoday — the paper is in the repo
Do a derivationsymbolic algebra, and a way to be checkedtoday (sympy) · day 4 (Hellings–Downs)
Explore — try things, most of them wrongcheap iteration, no ceremonyevery workshop
Write codeyour conventions, your file layoutday 2
Run code you did not writedocs, source, the ability to read bothday 2 (gwpy, lal)
Run something heavya cluster, patience, a babysitterday 3
Make a figuredata provenance and a way to regenerate ittoday (marimo)
Keep track of what you learnednotes that survive the session endingtoday (the wiki)
Write the paperLaTeX, and everything above still reproducibleday 5
Give the talkslides that are built, not maintainedday 5

Every row is a place where the tool either fits your workflow or fights it. Most of the practical content of this course is about the fit.

While that runs

Three things worth understanding before you read the output

Your agent will take a while. Leave it. If it asks you something, answer briefly and let it continue.

the loop

Chat, agent, harness. A chatbot is text in, text out. An agent is the same model in a loop with tools. The harness is everything around it — and the part you configure.

context

It is one flat block of text, and it is finite. When it fills, it gets summarised, and you do not choose which detail is lost.

It is also what you pay for. The next stage is what a turn costs.

cold start

Close the terminal and it is gone. There is exactly one fix: write it down, in the project, in a form the agent will read next time.

All three in full, with the commands, are on using-the-tools.html — one page for the whole week, which fills in as the week goes. Read them while your run works.

Still while that runs

What a turn costs

A turn is one step of the loop: the agent acts, sees what came back, and decides what to do next. Every turn sends the whole conversation again, from the beginning. Five consequences, and all five are practical.

  1. A long session pays for its own history on every turn. Nothing is dropped as you go. What has accumulated by turn one hundred is sent whole, and then sent whole again at turn one hundred and one.
  2. A long output early is paid for on every turn after it. A file read in full at the start, a screenful of traceback, the listing of a directory nobody wanted — each of them is in the input of every turn that follows, for as long as the session lasts.
  3. Changing subject without starting fresh means paying for the subject you abandoned. The old thread does not stop being sent because you stopped caring about it. /clear costs you the setup again and nothing else.
  4. Caching makes the re-send much cheaper. The part of the conversation that has not changed since the last turn is read back at a fraction of the price of new text. It is not free, and it does not make anything smaller: a cached turn is a cheaper turn, not a shorter one, and it fills the same context.
  5. Something wrong in there is re-sent, and re-read, until you clear it. A file you have since corrected on disk is still in front of the agent in the form it was read; so is a conclusion it drew an hour ago and you rejected. That is the argument for bounding a task, and it is a cost argument as well as a quality one.

Two runs from later in the week, as each one recorded itself. Each is one task, run once, with nobody watching.

                                           day 2                 day 3
model                              claude-opus-5         claude-opus-5
effort                                     xhigh                 xhigh
turns                                        132                   242
tokens in                             19,475,555            58,906,227
tokens out                               169,476               235,493
of the input, a cache read            19,213,028            58,560,011

in, per token out                            115                   250
cached share of the input                  98.7%                 99.4%

Both of those are one task run once, later in this week — the grid on day 2 and the run left alone on day 3. Read the last two lines. Roughly a hundred times more went in than came out of the first, and about two hundred and fifty times on the second; and in both, near enough all of the input was a cache read rather than something the model had not seen before. That is what a conversation of a few hundred turns looks like from the invoice's side.

Token counts rather than prices, deliberately: a price is a fact about a price list on the day, and these two runs were measured in August 2026. The ratios are the durable part. Your own harness will print both if you ask it — /context in Claude Code, /status in Codex.

Somebody else's run

Seeing gravitational waves, in four prompts

Mark Cheung, down the corridor at the Institute, wanted to see what a gravitational wave does at every viewing angle at once. He sent this over. We are going to sit in it.

What it is. One HTML file, 6.9 MB, no install and no network: the four prompts he typed, and beside each one the thing it produced. Five interactive visualisers of numerical-relativity waveforms, all embedded inside it.

What to watch for. The second prompt does not ask for a visualiser. It asks for eight candidate ways to draw the thing, side by side, so that he can choose. The third prompt is one sentence: “I like option 4.” Taste, in the loop, twice.

The two dials, from his work. All four prompts ran on the same model at the same reasoning effort — Opus, medium, unchanged between prompts — and his README says so. That is what the dials being recorded looks like.

day1/show/gw_tutorial.html, and his four prompts verbatim beside it in prompts.txt. Both are yours to keep — he wrote them to be reused for teaching. [src:borrowed — the file, the four prompts, the 6.9 MB and the model-and-effort line are Mark Ho-Yeuk Cheung's (IAS), handed over 2026-08-10 and reused with his permission; credit and licence in day1/show/CREDIT.md]

it opens on its own

It is not embedded here. It is self-contained, it is 6.9 MB, and it has its own navigation — open it in a second tab and come back to this one.

What I got

The algebra came back right. The figures made from data came back wrong.

I ran both papers before you did. One instruction, given twice: once to a four-page analytic paper, once to a paper whose first three figures are made from the strain itself.

Everything that could be checked against an equation was checked and passed. All three figures that touched data are wrong, each in a different way, and nothing in either repository noticed.

Two runs, and the lines they finished with

runmodeleffortformat asked fortokens in / outwallwhenreceipt
Peters 1964——tex → pdf, marimo → html, a script of assertions——2026-07-24run.log
11/11 reported results reproduced
GW150914, the basic physics——tex → pdf, marimo → html, a script of assertions, and three figures from strain——2026-07-24run.log
23/23 reported results reproduced
Fig 1 strain, Fig 2 spectrogram, Fig 3 chirp-mass ridge fit -> 36 Msun (paper 30-40)

Not recorded for these runs: tokens, wall time, model, reasoning effort. They predate the convention; the fields are here so a later run fills them.

That is the usual report: a date, a list of files, and a line saying it all worked. Both lines are true. Neither of them is about the three figures further down, because no assertion in either repository touches those figures at all.

what the record is, and what it is not

Both runs happened on 2026-07-24. The instructions on the next stages are the ones recorded in this repository — the standing rule the runs were given, and the wording this session has quoted since it was written. They are not a keystroke log, and no tokens, wall time, model or effort setting were recorded at the time.

That is a defect, not a style: from day 2 the prompts are kept verbatim as they are typed and the run records what it consumed. The empty columns above are what a run looks like when nobody wrote that down.

What was asked

Peters 1964 — section by section

Not “reproduce this paper”. The instruction names the unit of work (a section), what to produce for each one, and how each claim is to be checked.

Instruction 1 — as recorded

  

Four pages of analysis, from 1964, and no data anywhere in it. This is the easy case, and it is worth naming as the easy case before the hard one.

What came back

An argument you read, figures you can vary, assertions that fail loudly

reproduction.pdf, first page — the whole of it is further down this stage
first page of the Peters reproduction
what the directory holds
reproduction.tex → .pdfthe argument, section by section
reproduce_peters.pythe assertions, and where every number is stamped
results.jsonevery number the reproduction computed, as it came out
results_provenance.jsonwhere each of those came from — the script, the function, the line, and whether that section touched data or only algebra
provenance.jsonthe same facts as a registry, which is what the .tex resolves its tags against
figures.py → figures.htmla marimo notebook: the two figures, with the parameters left free
peters.pythe reference implementation the rest imports
run.logthe receipt

Those files are ours and are not in the copy you were given — the point is what your own run leaves, in whatever shape it leaves it.

What a stamped number looks like

The middle three files are the ones worth opening. A number is written into results.json at the moment it is computed, and the same assignment writes a second record saying which function computed it and on which line. Neither file is edited by hand and neither can be: they are both written by the run. Here is one number in all four places it appears.

One number, in the four places that hold it: the line that computed it, what it came out at, the record of where that came from, and the sentence in the write-up that cites it.

reproduce_peters.py:259
    OUT["ht_lifetime_myr"] = Tht

results.json
    "ht_lifetime_myr": 300.5982272803736

results_provenance.json
    "ht_lifetime_myr": {
      "evidence": "numeric",
      "file": "reproduce_peters.py",
      "func": "section_V_numbers",
      "line": 259,
      "section": 5,
      "section_title": "Peters' own worked lifetimes (§V, p. B1231-B1232)"
    }

reproduction.tex:204
    lifetime $301$ Myr\src{ht_lifetime_myr} (literature $\sim300$).

The first line is the physics — how long the Hulse–Taylor binary has left before it merges. Everything under it is bookkeeping, and the bookkeeping is what lets somebody who was not there check the first line. The write-up cites the key rather than the digits, so a rerun that moved the number would move the sentence with it, or refuse to build.

The whole thing, to scroll

Scroll it. It is not a paper and it is not trying to be one — it is what the run left so that somebody who was not there could check it. Notice how much of it is claims and evidence and how little is exposition, and notice the small grey key beside each number.

reproduction.pdf — opens when you reach this stage
Peters Fig. 1, reproduced
Peters figure 1, reproduced
Peters Fig. 2, reproduced
Peters figure 2, reproduced

What was actually checked

  • The orbit-averaged energy and angular-momentum fluxes of §III–IV, set up as spectral sums and evaluated numerically, agree with Peters' closed forms to a relative 4e-12 [src:flux_dEdt_max_relerr] and 9e-16 [src:flux_dLdt_max_relerr].
  • §V is derived in sympy: the difference between the derived ⟨da/dt⟩, ⟨de/dt⟩ and da/de and the paper's printed expressions simplifies to zero [src:dadt_5_6_residual_is_zero] [src:dedt_5_7_residual_is_zero] [src:dade_5_8_residual_is_zero]. A machine check, not a re-reading by the mind that made the error.
  • Peters' own worked lifetimes come back: Hulse–Taylor at 300 Myr [src:ht_lifetime_myr], a Sun-like pair at 3e12 yr [src:solar_lifetime_yr], a white-dwarf pair at 3e4 yr [src:wd_lifetime_yr].
one check I liked

Peters works a Sun-like binary with a 4.5-day period, gets a lifetime of about 3×1012 years, and describes the separation as about ten solar radii. Both statements are his own text, in §V. [src:borrowed — the 4.5-day period and the "about ten solar radii" are Peters 1964 §V, p. B1228]

The lifetime comes back at 3e12 yr [src:solar_lifetime_yr]. The separation does not: Kepler at two solar masses and 4.5 days gives 14 R☉ [src:solar_a_from_4p5d_Rsun], and ten solar radii would be a period of 2.6 d [src:solar_period_at_10Rsun_d]. The lifetime is right; the separation in the text is rounded.

Unimportant to the physics, and completely characteristic. It is the difference between a reproduction and a paraphrase, and neither I nor an agent would have noticed it without running the number.

What was asked

GW150914 — the same instruction, on a paper with data in it

The one change: this paper's first three figures are made from the strain, not from the algebra. So the run had to fetch data and use a package neither of us had used before.

Instruction 2 — as recorded

  

It fetched 32 s of H1 and L1 strain around the event from GWOSC at 4096 Hz and cached it to a local array, so the analysis needs no network, and it used gwpy in a separate environment — the day-1 environment deliberately has no gravitational-wave stack in it.

What came back, first half

The half with no data in it

reproduction.pdf
first page of the GW150914 reproduction
Fig. 4 — the binary, to scale
the binary drawn to scale
Fig. 5 — compactness against mass ratio
compactness against mass ratio

The chain the paper builds, rebuilt and checked at every link: the chirp mass from f and its derivative; a floor on the total mass of 69 M☉ [src:equal_mass_total_msun] because the symmetric mass ratio cannot exceed 1/4; a Kepler separation at 150 Hz of 345 km [src:eq9_separation_km]; a compactness of 1.7 [src:compactness_ratio_equal_mass] Schwarzschild radii, which nothing but a pair of black holes fits; a peak luminosity of 3.4e49 W [src:peak_luminosity_w], or 9e-4 of c&sup5;/G [src:peak_luminosity_over_planck].

Relax the equal-mass assumption and the argument still closes: the lighter body is above 5.1 M☉ [src:eq15_m2_min_msun] under the permissive bound, and the compactness stays near unity [src:q13_compactness_ratio].

the check I care about most

That peak luminosity equals −dE/dt from Peters' circular da/dt, computed from the other paper and agreeing to zero relative error [src:peak_luminosity_vs_peters_relerr]. Two papers, fifty years apart, are the same formula read from opposite ends. An independent route agreeing is worth more than any number of internal assertions.

What came back, second half

The three figures made from strain

Fig. 1 — the band-passed strain
our version of the paper's figure 1
Fig. 2 — the spectrogram
our version of the paper's figure 2
Fig. 3 — the frequency track and its fit
our version of the paper's figure 3

Every one of them is wrong, in a different way. The next four stages are the four failures, one at a time, each beside the paper's own figure.

Two of the three were caught by someone who has seen enough of these plots to know what one looks like. The third was caught by nobody, and the fourth failure is about why.

Failure 1

The missing step, and a sign

The paper's Fig. 1
the paper's figure 1
What we produced
what we produced
The same code, two lines changed
the same code with two lines changed

Cause 1 — the pass band is not flat.

The code did what the paper's text says: band-pass 35–350 Hz, notch, no whitening. But inside that band the noise still varies by a factor of a few hundred, peaking at 37 Hz — so a plain band-pass leaves the low edge of the band dominant. That is the big slow wobble. The paper never mentions whitening (day 2's subject). It clearly whitened.

Cause 2 — the shift went the wrong way.

The paper shifts H1 back by 6.9 ms. The code added it, putting the two chirps 14 ms apart instead of on top of each other. Cross-correlating the whitened streams recovers −7.3 ms on its own — the data will tell you the answer if you ask it.

What caught it: someone who has seen enough of these plots to know what one looks like. No assertion in the repository would ever have fired. [src:borrowed — the 6.9 ms shift and the band and notch prescription are the paper's own (1608.01940 §Fig 1); the cross-correlation recovery and the noise variation across the band are the session artefact's own measurements]

Failure 2

Energy where the label says amplitude

The paper's Fig. 2 — colour bar 0–8, “normalized amplitude”
the paper's figure 2
Ours — colour bar 0–25, “normalized energy”
our spectrogram

What it did. A gwpy Q-transform, plotted raw. That function returns normalised energy; the paper's colour bar is normalised amplitude — the square root.

Why it is wrong. Two different quantities under the same axis label. The colour ceiling was then hand-set to 25 — vmax=25 in the plotting call — until the picture looked right, rather than matched to the paper's. The transform's own Q range was picked by hand too, and differs from the one used for Fig. 3, with no reason recorded for either.

What caught it. Matias, by eye — “your signal looks much brighter.” The chirp is visible in both, which is exactly why nobody looked twice.

Fixable in one line — take the square root and set the limit to the paper's — and that is the point. It was never wrong in a way that broke anything; it was wrong in a way that no test could see, and only a comparison against the original catches it.

Failure 3

It did not do what it said

The paper's Fig. 3 — nine points, with error bars
the paper's figure 3
Ours — a dense, stepped ridge
our frequency track

What the paper says to do. Measure the time between successive zero crossings of the band-passed strain, take f = 1/(2Δt), average five adjacent crossings where the signal is weak, combine H1 and L1.

What it actually did. Took the peak of each column of the Q-transform. The steps are the transform's frequency bins. This is a different measurement of a different thing — and the file's own docstring says “from zero-crossings”.

What caught it. Nothing. The fitted chirp mass landed inside the range the paper states — the line quoted in the run table above — so the number agreed while the method did not.

The best of the three. The docstring describes the paper's method; the code below it does something else; the answer still came out in range. A right answer obtained the wrong way is the hardest failure to catch, because the only thing you normally check is the answer.

Failure 4

Twenty-three out of twenty-three

The run log for that exercise says every assertion passes. It does. And not one of those assertions touches any of the three figures you just saw.

One — what the check tests

The check is named after Figure 3. Here it is, whole:

reproduce_basic_physics.py, lines 137–144.

# Fig 3 method: fit f^-8/3 vs t on a clean chirp track -> M (~30 detector frame).
Mc = 30.0 * MSUN
tt = np.linspace(-0.20, -0.005, 200)
ff = B.track_f_of_t(tt, Mc, t_c=0.0)
Mfit = B.fit_chirp_mass_from_track(tt, ff) / MSUN
OUT["fig3_method_recovers_mc_msun"] = Mfit
check("(Fig 3)  f^-8/3 straight-line fit recovers M = 30 Msun", abs(Mfit - 30) < 1e-6,
      f"{Mfit:.4f} Msun")

Read it from the top. A chirp mass is chosen by hand on the first line. track_f_of_t builds a frequency-versus-time track out of that number. The track goes straight back into fit_chirp_mass_from_track, which inverts the same formula and hands the number back. It is a function put against its own inverse. No strain enters it at any point, and there is no arrangement of the data under which the result would have come out differently.

Two — how that was reported

The check is called (Fig 3), and it sits in a list that ends with a line saying all twenty-three reported results were reproduced. Read as a receipt, that line says the paper was reproduced. What it says is that the algebra was.

The three figures made from strain — the band-pass, the spectrogram, and the ridge with its fit, all three of which the last three stages have just shown to be wrong — carry no assertion at all. Nothing in the output admits it. The count is over the checks that exist, and a check nobody wrote subtracts nothing.

Three — the tell, and there are two of them

The first is the tolerance on the last line. Nothing measured off real noisy data comes back at 30.000000 [src:fig3_method_recovers_mc_msun]; only algebra does. A tolerance that tight is a statement about what is being compared, and it is on the page for anyone who reads it.

The second is in the same receipt, a few lines below the green banner. The run also printed what the real Figure 3 produced from the strain:

run.log, lines 6–8 and line 14. The second is what the same run got off the strain.

======================================================================
23/23 reported results reproduced
======================================================================

   ...

Fig 1 strain, Fig 2 spectrogram, Fig 3 chirp-mass ridge fit -> 36 Msun (paper 30-40)

The number the run actually got off the data and the number the check verified are different numbers, in the same file, twelve lines apart — and it is the check that passed.

So the honest state of that exercise: the analytic half is verified; the data half was never checked at all — and the receipt does not say so.

The pattern

They cluster

Across all of them, the failures are not random. They cluster:

Failure modeWhat it looks likeWhat defends against it
Recall dressed as derivationthe right final answer, with a derivation that does not produce itask which steps were derived and which recalled
Confident interpolationa plausible coefficient where the real one is unusualcheck against a printed number
Silent unit errorseverything consistent, one constant in the wrong systemdimensional analysis; a second implementation
Claiming without running“the fit converges” — no log, no outputask for the receipt, always
Drifting under a full contextfine early, degraded late, no announcementbound the task; start fresh
Agreeing with youyou suggest something wrong and it goes alongask it to argue against you
Uncovered by constructiona green summary line over work that no assertion touchesmake it state what it did not check

None of these are fixed by a better model. They are fixed by checks.

Questions of that kind are scattered through homework.html, where every exercise the course sets lives. Treat them as examples rather than as a form: the job is to invent the ones your own run needs.

Before the next session

What to do with the rest of the week

Physics

  • What separation must two 30 M☉ black holes have to merge within a Hubble time?
  • How does eccentricity change that?
  • Run the whole chain again on a second event — GW230814, in the repo.

Ask for more than an answer

Make it explain the same result at three levels. Make it visualise something you only have as algebra. Make it derive a result a second way and reconcile the two.

Then push on the boundaries: where does each approximation break, and what happens on the other side?

The controlled experiment

Take one task and run it several ways: two models, two effort settings, terse prompt vs contract prompt. Ask the same questions of each. Report what actually moved the answer.

One sample per cell — report what you saw, not what is true in general.

Come back with a vault containing pages neither of us wrote by hand — and at least one page where you corrected something the agent got wrong.

Every exercise the course sets is on one page, homework.html. Every concept this week introduces, and the day it arrives: using-the-tools.html. Pick what interests you; nobody is expected to do all of it.

To take away

  • The loop is the difference. A chatbot guesses about your files; an agent reads them, runs things, and can be asked for the receipt.
  • Context is finite and cold starts are real. Everything you want to survive must be written to disk in a form that gets read next time.
  • Formats decide whether you can review the output — and review, not generation, is the bottleneck.
  • You cannot tell a good derivation from a bad one by reading it. Ask for checks, not answers.
  • The harness is the part you control, and that is where the leverage is.

None of this is settled. It is what one person does in July 2026, and half of it will look naive in a year. Take it as a starting point and go argue with it.