// Watch mode's stand-in for the model. It is a small rule-based agent written // in JavaScript, not Claude. It reads only the conversation (the same JSON // text a model would read) and answers with content blocks in the shape the // Messages API returns, so the loop around it runs exactly as it would live. import { DIRS, COLS, ROWS, SAFE_INCHES, FISH_GOAL } from "./world.js"; const LETTERS = "ABCDEFGHI"; const parseCell = (name) => [LETTERS.indexOf(name[0]), Number(name.slice(1)) - 1]; const nameOf = (x, y) => `${LETTERS[x]}${y + 1}`; const inBounds = (x, y) => x >= 0 && y >= 0 && x < COLS && y < ROWS; // Rebuild what the agent knows from the transcript alone. function readTranscript(messages) { const calls = new Map(); const k = { pos: [4, 0], visited: new Set(["E1"]), inches: new Map(), sonar: new Map(), box: [0, COLS - 1, 0, ROWS - 1], triedBlindMove: false, done: null, last: [] }; for (const m of messages) { if (m.role === "assistant" && Array.isArray(m.content)) { for (const b of m.content) if (b.type === "tool_use") calls.set(b.id, b); } if (m.role !== "user" || !Array.isArray(m.content)) continue; k.last = []; for (const r of m.content) { if (r.type !== "tool_result") continue; const call = calls.get(r.tool_use_id); let data = {}; try { data = JSON.parse(r.content); } catch { /* not JSON */ } k.last.push({ call, data, isError: !!r.is_error }); if (r.is_error) { if (call?.name === "move" && /haven't measured/.test(data.error || "")) k.triedBlindMove = true; continue; } if (call?.name === "measure_ice") k.inches.set(data.spot, data.ice_inches); if (call?.name === "move") { k.pos = parseCell(data.position); k.visited.add(data.position); } if (call?.name === "set_up_shanty") k.done = data; if (call?.name === "sonar") { k.sonar.set(data.spot, data); // "Stronger toward east" means the fish are somewhere east of here, and so on. // Each reading narrows the box the drop-off can be in. const [x, y] = parseCell(data.spot), t = data.stronger_toward, b = k.box; if (t.includes("east")) b[0] = Math.max(b[0], x + 1); else if (t.includes("west")) b[1] = Math.min(b[1], x - 1); else { b[0] = Math.max(b[0], x); b[1] = Math.min(b[1], x); } if (t.includes("north")) b[2] = Math.max(b[2], y + 1); else if (t.includes("south")) b[3] = Math.min(b[3], y - 1); else { b[2] = Math.max(b[2], y); b[3] = Math.min(b[3], y); } } } } return k; } // Breadth-first search over ice the agent has measured as safe. function safePaths(k) { const prev = new Map([[nameOf(...k.pos), null]]); const queue = [k.pos]; while (queue.length) { const [x, y] = queue.shift(); for (const [dx, dy] of Object.values(DIRS)) { const nx = x + dx, ny = y + dy, n = nameOf(nx, ny); if (!inBounds(nx, ny) || prev.has(n)) continue; const inches = k.inches.get(n); if (inches !== undefined && inches >= SAFE_INCHES) { prev.set(n, nameOf(x, y)); queue.push([nx, ny]); } } } return prev; // cell name -> the cell before it on a safe path } function firstStep(prev, from, to) { let cur = to; while (prev.get(cur) && prev.get(cur) !== from) cur = prev.get(cur); const [fx, fy] = parseCell(from), [tx, ty] = parseCell(cur); return Object.keys(DIRS).find((d) => fx + DIRS[d][0] === tx && fy + DIRS[d][1] === ty); } let idCounter = 0; const toolUse = (name, input = {}) => ({ type: "tool_use", id: `toolu_demo_${String(++idCounter).padStart(3, "0")}`, name, input }); const text = (t) => ({ type: "text", text: t }); export function resetScriptedIds() { idCounter = 0; } export function scriptedTurn(messages) { const reply = (content, stop_reason = "tool_use") => ({ content, stop_reason }); if (!messages.some((m) => m.role === "assistant")) { return reply([text("I'll get my bearings first: a look at the ice around us and a sonar reading, at the same time."), toolUse("look_around"), toolUse("sonar")]); } const k = readTranscript(messages); if (k.done) { return reply([text(`The shanty is up at ${k.done.shanty}, where sonar reads ${k.done.fish_marks} fish marks. I measured every step on the way and backed off anything under ${SAFE_INCHES} inches.`)], "end_turn"); } const here = nameOf(...k.pos); const sonarHere = k.sonar.get(here); if (sonarHere && sonarHere.fish_marks >= FISH_GOAL) { return reply([text(`${sonarHere.fish_marks} marks at ${sonarHere.depth_feet} feet. This is the spot.`), toolUse("set_up_shanty")]); } // Aim for the middle of the box the sonar readings leave open. const [ex, ey] = [(k.box[0] + k.box[1]) / 2, (k.box[2] + k.box[3]) / 2]; const score = ([x, y]) => -(Math.abs(x - ex) + Math.abs(y - ey)); const prev = safePaths(k); const reachable = [...prev.keys()].map(parseCell); const targets = []; for (const c of reachable) { const n = nameOf(...c); if (!k.visited.has(n)) targets.push({ cell: c, name: n, kind: "go" }); for (const [dx, dy] of Object.values(DIRS)) { const x = c[0] + dx, y = c[1] + dy, m = nameOf(x, y); if (inBounds(x, y) && !k.inches.has(m) && !targets.some((t) => t.name === m)) targets.push({ cell: [x, y], name: m, kind: "drill", via: n }); } } if (!targets.length) { return reply([text("Every way forward is thin ice or open water, so I'm stopping here rather than take a risk.")], "end_turn"); } const dist = ([x, y]) => Math.abs(x - k.pos[0]) + Math.abs(y - k.pos[1]); targets.sort((a, b) => score(b.cell) - score(a.cell) || dist(a.cell) - dist(b.cell)); const best = targets[0]; const dirTo = ([x, y]) => Object.keys(DIRS).find((d) => k.pos[0] + DIRS[d][0] === x && k.pos[1] + DIRS[d][1] === y); const adjacent = (t) => t.kind === "drill" && t.via === here && dirTo(t.cell); const readSonar = sonarHere ? [] : [toolUse("sonar")]; if (adjacent(best)) { const d = dirTo(best.cell); if (!k.triedBlindMove) { return reply([text(`The fish finder reads stronger to the ${d}. Heading that way.`), toolUse("move", { direction: d })]); } // Drill up to two promising neighbors at once. const drills = targets.filter(adjacent).slice(0, 2); const lastErr = k.last.find((l) => l.isError); const lead = lastErr && /haven't measured/.test(lastErr.data.error || "") ? "Right, the rule is measure first. " : ""; const where = drills.map((t) => `${dirTo(t.cell)} (${t.name})`).join(" and "); const sonarNote = readSonar.length ? `Reading sonar here and d` : "D"; return reply([text(`${lead}${sonarNote}rilling ${where} before I take a step.`), ...readSonar, ...drills.map((t) => toolUse("measure_ice", { direction: dirTo(t.cell) }))]); } if (readSonar.length) return reply([text(`Made it to ${here}. Checking the fish finder.`), ...readSonar]); const goal = best.kind === "go" ? best.name : best.via; const d = firstStep(prev, here, goal); const [nx, ny] = [k.pos[0] + DIRS[d][0], k.pos[1] + DIRS[d][1]]; const inches = k.inches.get(nameOf(nx, ny)); const why = best.kind === "go" && goal === nameOf(nx, ny) ? `${nameOf(nx, ny)} measured ${inches} inches` : `Working around the bad ice toward ${best.name}`; return reply([text(`${why}. Moving ${d}.`), toolUse("move", { direction: d })]); }