Skip to content

Commit 55faa62

Browse files
Tell a built-in Bot about its computer (#79)
The instructions that make the computer usable live in shared/bot-prompt.ts: navigate rather than claiming you cannot browse, snapshot before acting, and at a sign-in call computer_request_help so a person can take the wheel rather than reporting the task as impossible. Two files imported it, both of them shipped Bots. A built-in agent knew only the role its package gave it, while the surface offered it the same computer tools as everything else. Asked to file an issue on a site it was not signed in to, General Assistant browsed to the page, said it could not, and never asked for help, so the wheel was never offered. That is not a Bot declining; it is a Bot that was never told. Renamed to COMPUTER_GUIDANCE, because instructions about the computer belong to the computer rather than to one implementation, and appended to the package role for built-in agents. Only where a computer is configured: a deployment with the browser routes unmounted should not promise a Bot hands it has not got. Co-authored-by: David McKay <davidmckayv@users.noreply.github.com>
1 parent 016498f commit 55faa62

6 files changed

Lines changed: 75 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,13 @@ Sessions survive and nobody signs in again.
224224
laptop `http://localhost` counts as one, so this never showed up in development; on a real
225225
address it does not, and the surface did nothing at all when you pressed send. No message, no
226226
error. Ids now come from an API with no such restriction.
227+
- **A package Bot did not know it had a computer.** The instructions that make the computer usable —
228+
snapshot before acting, and ask a person to take the wheel at a sign-in rather than reporting the
229+
task as impossible — were imported by the two shipped Bots and by nothing else, so a built-in agent
230+
knew only the role its package gave it. The tools were on offer to it the whole time. Asked to file
231+
an issue on a site it was not signed in to, it browsed to the page, said it could not, and never
232+
called `computer_request_help`, so nobody was ever offered the wheel. Built-in agents are now told
233+
the same thing the shipped Bots are told, wherever a computer is configured.
227234
- **A chat could quietly forget everything and carry on.** The browser remembers a thread id for each
228235
Bot, and nothing ever asked whether Intelligence still had that thread. Where it did not, the
229236
transcript loaded empty, every later message silently recreated an empty thread under the same id,

agent-bot/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EventEncoder } from "@ag-ui/encoder";
33
import { serve } from "bun";
44
import OpenAI from "openai";
55
import { hasManagedAgentToken } from "../../shared/agent-authorisation";
6-
import { SYSTEM_PROMPT } from "../../shared/bot-prompt";
6+
import { COMPUTER_GUIDANCE } from "../../shared/bot-prompt";
77

88
/**
99
* The built-in Bot is an AG-UI HTTP service registered the same way as any customer-provided Bot.
@@ -70,7 +70,7 @@ const openai = new OpenAI({
7070
/** Translate the conversation AG-UI carries into the shape the model provider expects. */
7171
function toProviderMessages(input: RunAgentInput) {
7272
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
73-
{ role: "system", content: SYSTEM_PROMPT },
73+
{ role: "system", content: COMPUTER_GUIDANCE },
7474
];
7575

