Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,23 @@ git -c http.extraHeader="Authorization: Bearer <token>" clone https://<account-i
git log --format='%h %s [%(trailers:key=Codemode-Status,valueonly)]'
```

### Ask a model, with other agents' runs as examples

`POST /agents/:name/ask` runs a Workers AI tool-calling loop (`@cf/zai-org/glm-5.3-flash`, at most 10 steps) where the model answers by writing code against `lamps`. Before it starts, the agent retrieves up to 3 answer-producing runs of *other* agents from the `codemode-git-examples` Vectorize index (scope `lamps`) and shows them as unverified examples. Each commit's subject is the question, and its `Codemode-Examples` trailer lists the examples shown. After answering, the agent pushes its commits and indexes the run the answer came from. Create the Vectorize index and its metadata indexes first (see [Examples](#examples-learning-from-other-agents-runs-optional)).

```bash
curl -X POST localhost:8787/agents/michelle/ask -d '{"question": "How many lava lamps on the wall are flowing right now?"}'
# {"agent","question","answer","steps","executions","errors","tokens":{"input","output"},"examples":["agent-…@sha"],"commits":[{"sha","intent","status"}],"indexed"}
```

Pass `"useExamples": false` for a baseline without retrieval. `GET /agents/:name/examples?question=...` shows what `ask` would retrieve, without calling the chat model.

`scripts/walkthrough.mjs` runs the two-agent demo: `michelle` asks how many lamps are flowing, then `matthew` asks how many red lamps are flowing and is shown michelle's run. Between the two it polls until michelle's run is retrievable, because Vectorize metadata filters see new vectors only after ~1–2 minutes (`--wait` caps the wait in seconds, default 180). Agent names get a run-unique prefix, so each rerun uses fresh agents and repos. It prints both answers side by side with executions, steps, tokens, examples and commit shas.

```bash
node examples/agent/scripts/walkthrough.mjs https://codemode-git-example.<subdomain>.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.
Expand All @@ -153,7 +170,7 @@ node examples/agent/scripts/swarm.mjs https://codemode-git-example.<subdomain>.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
Expand Down
6 changes: 5 additions & 1 deletion examples/agent/cloudflare.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } })
}
}
});
6 changes: 6 additions & 0 deletions examples/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ADMIN_TOKEN>`. **Set this**: the Worker runs arbitrary code it is sent."
}
Expand Down
109 changes: 109 additions & 0 deletions examples/agent/scripts/walkthrough.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env node
// Two agents, one learning from the other.
//
// node scripts/walkthrough.mjs <base-url> [--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 (<prefix>-<run>-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 <base-url> [--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(", ")}`);
Loading
Loading