The unit of work is no longer a line of code. It is a verifiable scientific task.
A real session against demos/earth-agent-demo, replayed at reading speed. Every command and every line of output actually ran.
next token
You write every line. The tool guesses the end of the one you are on.
question → code
You paste a question, read an answer, and paste the code back yourself.
goal → inspect → plan → act → run → observe → iterate
You state an outcome. It works in the repository until the evidence says it is done.
We are moving from writing code with AI to supervising AI that works on code.
| Path | What it is | Why it is there |
|---|---|---|
| demos/earth-agent-demo/ | A small streamflow project, two water years | The repository the agent works in today |
| docs/SCIENTIFIC_CONTEXT.md | Units, water year, missing-value rule, invariants | What the agent must read before it edits |
| src/streamflow.py | Four summaries and a plot | Runs clean; four of its answers are wrong |
| tests/test_streamflow.py | Five tests, all green | Green tests that catch none of the four |
Not a mystery. A model that can look at the world, pick a tool, and look again.
Coding agent = Model + Context + Tools + Environment + Feedback loop + Permissions
It reads files, output and errors from a real environment, not from your message.
At each step it picks the next tool. That choice is the whole of the "agency".
The result of one step becomes the input to the next, until a stop condition is met.
A software agent loop stops at Observe. Verify and Record are what make it usable for science.
| Claude Code | Codex | GitHub Copilot | |
|---|---|---|---|
| Project context | CLAUDE.md | AGENTS.md | copilot-instructions.md |
| Plan before acting | Plan mode | Plan mode | Plan mode |
| Acts through | Shell, edit, git, MCP | CLI, IDE, cloud | IDE, CLI, GitHub |
| Extends with | Skills, hooks, subagents, MCP | Skills, subagents, MCP | Skills, custom agents, MCP |
Good agentic coding is less about clever prompting than about a well-built work environment.
| What you write | What you can check afterwards | |
|---|---|---|
| Weak | Fix my analysis. | Nothing. You cannot tell whether it is done. |
| Better | Read analysis.py, find the bug, fix it. | That a bug was found. Not that it was the right one. |
| A contract | Outcome, constraints, boundary, evidence, stop condition. | Every clause, one at a time. |
Five words to carry out of this lecture: Goal, Context, Constraints, Evidence, Stop.
docs/SCIENTIFIC_CONTEXT.md
## The Water Year The water year begins on October 1 and is named for the calendar year it ends in. Water year 2018 runs 2017-10-01 to 2018-09-30. Annual summaries in this project use the water year. On this record the two conventions disagree by nearly a factor of two for 2018. ## Missing Observations Missing discharge stays NaN. It is never filled with zero, and never interpolated unless a human asks in writing and the choice is recorded. Zero is a real discharge: it means the channel is dry.
discharge in cfs; 1 cfs = 0.0283168 m3/s; elevation in metres above NAVD88.
Water year from October 1. Local standard time. No daylight saving.
NaN stays NaN. Zero means a dry channel, not an absent sensor.
Raw data immutable. No interpolation without a written request.
?
Your agent has no scientific context file. You ask it to compute annual means from a daily record. What will it almost certainly do, and will anything complain?
Can an agent improve this analysis without changing the science?
$ python -m pytest tests -q ..... [100%] 5 passed in 0.73 s $ python -c "import streamflow; print(streamflow.annual_mean(df).loc[2018])" 4195.4 cfs the record says the answer is 2418.0
Five green tests, and the number is 74 per cent too high. The suite checks shapes and exceptions. Nobody wrote a test for the year convention, because the person who wrote the tests had the same misunderstanding as the code.
The task given to the agent
Inspect this repository. Explain the analysis workflow and identify the scientific assumptions it makes. Do not edit anything.
What came back
read docs/SCIENTIFIC_CONTEXT.md
water year begins October 1
missing discharge stays NaN, never zero
read src/streamflow.py
grep 'fillna|dt.year|m3/s'
line 14 .fillna(0) contradicts the missing-value rule
line 20 .dt.year calendar year, not water year
line 38 'Discharge (m3/s)' the values are cfs
line 47 .dt.year again the same decision, duplicatedThe safe change first
- ax.set_ylabel("Discharge (m3/s)")
+ ax.set_ylabel("Discharge (cfs)")
$ python -m pytest tests -qOutput
..... [100%] 5 passed in 1.24s
The approved scientific change
+ def water_year(dates): + return dates.dt.year + (dates.dt.month >= 10) - return df.groupby(df["date"].dt.year)[...].mean() + return df.groupby(water_year(df["date"]))[...].mean() - df["discharge_cfs"] = df["discharge_cfs"].fillna(0) $ python -m pytest tests -q
Output
def test_annual_mean_returns_one_value_per_year(df):
result = streamflow.annual_mean(df)
> assert len(result) == 3
E assert 2 == 3
1 failed, 4 passed?
You made the code scientifically correct and a passing test started failing. Which one is wrong, the code or the test?
tests/test_streamflow.py
def test_annual_mean_uses_water_years(df):
# 2017-10-01 to 2019-09-30 is water years 2018 and 2019.
# Three groups would mean it is grouping by calendar year.
result = streamflow.annual_mean(df)
assert list(result.index) == [2018, 2019]
assert result.loc[2018] == pytest.approx(2418.0, abs=0.1)
def test_missing_days_are_skipped_not_zeroed(df):
# Ten days are missing in June 2018. Filling them with zero
# would pull the water-year mean down to about 2351.8 cfs.
assert streamflow.annual_mean(df).loc[2018] > 2400git diff, read in full
$ python -c "print(annual_mean(df).round(1).to_dict())"
$ python -c "print(summary_table(df).set_index('year').mean_cfs.round(1).to_dict())"Output
annual_mean {2018: 2418.0, 2019: 5764.3}
summary_table {2017: 664.2, 2018: 4313.5, 2019: 5018.2}
6 passedPassing tests is not the same as being scientifically correct.
Agents help at every level. Responsibility moves to you as you climb.
| Silent error | What it looks like in our own data |
|---|---|
| Units | cfs read as m3/s: the 2018 peak becomes 419 instead of 14,800 |
| Calendar | Calendar year where the water year was meant: 4,313 against 2,418 cfs |
| Missing values | Ten absent days filled with zero: every mean containing them drops |
| Coordinates | Latitude and longitude swapped, or 0-360 against -180-180 |
| Vertical sign, projection | Elevation against depth; two layers in different systems |
| Time and scale | UTC against local; a satellite product used without its scale factor |
Python
reference_monthly = (stream.assign(month=stream["date"].dt.to_period("M"))
.groupby("month")["discharge_cfs"].mean())
reference_annual = (stream.assign(year=stream["date"].dt.year)
.groupby("year")["discharge_cfs"].mean())
print(reference_annual.loc[2018:2020].round(1).to_string())Output
year 2018 4263.5 2019 4712.1 2020 2775.8
Python
tiny = pd.DataFrame({
"date": pd.to_datetime(["2030-01-01", "2030-01-02", "2030-01-03", "2030-02-01"]),
"discharge_cfs": [100.0, 300.0, np.nan, 50.0],
})
by_month = (tiny.assign(month=tiny["date"].dt.to_period("M"))
.groupby("month")["discharge_cfs"])
print("mean: ", by_month.mean().tolist())
print("count:", by_month.count().tolist())
assert by_month.mean().loc["2030-01"] == 200.0Output
mean: [200.0, 50.0] count: [2, 1]
?
np.allclose says the monthly values match the reference. assert_series_equal says they do not. Which do you believe?
Six ways to give an agent more reach, and one way to fool yourself with them.
Project knowledge that persists, so you stop repeating yourself.
A procedure you wrote once, reused whenever it applies.
A deterministic check that runs every time, whatever the model decides.
A bridge to outside tools and data: a catalogue, a database, an instrument.
A bounded task in its own context, so one long job does not crowd out the rest.
Separate worktrees, so two changes cannot overwrite each other.
The agent owns 04 and 05. You own 01, 02, 06, 07 and 08. Steps 6 to 8 are in accent because they are the ones a software course leaves out.
What to hand over, what to keep, and how to say which you did.
| Log field | Example entry, Group Lab 4 |
|---|---|
| Purpose | Move the annual summary onto the water year |
| Prompt | Read the repository, list contradictions with the context file, propose a plan, do not edit |
| How verified | pytest green, plus WY2018 == 2418.0 against the record |
| Changed by hand | Rewrote the test the agent wanted to delete |
| Rejected | Its offer to interpolate the June gap; the context file forbids it |
?
Name one convention from your own field that an agent would get wrong without being told. What would you write in the context file?