A coordinator asked to set an uploaded logo as a workspace picture could not: the picture tools take a media id, and nothing ever gave the bot one. Attachments reached the model as "[Attached image \"logo.png\"]", every file tool answers with a [file:ID] ref, and an SVG is deliberately stored as a document rather than an image. - copyMediaAsPicture copies an approved image into a new picture row (bot-image/workspace-image). The source keeps its row and bytes, so a message that attached it still loads and changing a picture can never take a file away from a conversation. - grabPictureSource resolves a picture by [file:ID] (the same scope gate every file tool uses) or by the user's own upload; parseFileRef reads the [file:12] form. - The new set_picture tool ships on every run: a bot sets its own face, the coordinator can also dress a worker. update_workspace gained picture_file_id, and setBotPicture/setProfileImage share the copy and the delete. - removeReplacedPicture deletes a replaced picture only when it really is one, never a chat upload. - uploadAndRecordMedia writes images in their own format: PNG stays PNG with its transparency, WebP stays WebP, GIF becomes PNG, everything else JPEG, and the row records the real mime type (it used to say jpeg for every image). - Pictures change live: BotUpdatedData carries image_media_id and a new server:workspace-updated pushes workspace pictures, relayed between processes. - Attachments are labelled with their media id, and BotFilesNote tells a bot that a [file:ID] can become a picture. - grabRunScopes is now the single source of a run's reachable folders, shared by the file tools, imports and the picture tools.
90 lines
3.9 KiB
TypeScript
90 lines
3.9 KiB
TypeScript
import { z } from "zod";
|
|
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
|
import { AppData } from "@/src/data/app-data";
|
|
import grabCrewWorkers from "@/src/functions/backend/db/bots/grab-crew-workers";
|
|
import updateBot from "@/src/functions/backend/db/bots/update-bot";
|
|
import { paletteColor, resolveBotStone } from "@/src/utils/bot-color";
|
|
import type { HarnessTool } from "../../types";
|
|
|
|
type Params = {
|
|
user_id: number | string;
|
|
/**
|
|
* The user's worker bots.
|
|
*/
|
|
workers: BUN_SQLITE_COBBLE_BOTS[];
|
|
};
|
|
|
|
type Args = {
|
|
bot_id: number;
|
|
name?: string;
|
|
role?: string;
|
|
instructions?: string;
|
|
};
|
|
|
|
/**
|
|
* Lets the coordinator change an existing crew member: its name, its
|
|
* one-line job, and its core instructions (the start of its prompt in its
|
|
* own chat and in every workspace). Its colour and stone stay as they are,
|
|
* and its picture is set with `set_picture` (`set_picture_tool.ts`), which
|
|
* also lets a bot dress itself.
|
|
* Workspace-only instructions have their own tool
|
|
* (`set_bot_workspace_instructions`).
|
|
*/
|
|
export default function updateBotTool({ user_id, workers }: Params): HarnessTool<Args> {
|
|
const crew = workers.map((bot) => `${bot.id}: ${bot.name}`).join("; ");
|
|
|
|
return {
|
|
name: "update_bot",
|
|
description: `Change an existing crew member: name, the one-line role it's shown with, and/or its core instructions (markdown: what they do and how they work, the start of their prompt in their own chat and in every workspace). Only the fields you pass change. Instructions replace what they had, so keep what still applies, their own edits included. This is not their instructions for one workspace: use set_bot_workspace_instructions for that. Crew: ${crew || "none"}.`,
|
|
schema: z
|
|
.object({
|
|
bot_id: z.number().int(),
|
|
name: z.string().trim().min(1).max(40).optional(),
|
|
role: z.string().trim().max(120).optional(),
|
|
instructions: z.string().trim().min(1).max(AppData.PromptMaxLength).optional(),
|
|
})
|
|
.refine(
|
|
(args) =>
|
|
args.name !== undefined ||
|
|
args.role !== undefined ||
|
|
args.instructions !== undefined,
|
|
"Give the name, role or instructions to change.",
|
|
),
|
|
run: async ({ args }) => {
|
|
const fresh = await grabCrewWorkers();
|
|
const worker = fresh.find((bot) => bot.id == args.bot_id);
|
|
|
|
if (!worker?.id) {
|
|
throw new Error(
|
|
`No crew member has id ${args.bot_id}. Crew: ${fresh
|
|
.map((bot) => `${bot.id}: ${bot.name}`)
|
|
.join("; ") || "none"}.`,
|
|
);
|
|
}
|
|
|
|
const changed = [
|
|
args.name !== undefined ? "name" : "",
|
|
args.role !== undefined ? "role" : "",
|
|
args.instructions !== undefined ? "core instructions" : "",
|
|
].filter(Boolean);
|
|
|
|
const saved = await updateBot({
|
|
user_id,
|
|
bot_id: worker.id,
|
|
name: args.name ?? worker.name ?? "Bot",
|
|
role: args.role ?? worker.role ?? "",
|
|
/**
|
|
* Never changed here: the user's pick is theirs. A bot saved
|
|
* before colours were free keeps the colour it showed.
|
|
*/
|
|
color:
|
|
worker.color || paletteColor({ hue: worker.hue }) || "#e4572e",
|
|
stone: resolveBotStone({ stone: worker.stone, hue: worker.hue }),
|
|
system_prompt: args.instructions ?? worker.system_prompt ?? "",
|
|
});
|
|
|
|
return `${saved.name}'s ${changed.join(", ")} saved.${args.instructions !== undefined ? " The instructions apply from its next run, in its own chat and in every workspace." : ""}`;
|
|
},
|
|
};
|
|
}
|