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" } } }