Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e8e8de976 | ||
|
|
e75ab28041 | ||
|
|
4261535741 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cobble",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import {
|
||||
ActionCheckNote,
|
||||
BotFilesNote,
|
||||
ChatReplyNote,
|
||||
botFilesNote,
|
||||
chatReplyNote,
|
||||
ComputerFilesNote,
|
||||
CoordinatorReplyNote,
|
||||
coordinatorReplyNote,
|
||||
CoordinatorSoloNote,
|
||||
TasksNote,
|
||||
TaskReminderNote,
|
||||
@@ -138,7 +138,7 @@ export default async function runBotReply({
|
||||
bot.kind != "coordinator"
|
||||
? ""
|
||||
: tools.some((tool) => tool.name == "delegate_task")
|
||||
? CoordinatorReplyNote
|
||||
? coordinatorReplyNote({ bot_id: bot.id })
|
||||
: CoordinatorSoloNote;
|
||||
|
||||
try {
|
||||
@@ -146,8 +146,8 @@ export default async function runBotReply({
|
||||
system: [
|
||||
prompt,
|
||||
carry_over,
|
||||
ChatReplyNote,
|
||||
BotFilesNote,
|
||||
chatReplyNote({ bot_id: bot.id }),
|
||||
botFilesNote({ bot_id: bot.id }),
|
||||
ComputerFilesNote({ computer_id: computers[0]?.id || 0, can_import: false }),
|
||||
TasksNote,
|
||||
TaskReminderNote,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import {
|
||||
ActionCheckNote,
|
||||
BotFilesNote,
|
||||
botFilesNote,
|
||||
ComputerFilesNote,
|
||||
ScheduledRunNote,
|
||||
TasksNote,
|
||||
@@ -191,7 +191,7 @@ export default async function runBotTask({
|
||||
carry_over,
|
||||
TaskRunNote,
|
||||
scheduled ? ScheduledRunNote : "",
|
||||
BotFilesNote,
|
||||
botFilesNote({ bot_id: bot.id }),
|
||||
ComputerFilesNote({ computer_id: job.computer_id || 0 }),
|
||||
TasksNote,
|
||||
ActionCheckNote,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
botFilesNote,
|
||||
chatReplyNote,
|
||||
ComputerFilesNote,
|
||||
coordinatorReplyNote,
|
||||
TasksNote,
|
||||
} from "./harness-prompts";
|
||||
|
||||
/**
|
||||
* Every note a bot reads names it by its own id, so a bot copying an
|
||||
* example never points at another bot's folder, conversation or task.
|
||||
*/
|
||||
const bot_ids = [1, 5, 42] as const;
|
||||
|
||||
/**
|
||||
* The bot ids a note's own-folder examples point at, in order.
|
||||
*/
|
||||
function folderOwners(note: string) {
|
||||
return [...note.matchAll(/\[folder:bot:(\d+):/g)].map((match) => match[1]);
|
||||
}
|
||||
|
||||
describe("harness prompts", () => {
|
||||
for (const bot_id of bot_ids) {
|
||||
test(`chat reply note names bot ${bot_id}'s own folder`, () => {
|
||||
const owners = folderOwners(chatReplyNote({ bot_id }));
|
||||
|
||||
expect(owners).toEqual([String(bot_id)]);
|
||||
});
|
||||
|
||||
test(`files note names bot ${bot_id}'s own folder`, () => {
|
||||
const owners = folderOwners(botFilesNote({ bot_id }));
|
||||
|
||||
expect(owners).toEqual([String(bot_id)]);
|
||||
});
|
||||
|
||||
test(`coordinator note names itself as bot ${bot_id}`, () => {
|
||||
const note = coordinatorReplyNote({ bot_id });
|
||||
|
||||
expect(note).toContain(`[bot:${bot_id}]`);
|
||||
expect(note).not.toContain("[bot:3]");
|
||||
});
|
||||
}
|
||||
|
||||
test("a note that already names the running bot is unchanged by its id", () => {
|
||||
expect(botFilesNote({ bot_id: 5 })).toContain("[folder:bot:5:reports]");
|
||||
});
|
||||
|
||||
test("the computer files note keeps naming the computer it was given", () => {
|
||||
expect(ComputerFilesNote({ computer_id: 8 })).toContain("[folder:computer:8:seo/coderank/raw]");
|
||||
});
|
||||
|
||||
test("notes with nothing bot-specific carry no ref example", () => {
|
||||
expect(TasksNote).not.toContain("[folder:bot:");
|
||||
expect(TasksNote).not.toContain("[bot:");
|
||||
});
|
||||
});
|
||||
@@ -19,9 +19,19 @@ export const ReadImagePrompt = `Describe this image for someone who can't see it
|
||||
export const MediaReadNote = `Some attachments in this conversation were read for you by another model; you see its reading in [brackets] instead of the file.`;
|
||||
|
||||
/**
|
||||
* Added to a bot's prompt when it answers the user in chat.
|
||||
* The folder-ref example a note shows, always over the running bot's own
|
||||
* folder: a literal owner id in the prompt teaches the bot to name another
|
||||
* bot's folder as if it were its own.
|
||||
*/
|
||||
export const ChatReplyNote = `# Replying in chat
|
||||
function ownFolderRef({ bot_id }: { bot_id: number | string }) {
|
||||
return folderRef({ volume: "bot", owner_id: bot_id, path: "reports" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Added to a bot's prompt when it answers the user in chat. `bot_id` is the
|
||||
* bot itself: the examples it reads are its own refs.
|
||||
*/
|
||||
export const chatReplyNote = ({ bot_id }: { bot_id: number | string }) => `# Replying in chat
|
||||
|
||||
You are chatting with the user. Answer their latest messages directly and briefly, in markdown.
|
||||
|
||||
@@ -33,7 +43,7 @@ You are chatting with the user. Answer their latest messages directly and briefl
|
||||
- To add a worker to the crew, use create_bot once the user has asked for it or agreed, and add_bot_to_workspace to place it. A bot exists only when create_bot made it: never describe one as if it did, and never write it into a workspace's context.
|
||||
- Never claim you did something you didn't do.
|
||||
- The user can mention a bot as [bot:ID] and a workspace as [workspace:ID]; a [Mentions: ...] line after their message says who and what those are. Write the same refs when you name them back.
|
||||
- Documents, images and reports you make are files: save them with write_file, make_pdf or generate_image, and send one to the user with it ("attach"). When you name a file, write its [file:ID] ref, e.g. [file:12], so the user can open it. When you name a folder, write a folder ref instead, e.g. ${folderRef({ volume: "bot", owner_id: 5, path: "reports" })}: that opens the folder in Files and downloads nothing.
|
||||
- Documents, images and reports you make are files: save them with write_file, make_pdf or generate_image, and send one to the user with it ("attach"). When you name a file, write its [file:ID] ref, e.g. [file:12], so the user can open it. When you name a folder, write a folder ref instead, e.g. ${ownFolderRef({ bot_id })}: that opens the folder in Files and downloads nothing.
|
||||
|
||||
## Keep the work moving
|
||||
|
||||
@@ -41,9 +51,10 @@ Be proactive. Follow where the conversation is heading and, once you've answered
|
||||
|
||||
/**
|
||||
* Added to every run, chat replies and tasks alike: where its files live,
|
||||
* that they persist, and how to search them.
|
||||
* that they persist, and how to search them. `bot_id` is the bot itself,
|
||||
* so the folder-ref example it reads is its own folder.
|
||||
*/
|
||||
export const BotFilesNote = `# Your files
|
||||
export const botFilesNote = ({ bot_id }: { bot_id: number | string }) => `# Your files
|
||||
|
||||
You have a folder of your own, kept with the app (not on a computer), and it stays between runs and conversations.
|
||||
|
||||
@@ -53,7 +64,7 @@ You have a folder of your own, kept with the app (not on a computer), and it sta
|
||||
- A picture can be set from a file: set_picture makes a file your own avatar, and update_workspace sets a workspace's. Both take the [file:ID] ref, so a logo the user sent you is usually ready to go: list_files for it, then set it. An SVG or another kind of file is turned into a PNG first. Its picture is free to change and never removes your file.
|
||||
- A file and a folder are two different refs, and you never guess which one from the name:
|
||||
- A file you saved is a [file:ID] ref (from write_file, list_files, search_files or import_file). The user sees a chip that opens the file.
|
||||
- A folder is a folder ref, e.g. ${folderRef({ volume: "bot", owner_id: 5, path: "reports" })}: the user sees a chip that opens that folder in Files, and nothing is downloaded. Use the volume of the folder you mean ("bot" for your own, "workspace" for a workspace's shared one).
|
||||
- A folder is a folder ref, e.g. ${ownFolderRef({ bot_id })}: the user sees a chip that opens that folder in Files, and nothing is downloaded. Use the volume of the folder you mean ("bot" for your own, "workspace" for a workspace's shared one).
|
||||
- Never write a path as plain text and expect the user to find it; never write a [file:ID] ref for something that is a folder.
|
||||
- A file you only described is not saved. Save it first, then say it's saved.
|
||||
- Save what the user will want before the long tail of a run. A step limit ends a run where it stands: write the report first with write_file (or copy it off the computer with import_file), then carry on verifying.
|
||||
@@ -114,8 +125,10 @@ Every action counts here: saving memory or instructions, creating or changing a
|
||||
|
||||
/**
|
||||
* Added to the coordinator's chat prompt: it hands work to the crew.
|
||||
* `bot_id` is the coordinator itself, so the ref example it copies is its
|
||||
* own rather than a worker's.
|
||||
*/
|
||||
export const CoordinatorReplyNote = `As the coordinator, hand work that fits a crew member's role to them with delegate_task. They post the result in their own conversation, and it's reported back here when they finish or fail, so tell the user who is on it and where: a task with no workspace_id runs in that bot's own chat, on its own memory, files and directly assigned computers; one with workspace_id runs in the workspace, on the workspace's context, files and computers as well as the bot's, and lands in that project's conversation. Never swap one for the other without saying so. When a report comes back, pass on what matters to the user, follow up on anything left, and propose what comes next: the next piece of work, which bot would take it, or a question that settles the direction. Once the user agrees, you can also grow the crew with create_bot, put a worker in a workspace with add_bot_to_workspace, and create or change a workspace with create_workspace and update_workspace. When you name a crew member, write its ref instead of its name, e.g. ${botRef({ id: 3 })}: the user sees it as the bot's name, linked to its conversation.`;
|
||||
export const coordinatorReplyNote = ({ bot_id }: { bot_id: number | string }) => `As the coordinator, hand work that fits a crew member's role to them with delegate_task. They post the result in their own conversation, and it's reported back here when they finish or fail, so tell the user who is on it and where: a task with no workspace_id runs in that bot's own chat, on its own memory, files and directly assigned computers; one with workspace_id runs in the workspace, on the workspace's context, files and computers as well as the bot's, and lands in that project's conversation. Never swap one for the other without saying so. When a report comes back, pass on what matters to the user, follow up on anything left, and propose what comes next: the next piece of work, which bot would take it, or a question that settles the direction. Once the user agrees, you can also grow the crew with create_bot, put a worker in a workspace with add_bot_to_workspace, and create or change a workspace with create_workspace and update_workspace. When you name a crew member, write its ref instead of its name, e.g. ${botRef({ id: bot_id })} for yourself: the user sees it as the bot's name, linked to its conversation.`;
|
||||
|
||||
/**
|
||||
* Added to the coordinator's chat prompt instead while it has no workers,
|
||||
|
||||
@@ -19,10 +19,10 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The full prompt for one of the user's bots (`composeBotPrompt`): its
|
||||
* core instructions, the install-wide appendix, its memory, what it
|
||||
* remembers about this user, and in a workspace that workspace's context
|
||||
* and what the bot has for it.
|
||||
* The full prompt for one of the user's bots (`composeBotPrompt`): who it
|
||||
* is (its name and id), its core instructions, the install-wide appendix,
|
||||
* its memory, what it remembers about this user, and in a workspace that
|
||||
* workspace's context and what the bot has for it.
|
||||
*/
|
||||
export default async function grabBotPrompt({ bot_id, user_id, workspace_id }: Params) {
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
@@ -40,6 +40,7 @@ export default async function grabBotPrompt({ bot_id, user_id, workspace_id }: P
|
||||
}
|
||||
|
||||
return composeBotPrompt({
|
||||
bot: { id: bot.id, name: bot.name, kind: bot.kind, role: bot.role },
|
||||
system_prompt: bot.system_prompt,
|
||||
appendix: await grabSystemPromptAppend(),
|
||||
memory: bot.memory,
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function createTaskTool({
|
||||
name: "create_task",
|
||||
description: is_coordinator
|
||||
? `Add a task to a list: a worker's own (${crew || "no workers yet"}), a workspace's shared one (${places || "no workspaces yet"}), or your own (leave both ids out). Write the description as a brief: what to do, where, and how you'll know it's done. A workspace's tasks are visible to every bot working there.`
|
||||
: `Add a task to a list: ${workspace_id ? "this workspace's shared one (leave the ids out), or your own (bot_id: " + bot_id + ")" : "your own (leave the ids out)"}. Write the description as a brief: what to do, where, and how you'll know it's done.`,
|
||||
: `Add a task to a list: ${workspace_id ? `this workspace's shared one (leave the ids out), or your own (bot_id: ${bot_id})` : `your own, bot_id ${bot_id} (or leave the id out, since it defaults to yours)`}. Write the description as a brief: what to do, where, and how you'll know it's done.`,
|
||||
schema: z.object({
|
||||
title: z.string().trim().min(1).max(AppData.TaskTitleMaxLength).describe(`A short label`),
|
||||
description: z
|
||||
@@ -111,7 +111,7 @@ export default function createTaskTool({
|
||||
bot_id: z.number().int().optional().describe(
|
||||
is_coordinator
|
||||
? `Whose own list, when it isn't a workspace's. Leave out for your own.`
|
||||
: `Your own id (${bot_id}), to keep it in your list rather than the workspace's.`,
|
||||
: `Your own id (${bot_id}), to keep it in your list rather than the workspace's; leaving it out has the same effect here.`,
|
||||
),
|
||||
workspace_id: z
|
||||
.number()
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
|
||||
import type { BotComputers } from "../(hooks)/use-bot-computers";
|
||||
import { BotComputersContext } from "../(data)/bot-computers-context";
|
||||
import ComputerRefChip from "./computer-ref-chip";
|
||||
|
||||
const main: BUN_SQLITE_COBBLE_COMPUTERS = { id: 1, name: "Main computer" };
|
||||
const sandbox: BUN_SQLITE_COBBLE_COMPUTERS = { id: 2, name: "SEO box" };
|
||||
|
||||
type RenderProps = {
|
||||
computer_id: number;
|
||||
computers: BUN_SQLITE_COBBLE_COMPUTERS[];
|
||||
};
|
||||
|
||||
function render({ computer_id, computers }: RenderProps) {
|
||||
const value = makeComputers({ computers });
|
||||
|
||||
const html = renderToString(
|
||||
<BotComputersContext.Provider value={value}>
|
||||
<ComputerRefChip computer_id={computer_id} />
|
||||
</BotComputersContext.Provider>,
|
||||
);
|
||||
|
||||
return { html, value };
|
||||
}
|
||||
|
||||
type MakeComputersProps = {
|
||||
computers: BUN_SQLITE_COBBLE_COMPUTERS[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A computers context recording what the chip asked for: the viewer it
|
||||
* opened and the switches it made.
|
||||
*/
|
||||
function makeComputers({ computers }: MakeComputersProps) {
|
||||
const switches: number[] = [];
|
||||
const opened: { computer_id: number; mode: string }[] = [];
|
||||
|
||||
const value: BotComputers = {
|
||||
computers,
|
||||
switchComputer: async ({ computer_id }) => {
|
||||
switches.push(computer_id);
|
||||
},
|
||||
openComputer: (params) => {
|
||||
opened.push(params);
|
||||
},
|
||||
closeComputer: () => {},
|
||||
};
|
||||
|
||||
return Object.assign(value, { switches, opened });
|
||||
}
|
||||
|
||||
describe("ComputerRefChip", () => {
|
||||
test("opens the viewer in its browser and switches nothing", async () => {
|
||||
const { html, value } = render({
|
||||
computer_id: 2,
|
||||
computers: [main, sandbox],
|
||||
});
|
||||
|
||||
expect(html).toContain("SEO box");
|
||||
// The chip's only job is opening, so no switch affordance survives.
|
||||
expect(html).not.toContain("Switch to");
|
||||
expect(value.switches).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("the computer the bot is on is still a chip, sunken with a check", () => {
|
||||
const { html } = render({ computer_id: 1, computers: [main, sandbox] });
|
||||
|
||||
expect(html).toContain("Main computer");
|
||||
expect(html).toContain("<button");
|
||||
expect(html).toContain("bg-sunken");
|
||||
expect(html).toContain('title="Open Main computer"');
|
||||
});
|
||||
|
||||
test("a computer the bot can't use stays a plain name", () => {
|
||||
const { html } = render({ computer_id: 9, computers: [main] });
|
||||
|
||||
expect(html).toContain("computer 9");
|
||||
expect(html).not.toContain("<button");
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { useContext } from "react";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { Check, Monitor } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { BotComputersContext } from "../(data)/bot-computers-context";
|
||||
|
||||
type Props = {
|
||||
@@ -9,33 +10,22 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A computer the bot named in a message (`computerRef`): a button that
|
||||
* switches the bot to it at once, or a checked chip when it's already on
|
||||
* it. A computer the bot can't use (any more) is shown by name only.
|
||||
* A computer the bot named in a message (`computerRef`): a chip that opens
|
||||
* it in the viewer, in its browser. The one the bot is already on says so,
|
||||
* sunken with a check. A computer the bot can't use (any more) is shown by
|
||||
* name only.
|
||||
*/
|
||||
export default function ComputerRefChip({ computer_id }: Props) {
|
||||
const { computers, switching_id, error, switchComputer } = useContext(BotComputersContext);
|
||||
const { computers, openComputer } = useContext(BotComputersContext);
|
||||
|
||||
const index = computers.findIndex((c) => c.id == computer_id);
|
||||
const computer = computers[index];
|
||||
const computer = computers.find((c) => c.id == computer_id);
|
||||
const name = computer?.name || `computer ${computer_id}`;
|
||||
|
||||
if (!computer) {
|
||||
if (!computer?.id) {
|
||||
return <Span className="font-bold">{name}</Span>;
|
||||
}
|
||||
|
||||
if (index == 0) {
|
||||
return (
|
||||
<Span
|
||||
title="The computer this bot is on"
|
||||
className="inline-flex h-6 gap-1.5 items-center px-2 align-middle rounded-default-xs bg-sunken text-[13px] font-bold leading-4 text-ink whitespace-nowrap"
|
||||
>
|
||||
<Monitor size={14} aria-hidden />
|
||||
{name}
|
||||
<Check size={14} aria-hidden />
|
||||
</Span>
|
||||
);
|
||||
}
|
||||
const current = computers[0]?.id == computer.id;
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -43,14 +33,20 @@ export default function ComputerRefChip({ computer_id }: Props) {
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
size="smaller"
|
||||
title={error && switching_id == undefined ? error : `Switch to ${name}`}
|
||||
loading={switching_id == computer_id}
|
||||
disabled={Boolean(switching_id)}
|
||||
onClick={() => switchComputer({ computer_id })}
|
||||
className="inline-flex h-6 gap-1.5! flex-nowrap align-middle px-2! text-[13px] font-bold text-ink whitespace-nowrap"
|
||||
title={`Open ${name}`}
|
||||
onClick={() =>
|
||||
openComputer({ computer_id: computer.id || 0, mode: "browser" })
|
||||
}
|
||||
className={twMerge(
|
||||
"inline-flex h-6 gap-1.5! flex-nowrap align-middle px-2! text-[13px] font-bold whitespace-nowrap text-ink dark:text-ink",
|
||||
current
|
||||
? "bg-sunken hover:bg-sunken outline-0"
|
||||
: "bg-transparent hover:bg-sunken",
|
||||
)}
|
||||
>
|
||||
<Monitor size={14} aria-hidden className="shrink-0" />
|
||||
{name}
|
||||
{current ? <Check size={14} aria-hidden className="shrink-0" /> : null}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,47 @@ import { describe, expect, test } from "bun:test";
|
||||
import composeBotPrompt from "./compose-bot-prompt";
|
||||
|
||||
describe("composeBotPrompt", () => {
|
||||
test("opens with who the bot is, its id included", () => {
|
||||
expect(composeBotPrompt({ bot: { id: 7, name: "Scout", kind: "worker", role: "Finds things" } })).toBe(
|
||||
[
|
||||
"# You are Scout",
|
||||
"You are a worker bot on this crew. Your role: Finds things.",
|
||||
"Your bot id is 7. That id is you: when a tool takes a bot id for the bot it is about (your own tasks, your own picture), this is it, and when you name yourself in a message write the ref `[bot:7]`, never another bot's.",
|
||||
].join("\n\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("knows the coordinator by its kind and keeps an unknown name", () => {
|
||||
expect(composeBotPrompt({ bot: { id: "1", kind: "coordinator" } })).toBe(
|
||||
[
|
||||
"# You are A bot",
|
||||
"You are the coordinator on this crew.",
|
||||
"Your bot id is 1. That id is you: when a tool takes a bot id for the bot it is about (your own tasks, your own picture), this is it, and when you name yourself in a message write the ref `[bot:1]`, never another bot's.",
|
||||
].join("\n\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("says nothing about identity when the caller gives no bot", () => {
|
||||
expect(composeBotPrompt({ bot: { id: null, name: "Scout" }, system_prompt: "Core." })).toBe(
|
||||
"Core.",
|
||||
);
|
||||
});
|
||||
|
||||
test("puts the identity above the core instructions", () => {
|
||||
expect(composeBotPrompt({ bot: { id: 4, name: "Scout" }, system_prompt: "Core." })).toBe(
|
||||
[
|
||||
"# You are Scout",
|
||||
"You are a worker bot on this crew.",
|
||||
"Your bot id is 4. That id is you: when a tool takes a bot id for the bot it is about (your own tasks, your own picture), this is it, and when you name yourself in a message write the ref `[bot:4]`, never another bot's.",
|
||||
"Core.",
|
||||
].join("\n\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("orders core instructions, the workspace, then memory", () => {
|
||||
expect(
|
||||
composeBotPrompt({
|
||||
bot: { id: 7, name: "Scout" },
|
||||
system_prompt: "# You are Scout\n\nFind things.",
|
||||
memory: "- Cite sources.",
|
||||
user_memory: "- The user prefers short replies.",
|
||||
@@ -20,6 +58,9 @@ describe("composeBotPrompt", () => {
|
||||
}),
|
||||
).toBe(
|
||||
[
|
||||
"# You are Scout",
|
||||
"You are a worker bot on this crew.",
|
||||
"Your bot id is 7. That id is you: when a tool takes a bot id for the bot it is about (your own tasks, your own picture), this is it, and when you name yourself in a message write the ref `[bot:7]`, never another bot's.",
|
||||
"# You are Scout\n\nFind things.",
|
||||
`# Workspace: Launch\n\nYou're working in the workspace "Launch". Follow its context and your instructions for it below, over your general habits where they differ.\n\nThe workspace's context, which the user sets:\n\nThe spring launch.\n\nShip by Friday.`,
|
||||
"# Your instructions in Launch\n\nOwn the changelog.",
|
||||
@@ -33,6 +74,7 @@ describe("composeBotPrompt", () => {
|
||||
test("puts the install-wide appendix right after the core instructions", () => {
|
||||
expect(
|
||||
composeBotPrompt({
|
||||
bot: { id: 4, name: "Scout" },
|
||||
system_prompt: "Core.",
|
||||
appendix: "Never invent sources.",
|
||||
workspace: { name: "Launch", description: "The spring launch." },
|
||||
@@ -40,6 +82,9 @@ describe("composeBotPrompt", () => {
|
||||
}),
|
||||
).toBe(
|
||||
[
|
||||
"# You are Scout",
|
||||
"You are a worker bot on this crew.",
|
||||
"Your bot id is 4. That id is you: when a tool takes a bot id for the bot it is about (your own tasks, your own picture), this is it, and when you name yourself in a message write the ref `[bot:4]`, never another bot's.",
|
||||
"Core.",
|
||||
"# Install-wide instructions\n\nNever invent sources.",
|
||||
`# Workspace: Launch\n\nYou're working in the workspace "Launch". Follow its context and your instructions for it below, over your general habits where they differ.\n\nThe workspace's context, which the user sets:\n\nThe spring launch.`,
|
||||
|
||||
@@ -9,7 +9,27 @@ type WorkspaceBotPrompt = {
|
||||
memory?: string | null;
|
||||
};
|
||||
|
||||
type BotPrompt = {
|
||||
/**
|
||||
* The bot's id: how it names itself, and how the user, the crew tools
|
||||
* and every ref in a message point at it.
|
||||
*/
|
||||
id?: number | string | null;
|
||||
name?: string | null;
|
||||
kind?: string | null;
|
||||
/**
|
||||
* The one-line role it does that job as, when it has one.
|
||||
*/
|
||||
role?: string | null;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The bot itself. Its id is always in the prompt: it is how the bot
|
||||
* names itself and how everything else (crew tools, refs, tasks,
|
||||
* files) points back at it, in chat replies, tasks and routines alike.
|
||||
*/
|
||||
bot?: BotPrompt | null;
|
||||
system_prompt?: string | null;
|
||||
/**
|
||||
* The install-wide text every bot gets, right after its own core
|
||||
@@ -32,14 +52,16 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The full prompt a bot runs with (markdown): its own core instructions,
|
||||
* then the install-wide appendix, then, in a workspace, the workspace's
|
||||
* The full prompt a bot runs with (markdown): who it is (its name and id,
|
||||
* so it always knows which bot it is), its own core instructions, then the
|
||||
* install-wide appendix, then, in a workspace, the workspace's
|
||||
* context and the bot's instructions there, then its core memory, its
|
||||
* memory about the user and its memory in the workspace (said to be the
|
||||
* bot's own notes, not the workspace's context). Empty parts are left out;
|
||||
* the workspace heading always shows, so the bot knows where it is.
|
||||
*/
|
||||
export default function composeBotPrompt({
|
||||
bot,
|
||||
system_prompt,
|
||||
appendix,
|
||||
memory,
|
||||
@@ -49,6 +71,11 @@ export default function composeBotPrompt({
|
||||
}: Params) {
|
||||
const parts: string[] = [];
|
||||
const name = workspace?.name?.trim() || "Untitled";
|
||||
const identity = composeIdentity({ bot });
|
||||
|
||||
if (identity) {
|
||||
parts.push(identity);
|
||||
}
|
||||
|
||||
if (system_prompt?.trim()) {
|
||||
parts.push(system_prompt.trim());
|
||||
@@ -95,3 +122,24 @@ export default function composeBotPrompt({
|
||||
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Who the bot is, said once at the top of its prompt: its name, kind and
|
||||
* id, and that the id is how it is named. Empty when the caller didn't say
|
||||
* which bot it is.
|
||||
*/
|
||||
function composeIdentity({ bot }: { bot?: BotPrompt | null }) {
|
||||
const id = bot?.id === null || bot?.id === undefined ? "" : String(bot.id).trim();
|
||||
|
||||
if (!id) return "";
|
||||
|
||||
const name = bot?.name?.trim() || "A bot";
|
||||
const kind = bot?.kind == "coordinator" ? "the coordinator" : "a worker bot";
|
||||
const role = bot?.role?.trim();
|
||||
|
||||
return [
|
||||
`# You are ${name}`,
|
||||
`You are ${kind} on this crew.${role ? ` Your role: ${role}.` : ""}`,
|
||||
`Your bot id is ${id}. That id is you: when a tool takes a bot id for the bot it is about (your own tasks, your own picture), this is it, and when you name yourself in a message write the ref \`[bot:${id}]\`, never another bot's.`,
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user