diff --git a/README.md b/README.md index 9232f36..9b17495 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,23 @@ git -c http.extraHeader="Authorization: Bearer " clone https://.workers.dev [--prefix demo] [--wait 180] +``` + ### Run many agents at once `scripts/swarm.mjs` starts N agents concurrently. Each one runs one execution and pushes the commit to its own repo, then the script prints commits per second and latency percentiles. Pass `ADMIN_TOKEN` if the Worker requires one. @@ -153,7 +170,7 @@ node examples/agent/scripts/swarm.mjs https://codemode-git-example..w [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/smithclay/codemode-git/tree/main/examples/agent) -The button copies `examples/agent` into a new repo on your GitHub or GitLab account and deploys it with Workers Builds. Create the Artifacts namespace first: the button doesn't provision Artifacts. Set `ADMIN_TOKEN` when it asks, because the Worker runs any code it's sent. The button only reads Wrangler config, so the example also has a `wrangler.jsonc` that mirrors `cloudflare.config.ts` (CI checks both). If the button fails, deploy from the command line: +The button copies `examples/agent` into a new repo on your GitHub or GitLab account and deploys it with Workers Builds. Create the Artifacts namespace first: the button doesn't provision Artifacts. For `ask`, also create the `codemode-git-examples` Vectorize index with its metadata indexes, since retrieval filters on them. Set `ADMIN_TOKEN` when it asks, because the Worker runs any code it's sent. The button only reads Wrangler config, so the example also has a `wrangler.jsonc` that mirrors `cloudflare.config.ts` (CI checks both). If the button fails, deploy from the command line: ```bash npx cf artifacts namespaces create --namespace codemode-git diff --git a/examples/agent/cloudflare.config.ts b/examples/agent/cloudflare.config.ts index 7858bcb..9e05c4f 100644 --- a/examples/agent/cloudflare.config.ts +++ b/examples/agent/cloudflare.config.ts @@ -14,7 +14,11 @@ export default defineConfig({ // One repo per agent in this namespace. ARTIFACTS: bindings.artifacts({ namespace: "codemode-git", dev: { remote: true } }), AGENT: bindings.durableObject({ worker: "codemode-git-example", exportName: "CodeAgent" }), - LOADER: bindings.workerLoader() + LOADER: bindings.workerLoader(), + // `ask`: the chat model and the embeddings for examples. + AI: bindings.ai({ dev: { remote: true } }), + // Answer-producing runs embedded as question + code, shared by all agents. + EXAMPLES: bindings.vectorize({ name: "codemode-git-examples", dev: { remote: true } }) } } }); diff --git a/examples/agent/package.json b/examples/agent/package.json index c944f6b..b411231 100644 --- a/examples/agent/package.json +++ b/examples/agent/package.json @@ -28,6 +28,12 @@ "AGENT": { "description": "Durable Object, one per agent. Holds the Code Mode runtime and the commit outbox." }, + "AI": { + "description": "Workers AI. `ask` uses it for the chat model and for the embeddings that find other agents' runs." + }, + "EXAMPLES": { + "description": "Vectorize index `codemode-git-examples` (1024 dimensions, cosine) of answer-producing runs. Needs metadata indexes on `scope` (string), `ok` (boolean) and `task` (string); see the repo README." + }, "ADMIN_TOKEN": { "description": "Requests must send `Authorization: Bearer `. **Set this**: the Worker runs arbitrary code it is sent." } diff --git a/examples/agent/scripts/walkthrough.mjs b/examples/agent/scripts/walkthrough.mjs new file mode 100644 index 0000000..799d20b --- /dev/null +++ b/examples/agent/scripts/walkthrough.mjs @@ -0,0 +1,109 @@ +#!/usr/bin/env node +// Two agents, one learning from the other. +// +// node scripts/walkthrough.mjs [--prefix demo] [--wait 180] [--poll 10] +// +// 1. michelle answers "How many lava lamps on the wall are flowing right now?" +// by writing and running her own code. Her answer-producing run is indexed. +// 2. Wait until matthew's retrieval can see michelle's run. Vectorize metadata +// filters lag new vectors by ~1–2 minutes. --wait caps the wait (seconds); +// --poll is how often to check. +// 3. matthew answers "How many red lava lamps are flowing?", shown michelle's +// run as an unverified example. +// +// Agent names get a run-unique prefix (--michelle) so every +// rerun starts with fresh agents and repos. Set ADMIN_TOKEN in the environment +// if the Worker requires it. + +const [base, ...rest] = process.argv.slice(2); +if (!base) { + console.error("usage: node scripts/walkthrough.mjs [--prefix demo] [--wait 180] [--poll 10]"); + process.exit(1); +} +const flag = (name, fallback) => { + const i = rest.indexOf(`--${name}`); + return i >= 0 ? rest[i + 1] : fallback; +}; +const prefix = flag("prefix", "demo"); +const waitSeconds = Number(flag("wait", 180)); +const pollSeconds = Number(flag("poll", 10)); +const run = Date.now().toString(36); +const auth = process.env.ADMIN_TOKEN ? { authorization: `Bearer ${process.env.ADMIN_TOKEN}` } : {}; + +const AGENTS = [ + { label: "michelle", question: "How many lava lamps on the wall are flowing right now?", expected: "67" }, + { label: "matthew", question: "How many red lava lamps are flowing?", expected: "10" } +]; +const agentName = (label) => `${prefix}-${run}-${label}`; + +async function request(method, path, body) { + const res = await fetch(new URL(path, base), { + method, + headers: body ? { ...auth, "content-type": "application/json" } : auth, + body: body && JSON.stringify(body) + }); + if (!res.ok) throw new Error(`${method} ${path}: HTTP ${res.status} ${(await res.text()).slice(0, 300)}`); + return res.json(); +} + +async function ask({ label, question }) { + console.error(`\n${label} asks: ${question}`); + const t0 = performance.now(); + const result = await request("POST", `/agents/${agentName(label)}/ask`, { question }); + const seconds = Math.round((performance.now() - t0) / 100) / 10; + console.error(`${label} answered ${JSON.stringify(result.answer)} in ${seconds}s`); + return { ...result, seconds }; +} + +/** Poll what matthew would retrieve until michelle's repo shows up, with a countdown. */ +async function waitForExample(asker, repo) { + const deadline = Date.now() + waitSeconds * 1000; + const path = `/agents/${agentName(asker.label)}/examples?question=${encodeURIComponent(asker.question)}`; + for (;;) { + const examples = await request("GET", path); + if (examples.some((e) => e.ref.startsWith(`${repo}@`))) { + process.stderr.write(`\r${repo} is retrievable.${" ".repeat(30)}\n`); + return true; + } + const left = Math.round((deadline - Date.now()) / 1000); + if (left <= 0) { + process.stderr.write(`\r${repo} not retrievable after ${waitSeconds}s; continuing anyway.\n`); + return false; + } + for (let s = Math.min(pollSeconds, left); s > 0; s--) { + process.stderr.write(`\rWaiting for Vectorize to index ${repo}: ${Math.round((deadline - Date.now()) / 1000)}s left `); + await new Promise((r) => setTimeout(r, 1000)); + } + } +} + +const [first, second] = AGENTS; +const results = []; +results.push(await ask(first)); +const firstRepo = `agent-${agentName(first.label)}`; +if (results[0].indexed) { + await waitForExample(second, firstRepo); +} else { + console.error(`${first.label}'s run was not indexed (no answer, or indexing failed); ${second.label} won't see it.`); +} +results.push(await ask(second)); + +const rows = [ + ["", ...AGENTS.map((a) => a.label)], + ["question", ...AGENTS.map((a) => a.question)], + ["answer", ...results.map((r) => String(r.answer))], + // The first number in the answer, so "67 lamps" still counts. + ["correct?", ...results.map((r, i) => (/-?\d+/.exec(String(r.answer))?.[0] === AGENTS[i].expected ? "yes" : `no (expected ${AGENTS[i].expected})`))], + ["executions", ...results.map((r) => String(r.executions))], + ["errors", ...results.map((r) => String(r.errors))], + ["steps", ...results.map((r) => String(r.steps))], + ["tokens in/out", ...results.map((r) => `${r.tokens.input}/${r.tokens.output}`)], + ["seconds", ...results.map((r) => String(r.seconds))], + ["indexed", ...results.map((r) => String(r.indexed))], + ["examples used", ...results.map((r) => r.examples.join(", ") || "none")], + ["commits", ...results.map((r) => r.commits.map((c) => (c.sha ? c.sha.slice(0, 12) : "unpushed")).join(" ") || "none")] +]; +const widths = rows[0].map((_, col) => Math.max(...rows.map((row) => row[col].length))); +console.log(); +for (const row of rows) console.log(row.map((cell, col) => cell.padEnd(widths[col])).join(" ").trimEnd()); +console.log(`\nrepos: ${AGENTS.map((a) => `agent-${agentName(a.label)}`).join(", ")}`); diff --git a/examples/agent/src/ask.ts b/examples/agent/src/ask.ts new file mode 100644 index 0000000..f011de2 --- /dev/null +++ b/examples/agent/src/ask.ts @@ -0,0 +1,234 @@ +import type { CodemodeRuntimeHandle } from "@cloudflare/codemode"; +import type { Example } from "@smithclay/codemode-git"; + +// A small Workers AI tool-calling loop around the Code Mode runtime: the model +// writes code (`codemode` tool), reads the result, and ends with `final_answer`. +// Trimmed from benchmarks/dabstep/src/solver.ts. + +export const MODEL = "@cf/zai-org/glm-5.3-flash"; +const MAX_FEEDBACK_CHARS = 8_000; + +export type LoopResult = { + answer: string; + steps: number; + executions: number; + errors: number; + tokens: { input: number; output: number }; + /** Every execution this loop started, oldest first. */ + executionIds: string[]; + /** The last execution that completed: the run the answer is based on. */ + answerExecutionId?: string; +}; + +export const exampleRef = (e: Example) => `${e.repo}@${e.sha.slice(0, 12)}`; + +function examplesText(examples: Example[]): string { + if (!examples.length) return ""; + return ` +## How other agents approached similar questions +Unverified code other agents ran to answer related questions. It may be wrong or answer a slightly different question: adapt it, run it, and check the result yourself. +${examples.map((e) => `### ${e.question} (${exampleRef(e)})\n\`\`\`js\n${e.code.trim()}\n\`\`\``).join("\n\n")} +`; +} + +function systemPrompt(examples: Example[]): string { + return `You are an agent that answers questions about a wall of lava lamps by writing JavaScript that runs in a sandbox. + +## How to work +Call the \`codemode\` tool with an async arrow function, e.g. +async () => { const page = await lamps.wall({}); return page.lamps.length; } +You get back its return value or error. Repeat as needed. When you are confident, call \`final_answer\`. + +## Sandbox +Standard JavaScript plus these globals (no fetch, no imports): +- \`await lamps.wall({ row?, wax?, status?, cursor? })\` returns \`{ lamps, nextCursor }\`. At most 12 lamps per page: follow \`nextCursor\` until it is null to see them all. Each lamp is \`{ id, row, col, wax, status, hoursOn }\`; wax is red, orange, yellow, green, blue, purple or pink; status is flowing, warming or off. +- \`await lamps.get({ id })\` returns one lamp or null. +- \`await history.log({})\` and \`await history.show({ ref })\` list and show your own earlier runs. + +## Rules +- Answer from data you fetched with code, not from guesses. +- final_answer takes only the answer value (e.g. a number), no explanation. +${examplesText(examples)}`; +} + +const TOOLS = [ + { + type: "function", + function: { + name: "codemode", + description: "Run JavaScript in the sandbox. `code` is an async arrow function; its return value comes back to you.", + parameters: { type: "object", properties: { code: { type: "string" } }, required: ["code"] } + } + }, + { + type: "function", + function: { + name: "final_answer", + description: "Submit only the final answer value. No explanation or markdown.", + parameters: { type: "object", properties: { answer: { type: "string" } }, required: ["answer"] } + } + } +]; + +type ToolCall = { id: string; type: "function"; function: { name: string; arguments: string } }; +type ChatMessage = + | { role: "system" | "user"; content: string } + | { role: "assistant"; content: string; tool_calls?: ToolCall[] } + | { role: "tool"; tool_call_id: string; content: string }; +type Action = { kind: "code"; code: string; callId?: string } | { kind: "answer"; answer: string; callId?: string }; + +/** Workers AI with a few retries on rate limits and transient errors. */ +async function runAi(ai: Ai, model: string, input: unknown): Promise { + for (let i = 0; ; i++) { + try { + return await (ai.run as (m: string, i: unknown) => Promise)(model, input); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (i >= 4 || !/3021|3040|rate limit|capacity|timeout|network connection lost/i.test(message)) throw err; + await new Promise((r) => setTimeout(r, 2_000 * 2 ** i)); + } + } +} + +async function chat(ai: Ai, model: string, messages: ChatMessage[]) { + const out = (await runAi(ai, model, { messages, tools: TOOLS, max_completion_tokens: 4000 })) as { + choices?: { message?: { content?: string | null; tool_calls?: ToolCall[] } }[]; + response?: string; + tool_calls?: { name: string; arguments: unknown }[]; + usage?: { prompt_tokens?: number; completion_tokens?: number }; + }; + const message = out.choices?.[0]?.message; + let toolCalls = message?.tool_calls ?? []; + // Older Workers AI response shape. + if (!toolCalls.length && out.tool_calls?.length) { + toolCalls = out.tool_calls.map((c, i) => ({ + id: `call_${i}`, + type: "function", + function: { name: c.name, arguments: typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments) } + })); + } + return { + text: message?.content ?? out.response ?? "", + toolCalls, + input: out.usage?.prompt_tokens ?? 0, + output: out.usage?.completion_tokens ?? 0 + }; +} + +function parseArgs(raw: unknown): Record { + // Some models' tool-call arguments arrive already parsed. + if (raw && typeof raw === "object") return raw as Record; + try { + const parsed = JSON.parse(String(raw)) as unknown; + return parsed && typeof parsed === "object" ? (parsed as Record) : {}; + } catch { + return {}; + } +} + +/** What the model wants to do: from tool calls, or from text if it didn't use them. */ +function actionsFrom(text: string, toolCalls: ToolCall[]): Action[] { + const actions: Action[] = []; + for (const call of toolCalls) { + const args = parseArgs(call.function.arguments); + if (call.function.name === "final_answer" && args.answer !== undefined) { + actions.push({ kind: "answer", answer: String(args.answer), callId: call.id }); + } else if (args.code) { + actions.push({ kind: "code", code: args.code, callId: call.id }); + } + } + if (actions.length) return actions; + const fenced = /```(?:js|javascript|ts|typescript)?\s*\n([\s\S]*?)```/.exec(text); + if (fenced) return [{ kind: "code", code: fenced[1]!.trim() }]; + const final = /FINAL ANSWER:\s*(.+)/i.exec(text); + if (final) return [{ kind: "answer", answer: final[1]!.trim() }]; + return []; +} + +function clip(value: unknown): string { + const text = typeof value === "string" ? value : (JSON.stringify(value) ?? String(value)); + return text.length > MAX_FEEDBACK_CHARS ? `${text.slice(0, MAX_FEEDBACK_CHARS)}… (truncated, ${text.length} chars)` : text; +} + +/** Ask the model a question; it answers by running code through `runtime`. */ +export async function runLoop(opts: { + ai: Ai; + runtime: CodemodeRuntimeHandle; + question: string; + examples: Example[]; + model?: string; + maxSteps?: number; +}): Promise { + const model = opts.model ?? MODEL; + const maxSteps = opts.maxSteps ?? 10; + const messages: ChatMessage[] = [ + { role: "system", content: systemPrompt(opts.examples) }, + { role: "user", content: `Question: ${opts.question}` } + ]; + const result: LoopResult = { + answer: "", + steps: 0, + executions: 0, + errors: 0, + tokens: { input: 0, output: 0 }, + executionIds: [] + }; + /** Reply to a tool call if there was one, else as a user turn. */ + const reply = (callId: string | undefined, content: string) => + messages.push(callId ? { role: "tool", tool_call_id: callId, content } : { role: "user", content }); + + for (let step = 0; step < maxSteps && !result.answer; step++) { + result.steps++; + if (step === maxSteps - 2) { + messages.push({ role: "user", content: "You are almost out of steps. Call final_answer with your best answer now." }); + } + const turn = await chat(opts.ai, model, messages); + result.tokens.input += turn.input; + result.tokens.output += turn.output; + // Echo tool calls back with valid JSON arguments: the API rejects the + // whole conversation if a model emitted malformed arguments earlier. + const toolCalls = turn.toolCalls.map((c) => ({ + ...c, + function: { ...c.function, arguments: JSON.stringify(parseArgs(c.function.arguments)) } + })); + messages.push({ role: "assistant", content: turn.text, ...(toolCalls.length ? { tool_calls: toolCalls } : {}) }); + + const actions = actionsFrom(turn.text, turn.toolCalls); + // Every tool call needs a tool reply. + const answered = new Set(actions.map((a) => a.callId).filter(Boolean)); + for (const c of toolCalls) { + if (!answered.has(c.id)) { + reply(c.id, 'Invalid arguments. Call codemode with {"code": ""} or final_answer with {"answer": "..."}.'); + } + } + if (!actions.length) { + if (!toolCalls.length) { + messages.push({ role: "user", content: "Call the codemode tool to run code, or final_answer when you are confident." }); + } + continue; + } + for (const action of actions) { + if (action.kind === "answer") { + if (!result.answerExecutionId) { + reply(action.callId, "Check against the data with code before giving a final answer."); + continue; + } + result.answer = action.answer.trim(); + reply(action.callId, "Recorded."); + continue; + } + result.executions++; + const outcome = await opts.runtime.execute({ code: action.code }); + result.executionIds.push(outcome.executionId); + if (outcome.status === "completed") { + result.answerExecutionId = outcome.executionId; + const logs = outcome.logs?.length ? `\nLogs:\n${clip(outcome.logs.join("\n"))}` : ""; + reply(action.callId, `Result:\n${clip(outcome.result)}${logs}`); + } else { + result.errors++; + reply(action.callId, `Error:\n${clip("error" in outcome ? outcome.error : outcome.status)}`); + } + } + } + return result; +} diff --git a/examples/agent/src/index.ts b/examples/agent/src/index.ts index 7f03e78..305096a 100644 --- a/examples/agent/src/index.ts +++ b/examples/agent/src/index.ts @@ -1,6 +1,15 @@ import { DurableObject } from "cloudflare:workers"; import { createCodemodeRuntime, DynamicWorkerExecutor } from "@cloudflare/codemode"; -import { ArtifactsSink, GitHistoryConnector } from "@smithclay/codemode-git"; +import { + ArtifactsSink, + GitHistoryConnector, + indexExample, + retrieveExamples, + TRAILERS, + type Example, + type GitHistoryOptions +} from "@smithclay/codemode-git"; +import { exampleRef, MODEL, runLoop } from "./ask"; import { LampsConnector } from "./lamps"; // Code Mode's runtime facet class must be exported from the entry module. @@ -10,10 +19,44 @@ type Env = { LOADER: WorkerLoader; ARTIFACTS: Artifacts; AGENT: DurableObjectNamespace; + /** Workers AI: the chat model for `ask` and the embedding model for examples. */ + AI: Ai; + /** Vectorize index of answer-producing runs, shared by every agent. */ + EXAMPLES: Vectorize; /** If set, every request must send `Authorization: Bearer `. */ ADMIN_TOKEN?: string; }; +/** All lamp-wall agents share one examples scope in the Vectorize index. */ +const SCOPE = "lamps"; + +/** + * Example task id. `retrieveExamples` skips matches whose task equals + * `excludeTask` and keeps one match per task, so keying by agent excludes all + * of the asking agent's own runs and returns at most one run per other agent. + */ +const taskId = (agent: string) => `agent-${agent}`; + +const retrieve = (env: Env, agent: string, question: string) => + retrieveExamples({ ai: env.AI, index: env.EXAMPLES, scope: SCOPE, question, excludeTask: taskId(agent), k: 3 }); + +export type AskResult = { + agent: string; + question: string; + /** null if the model ran out of steps without calling final_answer. */ + answer: string | null; + steps: number; + executions: number; + errors: number; + tokens: { input: number; output: number }; + /** Other agents' runs shown to the model (repo@sha12). */ + examples: string[]; + /** This ask's commits, oldest first. */ + commits: { sha: string | null; intent: string; status: string }[]; + /** Whether the answer-producing run was indexed as an example for other agents. */ + indexed: boolean; +}; + /** * A stock Code Mode agent. The only codemode-git additions are the sink, * the GitHistoryConnector in the connector list, and `history.bind(...)`. @@ -22,13 +65,13 @@ export class CodeAgent extends DurableObject { // Kept per instance so the warm in-memory clone is reused across runs. #sink?: ArtifactsSink; - #setup(agent: string) { + #setup(agent: string, options: Omit = {}) { this.#sink ??= new ArtifactsSink({ artifacts: this.env.ARTIFACTS, repo: `agent-${agent}`, description: `Code Mode history for agent ${agent}` }); - const history = new GitHistoryConnector(this.ctx, this.env, { sink: this.#sink }); + const history = new GitHistoryConnector(this.ctx, this.env, { ...options, sink: this.#sink }); const runtime = history.bind( createCodemodeRuntime({ ctx: this.ctx, @@ -52,26 +95,98 @@ export class CodeAgent extends DurableObject { await history.flush(); return history.commits(20); } + + /** Answer a question with a model-driven Code Mode loop, shown other agents' runs as examples. */ + async ask(agent: string, question: string, useExamples: boolean): Promise { + const examples: Example[] = useExamples ? await retrieve(this.env, agent, question) : []; + const shown = examples.map(exampleRef); + const { runtime, history } = this.#setup(agent, { + describe: () => question.slice(0, 200), + extraTrailers: (): Record => (shown.length ? { [TRAILERS.examples]: shown.join(", ") } : {}) + }); + + const loop = await runLoop({ ai: this.env.AI, runtime, question, examples, model: MODEL, maxSteps: 10 }); + + // Push every run before reporting, so the response has real shas. + await history.flush(); + const ids = new Set(loop.executionIds); + const rows = history.commits(200).filter((c) => ids.has(c.execution_id)); + + // The run the answer came from becomes an example for other agents. + let indexed = false; + const sha = rows.find((c) => c.execution_id === loop.answerExecutionId && c.sha)?.sha; + if (loop.answer && sha) { + const code = (await runtime.executions()).find((e) => e.id === loop.answerExecutionId)?.code; + if (code) { + try { + await indexExample({ + ai: this.env.AI, + index: this.env.EXAMPLES, + scope: SCOPE, + task: taskId(agent), + repo: `agent-${agent}`, + sha, + question, + code, + ok: true + }); + indexed = true; + } catch (err) { + console.error(`indexExample failed for agent-${agent}@${sha}`, err); + } + } + } + + return { + agent, + question, + answer: loop.answer || null, + steps: loop.steps, + executions: loop.executions, + errors: loop.errors, + tokens: loop.tokens, + examples: shown, + commits: rows.reverse().map((c) => ({ sha: c.sha, intent: c.intent, status: c.status })), + indexed + }; + } } +const USAGE = `POST /agents/:name/execute (body: code) +POST /agents/:name/ask (body: {"question": "...", "useExamples"?: false}) +GET /agents/:name/examples?question=... (what ask would retrieve) +GET /agents/:name/commits +POST /agents/:name/flush +`; + export default { async fetch(request, env): Promise { if (env.ADMIN_TOKEN && request.headers.get("authorization") !== `Bearer ${env.ADMIN_TOKEN}`) { return new Response("unauthorized", { status: 401 }); } - const match = /^\/agents\/([a-z0-9-]{1,48})\/(execute|commits|flush)$/.exec(new URL(request.url).pathname); - if (!match) { - return new Response( - "POST /agents/:name/execute (body: code) | GET /agents/:name/commits | POST /agents/:name/flush\n", - { status: 404 } - ); - } + const url = new URL(request.url); + const match = /^\/agents\/([a-z0-9-]{1,48})\/(execute|ask|examples|commits|flush)$/.exec(url.pathname); + if (!match) return new Response(USAGE, { status: 404 }); const [, agent, action] = match as unknown as [string, string, string]; const stub = env.AGENT.get(env.AGENT.idFromName(agent)); if (action === "execute" && request.method === "POST") { return Response.json(await stub.execute(agent, await request.text())); } + if (action === "ask" && request.method === "POST") { + const body = (await request.json().catch(() => null)) as { question?: unknown; useExamples?: unknown } | null; + const question = typeof body?.question === "string" ? body.question.trim() : ""; + if (!question || question.length > 1000) { + return new Response('body must be {"question": "..."} (at most 1000 chars)\n', { status: 400 }); + } + return Response.json(await stub.ask(agent, question, body?.useExamples !== false)); + } + if (action === "examples" && request.method === "GET") { + const question = url.searchParams.get("question")?.trim(); + if (!question) return new Response("missing ?question=\n", { status: 400 }); + const examples = await retrieve(env, agent, question); + return Response.json(examples.map((e) => ({ ref: exampleRef(e), question: e.question, score: e.score }))); + } if (action === "commits" && request.method === "GET") { return Response.json(await stub.commits(agent)); } diff --git a/examples/agent/wrangler.jsonc b/examples/agent/wrangler.jsonc index 5072b32..8185843 100644 --- a/examples/agent/wrangler.jsonc +++ b/examples/agent/wrangler.jsonc @@ -13,5 +13,9 @@ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["CodeAgent"] }], // One repo per agent in this namespace. "artifacts": [{ "binding": "ARTIFACTS", "namespace": "codemode-git" }], - "worker_loaders": [{ "binding": "LOADER" }] + "worker_loaders": [{ "binding": "LOADER" }], + // `ask`: the chat model and the embeddings for examples. + "ai": { "binding": "AI", "remote": true }, + // Answer-producing runs embedded as question + code, shared by all agents. + "vectorize": [{ "binding": "EXAMPLES", "index_name": "codemode-git-examples", "remote": true }] }