From 8c110098ff8a98a90d30672be96f9be185087ecd Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 9 Oct 2026 00:35:56 -0500 Subject: [PATCH] feat(good-fences): a Think agent mows with a codemode mower connector Neighbor is a Project Think agent on gpt-oss-120b. Its execute tool runs model-written code in a dynamic worker; MowerConnector puts the mower there as the `mower.*` namespace (start/push/turn/stop, each taking an args object). makeMower is the Logo-turtle mower on a wrapping yard; for now it lives inside the agent's DO. Getting to green, one symptom at a time: - chatted instead of acting: say it acts by running code - reply cut mid-sentence: maxOutputTokens 4096 via beforeTurn - execute failed creating codemode's runtime facet ("Incorrect type for the 'class' field"): declare CodemodeRuntime as a DO class - looked for the mower in the workspace filesystem: activeTools ["execute"], and name the namespace in the prompt - found the mower, then asked for lawn dimensions: "don't ask me questions: put it to use" Green: 36 mower calls, a 30-cell rectangle. Co-Authored-By: Claude Opus 5.5 (1M context) --- good-fences/src/model.ts | 2 +- good-fences/src/mower-connector.ts | 30 ++++++++++++++++++ good-fences/src/mower.ts | 35 ++++++++++++++++++++ good-fences/src/neighbor.ts | 44 ++++++++++++++++++++++++++ good-fences/src/worker.ts | 11 +++++++ good-fences/wrangler.model-tests.jsonc | 4 ++- 6 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 good-fences/src/mower-connector.ts create mode 100644 good-fences/src/neighbor.ts create mode 100644 good-fences/src/worker.ts diff --git a/good-fences/src/model.ts b/good-fences/src/model.ts index 58885de..9c1864e 100644 --- a/good-fences/src/model.ts +++ b/good-fences/src/model.ts @@ -5,7 +5,7 @@ export const MODEL = "@cf/openai/gpt-oss-120b"; /** room for a reasoning model to think and then answer; the default, 256, * runs out mid-thought (`finish_reason: length`) */ -const MAX_TOKENS = 4096; +export const MAX_TOKENS = 4096; export type Message = Readonly<{ role: "system" | "user" | "assistant"; content: string }>; diff --git a/good-fences/src/mower-connector.ts b/good-fences/src/mower-connector.ts new file mode 100644 index 0000000..d2341a1 --- /dev/null +++ b/good-fences/src/mower-connector.ts @@ -0,0 +1,30 @@ +/** @file The mower as a codemode connector: sandboxed code calls `mower.push({})`. */ +import { CodemodeConnector, type ConnectorTools } from "@cloudflare/codemode"; +import type { Mower } from "./mower.ts"; + +export class MowerConnector extends CodemodeConnector { + #mower: Mower; + + constructor(ctx: DurableObjectState | ExecutionContext, env: unknown, mower: Mower) { + super(ctx, env); + this.#mower = mower; + } + + name() { + return "mower"; + } + + protected instructions() { + return "A lawn mower on a yard of cells, 10 yards each. It moves like a Logo turtle."; + } + + protected tools(): ConnectorTools { + const mower = this.#mower; + return { + start: { description: "put the mower down, facing north, and cut the cell under it", execute: () => mower.start() }, + push: { description: "move forward one cell, cutting the cell it enters", execute: () => mower.push() }, + turn: { description: "turn right 90 degrees, in place", execute: () => mower.turn() }, + stop: { description: "park the mower", execute: () => mower.stop() }, + }; + } +} diff --git a/good-fences/src/mower.ts b/good-fences/src/mower.ts index 2dec5b5..424c1e3 100644 --- a/good-fences/src/mower.ts +++ b/good-fences/src/mower.ts @@ -13,3 +13,38 @@ export const aboutMower = { stop(): Promise; }`, }; + +const headings = [ + [0, -1], // north + [1, 0], // east + [0, 1], // south + [-1, 0], // west +] as const; + +/** a mower on a `width` × `height` yard, starting at `at`; pushing past an + * edge wraps to the other side (6d) */ +export const makeMower = ({ width, height, at }: { width: number; height: number; at: { x: number; y: number } }) => { + let { x, y } = at; + let heading = 0; + const cut = new Set(); + const cutHere = () => cut.add(`${x},${y}`); + return { + start() { + heading = 0; + cutHere(); + }, + push() { + const [dx, dy] = headings[heading]; + x = (x + dx + width) % width; + y = (y + dy + height) % height; + cutHere(); + }, + turn() { + heading = (heading + 1) % headings.length; + }, + stop() {}, + cutCells: () => [...cut], + }; +}; + +export type Mower = ReturnType; diff --git a/good-fences/src/neighbor.ts b/good-fences/src/neighbor.ts new file mode 100644 index 0000000..81496d9 --- /dev/null +++ b/good-fences/src/neighbor.ts @@ -0,0 +1,44 @@ +/** @file The neighbor's harness as a Project Think agent, lent a mower. */ +import { Think } from "@cloudflare/think"; +import { createExecuteTool } from "@cloudflare/think/tools/execute"; +import { createWorkersAI } from "workers-ai-provider"; + +import { MAX_TOKENS, MODEL } from "./model.ts"; +import { MowerConnector } from "./mower-connector.ts"; +import { makeMower } from "./mower.ts"; + +type Env = { AI: Ai; LOADER: WorkerLoader }; + +export class Neighbor extends Think { + #mower = makeMower({ width: 40, height: 40, at: { x: 20, y: 20 } }); + + getModel() { + return createWorkersAI({ binding: this.env.AI })(MODEL); + } + + getSystemPrompt() { + return `You are my neighbor. You have 50 acres of lawn that need mowing. +You act by running code with the execute tool. Things I lend you are +namespaces there: the mower is \`mower.*\`. Don't ask me questions: when I +lend you something, put it to use, then tell me what you did.`; + } + + beforeTurn() { + // only code: Think's workspace tools sent the model looking for the + // mower in the filesystem + return { maxOutputTokens: MAX_TOKENS, activeTools: ["execute"] }; + } + + getTools() { + return { + execute: createExecuteTool(this, { + connectors: [new MowerConnector(this.ctx, this.env, this.#mower)], + }), + }; + } + + /** the cells the mower has cut, as `"x,y"` */ + cutCells() { + return this.#mower.cutCells(); + } +} diff --git a/good-fences/src/worker.ts b/good-fences/src/worker.ts new file mode 100644 index 0000000..b1c7dbd --- /dev/null +++ b/good-fences/src/worker.ts @@ -0,0 +1,11 @@ +/** @file The worker entry: the Neighbor agent, and the codemode runtime its execute tool needs. */ +import { routeAgentRequest } from "agents"; + +export { CodemodeRuntime } from "@cloudflare/codemode"; +export { Neighbor } from "./neighbor.ts"; + +export default { + async fetch(request: Request, env: Record) { + return (await routeAgentRequest(request, env)) ?? new Response("not found", { status: 404 }); + }, +}; diff --git a/good-fences/wrangler.model-tests.jsonc b/good-fences/wrangler.model-tests.jsonc index 409a8c5..dc86a72 100644 --- a/good-fences/wrangler.model-tests.jsonc +++ b/good-fences/wrangler.model-tests.jsonc @@ -11,6 +11,8 @@ "bindings": [{ "name": "Neighbor", "class_name": "Neighbor" }] }, "exports": { - "Neighbor": { "type": "durable-object", "storage": "sqlite" } + "Neighbor": { "type": "durable-object", "storage": "sqlite" }, + // codemode's runtime, a facet of the agent's DO + "CodemodeRuntime": { "type": "durable-object", "storage": "sqlite" } } }