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) <noreply@anthropic.com>
This commit is contained in:
1 parent
d61cb8c9a1
commit
8c110098ff
6 files changed
+124
-2
No files matched your search
@@ -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,
|
/** room for a reasoning model to think and then answer; the default, 256,
|
||||||
* runs out mid-thought (`finish_reason: length`) */
|
* 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 }>;
|
export type Message = Readonly<{ role: "system" | "user" | "assistant"; content: string }>;
|
||||||
|
|
||||||
|
|||||||
@@ -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() },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,3 +13,38 @@ export const aboutMower = {
|
|||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
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<typeof makeMower>;
|
||||||
@@ -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<Env> {
|
||||||
|
#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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string, unknown>) {
|
||||||
|
return (await routeAgentRequest(request, env)) ?? new Response("not found", { status: 404 });
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -11,6 +11,8 @@
|
|||||||
"bindings": [{ "name": "Neighbor", "class_name": "Neighbor" }]
|
"bindings": [{ "name": "Neighbor", "class_name": "Neighbor" }]
|
||||||
},
|
},
|
||||||
"exports": {
|
"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" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user