7676
for (const message of input.messages) {

agent-langgraph/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
import { ChatOpenAI } from "@langchain/openai";
1919
import { serve } from "bun";
2020
import { hasManagedAgentToken } from "../../shared/agent-authorisation";
21-
import { SYSTEM_PROMPT } from "../../shared/bot-prompt";
21+
import { COMPUTER_GUIDANCE } from "../../shared/bot-prompt";
2222

2323
/**
2424
* The same Bot, on a framework.
@@ -129,7 +129,7 @@ if (!API_KEY) {
129129

130130
/** Translate the conversation AG-UI carries into LangChain's message classes. */
131131
function toLangChainMessages(input: RunAgentInput): BaseMessage[] {
132-
const messages: BaseMessage[] = [new SystemMessage(SYSTEM_PROMPT)];
132+
const messages: BaseMessage[] = [new SystemMessage(COMPUTER_GUIDANCE)];
133133

134134
for (const message of input.messages) {
135135
if (message.role === "user") {

server/src/copilot.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import {
66
CopilotRuntime,
77
} from "@copilotkit/runtime/v2";
88
import { createCopilotHonoHandler } from "@copilotkit/runtime/v2/hono";
9+
import { z } from "zod";
10+
import { COMPUTER_GUIDANCE } from "../../shared/bot-prompt";
911
import type { AgentActor } from "./agents/profile-types";
1012
import type { StallGuard } from "./channels/stall-guard";
1113
import type { DeploymentConfig } from "./config";
12-
import { z } from "zod";
1314
import type { GrantedTool } from "./plugins/tools";
1415

1516
/**
@@ -175,6 +176,15 @@ export function builtInAgentConfiguration(
175176
* let the agent reach a vendor directly and walk around all three.
176177
*/
177178
tools: GrantedTool[] = [],
179+
/**
180+
* What this Bot should know about the computer, when this deployment has one.
181+
*
182+
* Appended to the role rather than replacing it: the package says what the Bot is for, this says
183+
* what its hands are. Absent leaves the role alone, which is right for a deployment with no
184+
* computer configured, where the browser routes are not mounted and a Bot promised a browser would
185+
* be promising something that does not exist.
186+
*/
187+
computerGuidance?: string,
178188
): BuiltInAgentConfiguration {
179189
if (!apiKey) {
180190
return {
@@ -190,7 +200,9 @@ export function builtInAgentConfiguration(
190200

191201
return {
192202
model: `${model.provider}/${model.defaultModel}`,
193-
prompt: agent.systemPrompt,
203+
prompt: computerGuidance
204+
? `${agent.systemPrompt}\n\n${computerGuidance}`
205+
: agent.systemPrompt,
194206
apiKey,
195207
/*
196208
* A run stops after one step unless told otherwise, which for a Bot with tools means it calls
@@ -228,12 +240,22 @@ export async function buildAgents(
228240
/** Absent leaves every Bot with no tools, which is the correct answer when nothing is granted. */
229241
loadTools: LoadToolsForBot = async () => [],
230242
signRun?: SignRun,
243+
/** What every built-in Bot is told about the computer. Absent means this deployment has none. */
244+
computerGuidance?: string,
231245
): Promise<Record<string, AbstractAgent>> {
232246
return Object.fromEntries(
233247
await Promise.all(
234248
agents.map(async (agent) => [
235249
agent.id,
236-
await buildAgent(agent, model, apiKey, stallGuard, loadTools, signRun),
250+
await buildAgent(
251+
agent,
252+
model,
253+
apiKey,
254+
stallGuard,
255+
loadTools,
256+
signRun,
257+
computerGuidance,
258+
),
237259
]),
238260
),
239261
);
@@ -246,6 +268,7 @@ async function buildAgent(
246268
stallGuard: StallGuard | undefined,
247269
loadTools: LoadToolsForBot,
248270
signRun?: SignRun,
271+
computerGuidance?: string,
249272
): Promise<AbstractAgent> {
250273
if (agent.type === "built_in") {
251274
return new BuiltInAgent(
@@ -254,6 +277,7 @@ async function buildAgent(
254277
model,
255278
apiKey,
256279
await loadTools(agent.id),
280+
computerGuidance,
257281
),
258282
);
259283
}
@@ -389,6 +413,7 @@ export async function resolveRuntimeAgents(
389413
stallGuard?: StallGuard,
390414
loadTools?: LoadToolsForBot,
391415
signRun?: SignRun,
416+
computerGuidance?: string,
392417
): Promise<Record<string, AbstractAgent>> {
393418
const registered = await loadAgents();
394419
if (registered.length === 0) {
@@ -400,7 +425,15 @@ export async function resolveRuntimeAgents(
400425
const apiKey = registered.some((agent) => agent.type === "built_in")
401426
? await resolveModelApiKey()
402427
: null;
403-
return buildAgents(registered, model, apiKey, stallGuard, loadTools, signRun);
428+
return buildAgents(
429+
registered,
430+
model,
431+
apiKey,
432+
stallGuard,
433+
loadTools,
434+
signRun,
435+
computerGuidance,
436+
);
404437
}
405438

406439
/** What one Bot may call, for the person whose request this is. */
@@ -445,6 +478,8 @@ export function createRequestAgents(
445478
loadToolsForActor?: (actorId: string) => LoadToolsForBot,
446479
/** Resolved per request, because what it signs is who this request turned out to be. */
447480
signRunForActor?: (actorId: string) => SignRun,
481+
/** What every built-in Bot is told about the computer. Absent means this deployment has none. */
482+
computerGuidance?: string,
448483
) {
449484
return async ({ request }: { request: Request }) => {
450485
const actor = await identifyActor(request);
@@ -455,6 +490,7 @@ export function createRequestAgents(
455490
stallGuard,
456491
loadToolsForActor?.(actor.id),
457492
signRunForActor?.(actor.id),
493+
computerGuidance,
458494
);
459495
};
460496
}
@@ -513,6 +549,13 @@ export function mountCopilotRuntime(
513549
stallGuard,
514550
loadToolsForActor,
515551
signRunForActor,
552+
/*
553+
* Only when a computer exists. The tools themselves are registered by the surface, so a Bot is
554+
* offered them without this and the guidance is what tells it how they go together: snapshot
555+
* before acting, and ask a person to take the wheel at a sign-in rather than reporting the task
556+
* as impossible. Absent computer, absent guidance: a Bot is not told about hands it has not got.
557+
*/
558+
config.computer ? COMPUTER_GUIDANCE : undefined,
516559
) as never,
517560
});
518561

shared/bot-prompt.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
import { describe, expect, test } from "bun:test";
2-
import { SYSTEM_PROMPT } from "./bot-prompt";
2+
import { COMPUTER_GUIDANCE } from "./bot-prompt";
33

4-
describe("SYSTEM_PROMPT", () => {
4+
describe("COMPUTER_GUIDANCE", () => {
55
test("keeps paragraph breaks as blank lines instead of collapsing them into spaces", () => {
6-
expect(SYSTEM_PROMPT).toContain("\n\n");
7-
expect(SYSTEM_PROMPT).not.toContain(" ");
6+
expect(COMPUTER_GUIDANCE).toContain("\n\n");
7+
expect(COMPUTER_GUIDANCE).not.toContain(" ");
88
});
99

1010
test("keeps each paragraph as one unbroken line of prose", () => {
11-
for (const paragraph of SYSTEM_PROMPT.split("\n\n")) {
11+
for (const paragraph of COMPUTER_GUIDANCE.split("\n\n")) {
1212
expect(paragraph).not.toContain("\n");
1313
expect(paragraph.length).toBeGreaterThan(0);
1414
}
1515
});
1616

1717
test("still contains the full instruction text, unchanged in wording", () => {
18-
expect(SYSTEM_PROMPT).toContain(
18+
expect(COMPUTER_GUIDANCE).toContain(
1919
"You are a Bot with your own computer, a real web browser the person can watch you use.",
2020
);
21-
expect(SYSTEM_PROMPT).toContain(
21+
expect(COMPUTER_GUIDANCE).toContain(
2222
"Say what you found or did in plain language, briefly.",
2323
);
2424
});

shared/bot-prompt.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
/**
22
* What a Bot in this box knows about its own hands.
33
*
4-
* Shared by `agent-bot` and `agent-langgraph` so the implementations differ by framework, not by
5-
* instructions or available computer behavior.
4+
* Shared by `agent-bot` and `agent-langgraph`, whose whole prompt this is, and by the built-in
5+
* agents, which append it to the role their tenant package gives them. A Bot's instructions about
6+
* its computer belong to the computer, not to one implementation: the tools are registered by the
7+
* surface and are on offer to every Bot alike, so a Bot told nothing about them is a Bot that
8+
* apologises for work it could have done. That is what happened to the built-in agents, which knew
9+
* only their role: asked to file an issue on a site it was not signed in to, one browsed to the page
10+
* and then said it could not, never calling `computer_request_help` to have a person sign in.
611
*/
712
/**
813
* The order of operations that makes the computer tools usable.
914
*
1015
* The prompt requires snapshot-first computer use. Element refs are opaque and valid only with the
1116
* snapshotId that produced them, so the Bot must read refs from the page before acting.
1217
*/
13-
const SYSTEM_PROMPT_LINES = [
18+
const COMPUTER_GUIDANCE_LINES = [
1419
"You are a Bot with your own computer, a real web browser the person can watch you use.",
1520
"When you are asked to look at, open, visit, check or read a web page, call computer_navigate.",
1621
"Never claim you cannot browse: opening a page is something you can actually do.",
@@ -56,11 +61,11 @@ const SYSTEM_PROMPT_LINES = [
5661
];
5762

5863
/**
59-
* `SYSTEM_PROMPT_LINES` uses `""` as a paragraph break. Joining the whole array with `" "` would
64+
* `COMPUTER_GUIDANCE_LINES` uses `""` as a paragraph break. Joining the whole array with `" "` would
6065
* collapse those breaks into a double space instead of a real paragraph gap, turning the prompt into
6166
* one run-on block. Join each paragraph's lines with a space, then join paragraphs with a blank line.
6267
*/
63-
export const SYSTEM_PROMPT = SYSTEM_PROMPT_LINES.reduce<string[]>(
68+
export const COMPUTER_GUIDANCE = COMPUTER_GUIDANCE_LINES.reduce<string[]>(
6469
(paragraphs, line) => {
6570
if (line === "") {
6671
paragraphs.push("");

0 commit comments

Comments
 (0)