Skip to content
Draft
2 changes: 1 addition & 1 deletion agent-claude-sdk/tests/fixtures/server-instructions.json

Large diffs are not rendered by default.

17 changes: 9 additions & 8 deletions examples/personal/agents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,19 @@ agents:
# wait for them", which contradicted the platform's own rule: a sign-in is handed over with a
# tool call, never asked for in a sentence. `synchronizeTenantPackage` replaces either earlier
# wording on a starter that still carries it word for word.
#
# It used to repeat OpenBot's own push to act ("do the work, not a description of it", "make
# the sensible choice and keep going"), so a Bot heard it three times and treated "are you able
# to...?" as "go and do it". How to work and when to ask are OpenBot's prompt's job, said once
# there; this keeps what is particular to an assistant.
system_prompt: >-
You are a personal assistant for one person. You take everyday work off their hands: email,
calendar, research, planning and errands. Do the work, not a description of it: when you
decide on a step, take it in the same reply. Look in what you already have first, this
conversation, earlier results and their apps, then the web, and ask the person last. Ask only
when a wrong guess would be hard to undo, when the possible readings lead to different work,
or when only they know the answer. Otherwise make the sensible choice, say what you assumed,
and keep going, with any question at the end. When you send, buy, delete or post something,
calendar, research, planning and errands. When they ask you to do something, do it; when
they ask whether you can, answer and offer. When you send, buy, delete or post something,
say exactly what it is; OpenBot asks the person before it happens, so you do not need to.
Never type a password or a code yourself: when a site needs one, hand the browser to the
person with the tool for that, and carry on once they hand it back. Be clear and brief, and
end with anything you could not confirm.
person with the tool for that, and carry on once they hand it back. Keep replies short: lead
with the answer, and end with anything you could not confirm.
# `bot-creator` offers the tools that make another Bot in a conversation, and `skill-creator` the
# tools that turn an interview into a saved skill. Both are defined in skills.yaml.
skills:
Expand Down
6 changes: 4 additions & 2 deletions server/tests/copilot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ const WORKING_SECTIONS = [
"asking",
"sources",
"answer_format",
"replies",
"documents",
"subagents",
"untrusted_content",
];
Expand Down Expand Up @@ -2184,7 +2186,7 @@ describe("a Claude Agent SDK Bot on a routine", () => {
describe("when a Bot gives work to subagents", () => {
const model = { provider: "openai" as const, defaultModel: "gpt-5.6-terra" };

test("a built-in Bot carries it after the component rule", () => {
test("a built-in Bot carries it after the reply and document rules", () => {
const configuration = builtInAgentConfiguration(
{
id: "general-assistant",
Expand All @@ -2200,7 +2202,7 @@ describe("when a Bot gives work to subagents", () => {
);
const sections = sectionsOf(prompt);
expect(sections.indexOf("subagents")).toBe(
sections.indexOf("answer_format") + 1,
sections.indexOf("documents") + 1,
);
expect(sectionOf(prompt, "subagents")).toBe(ORCHESTRATION_GUIDANCE);
});
Expand Down
2 changes: 1 addition & 1 deletion server/tests/runs-that-finish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ describe("a run that ends on a promise", () => {
expect(
JSON.stringify(lastMessage(asked[1] as ChatCompletionRequest)),
).toContain(
'You said \\"I\'ll fetch the rest now.\\" but made no tool call. Do it now, or finish with the result and what is missing.',
'You said \\"I\'ll fetch the rest now.\\" but made no tool call. If that is part of what you were asked to do, do it now. If it is only an offer, leave it for the person and end your answer.',
);
expect(lookups).toEqual(["ENG-2"]);
// The nudge was for the model only: the conversation holds what the Bot said and did.
Expand Down
168 changes: 163 additions & 5 deletions shared/bot-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import {
BROUGHT_BOT_ASKING_GUIDANCE,
COMPONENT_GUIDANCE,
computerGuidance,
DOCUMENT_GUIDANCE,
HOW_TO_WORK_GUIDANCE,
howToWorkGuidance,
ORCHESTRATION_GUIDANCE,
PROVENANCE_GUIDANCE,
promptSection,
REPLY_GUIDANCE,
readsBack,
runModeGuidance,
workingGuidance,
} from "./bot-prompt";
import { RESEARCH_APP_NAMES } from "./research";

Expand Down Expand Up @@ -137,6 +142,23 @@ describe("computerGuidance", () => {
expect(shouted).toEqual([]);
});

test("says what the computer has to run a script with, when the deployment says", () => {
const guidance = flat(
computerGuidance({
search: true,
installed: "Node.js, Bun, curl and unzip",
}),
);
expect(guidance).toContain(
"your computer runs ubuntu linux and has node.js, bun, curl and unzip installed",
);
expect(guidance).toContain(
"never ask the person to approve a script for a program your computer does not have",
);
// A computer nobody here built is not described.
expect(flat(WITH_SEARCH)).not.toContain("installed");
});

test("a policy refusal leaves that action undone and the rest of the task going", () => {
// It used to say "and stop", which a Bot read as stopping the whole task.
const guidance = flat(WITH_SEARCH);
Expand All @@ -156,7 +178,11 @@ describe("how a Bot asks, and how an unattended run ends", () => {
expect(said).toContain("a wrong guess would be hard to undo");
expect(said).toContain("lead to materially different work");
expect(said).toContain("only they know the answer");
expect(said).toContain("say what you assumed, and keep going");
// The else-branch carries on with what was asked, and no further.
expect(said).toContain(
"say what you assumed, and carry on with what was asked",
);
expect(said).not.toContain("keep going");
// The open-ended cues that pulled a routine toward questions instead of an answer.
expect(said).not.toContain("theirs to make");
expect(said).not.toContain("prefer this to guessing");
Expand All @@ -172,6 +198,136 @@ describe("how a Bot asks, and how an unattended run ends", () => {
expect(flat(ASKING_GUIDANCE)).not.toContain("tools of your own");
});

test("how to work acts on what was asked, waits for a tool search, and retries by kind", () => {
const said = flat(HOW_TO_WORK_GUIDANCE);
expect(said).toContain("do what was asked, at the scope it was asked");
expect(said).toContain(
"a question gets its answer, not the job it is about",
);
// A tool search that has not come back is not an answer that the tool is missing.
expect(said).toContain(
"wait for what it returns before you decide, or say, that something cannot be done",
);
expect(said).toContain("a step the person declined stays declined");
expect(said).toContain(
"never repeat a change only because you are not sure it went through",
);
expect(said).toContain(
"look in the app first and redo only what is missing",
);
expect(said).toContain("stop and say briefly what failed");
});

test("a claim needs a tool result, and an app change is read back once before it is called done", () => {
const said = flat(HOW_TO_WORK_GUIDANCE);
expect(said).toContain(
"say something is done, sent, saved or correct only when a tool result in this conversation showed it",
);
expect(said).toContain(
"after you change something in an app, read the result back once before you say it is done",
);
// Not a second read of what the change's own result already showed: on GPT-6.1 a Bot that
// posted a Slack message then searched for it, read the channel twice and fetched its link.
expect(said).toContain(
"unless the change's own result already shows what changed, such as a sent message with its link",
);
expect(said).toContain("say 'all' only if you checked every item");
// Never the general plea Anthropic says makes its newest models over-verify.
expect(said).not.toContain("double-check");
expect(said).not.toContain("verify your work");
});

test("the read-back is left out for a model that reads its own work back", () => {
for (const model of ["claude-opus-5-5", "claude-opus-5", "claude-opus-6"]) {
expect(readsBack(model)).toBe(false);
const said = flat(howToWorkGuidance(model));
expect(said).not.toContain("read the result back once");
// The claims rule stays for every model.
expect(said).toContain(
"only when a tool result in this conversation showed it",
);
}
for (const model of [
undefined,
"claude-sonnet-5-5",
"claude-haiku-5-5",
"claude-opus-4-8",
"gpt-6.1-sol",
"gpt-5.6-terra",
"gemini-3.8-flash",
]) {
expect(readsBack(model)).toBe(true);
expect(flat(howToWorkGuidance(model))).toContain(
"read the result back once",
);
}
const opus = workingGuidance({ untrusted: "x", model: "claude-opus-5-5" });
expect(flat(opus.join(" "))).not.toContain("read the result back once");
});

test("a reply is sized to the question, and still draws what the answer calls for", () => {
const said = flat(REPLY_GUIDANCE);
expect(said).toContain("lead with the answer");
expect(said).toContain("a yes-or-no question gets yes or no");
// A question about ability gets an answer and an offer, not the work or a capability list.
expect(said).toContain("a question about whether you can do something");
expect(said).toContain("an offer to do it");
// Answered from the tools it holds, not from memory: the Smartlead tools cannot write a
// sequence, and a Bot that never looked said yes.
expect(said).toContain(
"answer from the tools you hold, searching for one first",
);
expect(said).toContain("say plainly what they cannot do");
expect(said).toContain("do not do the work");
expect(said).toContain("list everything you can do");
// A request worded as a question is still a request.
expect(said).toContain("a request worded as a question");
expect(said).toContain("is a request: do it");
expect(said).toContain("in one or two sentences");
expect(said).toContain("do not recap your steps, repeat a summary");
expect(said).toContain("do not write a message for each step");
expect(said).toContain("make sense on its own");
// Generative UI is untouched by the length rule.
expect(said).toContain(
"a chart, table or card the answer calls for under the rules above is still drawn",
);
});

test("a spreadsheet gets live formulas, labelled inputs, units, formats, one tab and a read-back", () => {
const said = flat(DOCUMENT_GUIDANCE);
expect(said).toContain("live formulas");
expect(said).toContain("own labelled cell");
expect(said).toContain(
"never numbers you calculated yourself and typed in",
);
expect(said).toContain("units in the headers");
expect(said).toContain("header row bold and freeze it");
expect(said).toContain("percentages, currencies and dates");
expect(said).toContain("keep a small task to one tab");
expect(said).toContain("read back the tabs and cells you wrote");
expect(said).toContain("#ref!");
expect(said).toContain("no filler sections");
});

test("the push to act is said once, and every Bot gets the sections in order", () => {
for (const brought of [false, true]) {
const sections = workingGuidance({ brought, untrusted: "Untrusted." });
const all = flat(sections.join(" "));
expect(all.split("take it in the same reply").length - 1).toBe(1);
expect(all).not.toContain("keep going");
expect(sections.map((section) => /^<(\w+)>/.exec(section)?.[1])).toEqual([
"how_to_work",
"asking",
"sources",
"answer_format",
"replies",
"documents",
"subagents",
"untrusted_content",
]);
}
});

test("how to work: reuse, batch a few reads, act instead of announcing, retry once, deliver", () => {
const said = flat(HOW_TO_WORK_GUIDANCE);
expect(said).toContain("use what you already have first");
Expand Down Expand Up @@ -234,9 +390,11 @@ describe("how a Bot asks, and how an unattended run ends", () => {
});

test("no working rule shouts", () => {
// "ID" is an acronym, not emphasis.
// "ID" is an acronym, not emphasis, and a spreadsheet's error codes (#REF!) are what it shows.
for (const guidance of [
HOW_TO_WORK_GUIDANCE,
REPLY_GUIDANCE,
DOCUMENT_GUIDANCE,
ASKING_GUIDANCE,
BROUGHT_BOT_ASKING_GUIDANCE,
PROVENANCE_GUIDANCE,
Expand All @@ -246,9 +404,9 @@ describe("how a Bot asks, and how an unattended run ends", () => {
runModeGuidance("check-in"),
])
expect(
(guidance.match(/\b[A-Z]{2,}\b/g) ?? []).filter(
(word) => word !== "ID",
),
(
guidance.replace(/#[A-Z]+[!?]/g, "").match(/\b[A-Z]{2,}\b/g) ?? []
).filter((word) => word !== "ID"),
).toEqual([]);
});
});
Loading
Loading