Give the coordinator working crew management tools

- Resolve the crew and workspaces fresh inside each tool run, so a bot or
  workspace created earlier in the same reply is usable: create_bot,
  assign_computer, add_bot_to_workspace, delegate_task and
  set_bot_workspace_instructions.
- Add coordinator-only update_bot, find_bot, find_workspace, delete_bot,
  delete_workspace and remove_bot_from_workspace tools.
- Push server:workspace-removed when a workspace is deleted, and
  workspace_ids on server:bot-updated so a workspace's crew drops a bot
  that was taken out of it.
This commit is contained in:
2026-10-19 20:34:51 +01:00
parent b79cebf397
commit 139b3e443d
20 changed files with 648 additions and 39 deletions
@@ -2,7 +2,9 @@ import BunSQLite from "@moduletrace/bun-sqlite";
import { z } from "zod";
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
import pushBotCreated from "@/src/bots-queue/push-bot-created";
import grabCrewWorkers from "@/src/functions/backend/db/bots/grab-crew-workers";
import addBotToWorkspace from "@/src/functions/backend/db/workspaces/add-bot-to-workspace";
import grabUserWorkspaces from "@/src/functions/backend/db/workspaces/grab-user-workspaces";
import type { HarnessTool } from "../../types";
type Params = {
@@ -44,16 +46,24 @@ export default function addBotToWorkspaceTool({
workspace_id: z.number().int(),
}),
run: async ({ args }) => {
const worker = workers.find((bot) => bot.id == args.bot_id);
const workspace = workspaces.find((w) => w.id == args.workspace_id);
const fresh = await grabCrewWorkers();
const live = (await grabUserWorkspaces({ user_id })).filter((w) => !w.archived);
const worker = fresh.find((bot) => bot.id == args.bot_id);
const workspace = live.find((w) => w.id == args.workspace_id);
if (!worker?.id) {
throw new Error(`No crew member has id ${args.bot_id}. Crew: ${crew || "none"}.`);
throw new Error(
`No crew member has id ${args.bot_id}. Crew: ${fresh
.map((bot) => `${bot.id}: ${bot.name}`)
.join("; ") || "none"}.`,
);
}
if (!workspace?.id) {
throw new Error(
`No workspace has id ${args.workspace_id}. Workspaces: ${list || "none"}.`,
`No workspace has id ${args.workspace_id}. Workspaces: ${
live.map((w) => `${w.id}: ${w.name}`).join("; ") || "none"
}.`,
);
}
@@ -1,5 +1,6 @@
import { z } from "zod";
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
import grabCrewWorkers from "@/src/functions/backend/db/bots/grab-crew-workers";
import addBotToComputer from "@/src/functions/backend/db/computers/add-bot-to-computer";
import removeBotFromComputer from "@/src/functions/backend/db/computers/remove-bot-from-computer";
import computerRef from "@/src/utils/computer-ref";
@@ -57,11 +58,16 @@ export default function assignComputerTool({
access: z.enum(["grant", "revoke"]),
}),
run: async ({ args }) => {
const worker = workers.find((bot) => bot.id == args.bot_id);
const fresh = await grabCrewWorkers();
const worker = fresh.find((bot) => bot.id == args.bot_id);
const computer = computers.find((c) => c.id == args.computer_id);
if (!worker?.id) {
throw new Error(`No crew member has id ${args.bot_id}.`);
throw new Error(
`No crew member has id ${args.bot_id}. Crew: ${fresh
.map((bot) => `${bot.id}: ${bot.name}`)
.join("; ") || "none"}.`,
);
}
if (!computer?.id) {
+1 -1
View File
@@ -96,7 +96,7 @@ export default function createBotTool({
const where = target?.name ? ` and joins ${target.name}` : "";
return `${args.name} is on the crew (bot #${bot_id})${where}.${target ? "" : " It has no workspace yet: put it in one with add_bot_to_workspace, or give it a computer with assign_computer."}`;
return `${args.name} is on the crew (bot #${bot_id})${where}.${target ? "" : " It has no workspace yet: put it in one with add_bot_to_workspace, or give it a computer with assign_computer."} assign_computer, add_bot_to_workspace and delegate_task take #${bot_id} right away, in this same reply.`;
},
};
}
+8 -2
View File
@@ -3,6 +3,7 @@ import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
import pushBotNotice from "@/src/bots-queue/push-bot-notice";
import enqueueBotJob from "@/src/functions/backend/db/bots-queue/enqueue-bot-job";
import insertBotMessage from "@/src/functions/backend/db/bots/insert-bot-message";
import grabCrewWorkers from "@/src/functions/backend/db/bots/grab-crew-workers";
import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-computers";
import ensureBotSession from "@/src/functions/backend/db/bot-sessions/ensure-bot-session";
import type { HarnessTool } from "../../types";
@@ -65,10 +66,15 @@ export default function delegateTaskTool({
instructions: z.string().min(1).max(8000),
}),
run: async ({ args }) => {
const worker = workers.find((bot) => bot.id == args.bot_id);
const fresh = await grabCrewWorkers({ workspace_id });
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: ${crew}.`);
throw new Error(
`No crew member has id ${args.bot_id}. Crew: ${fresh
.map((bot) => `${bot.id}: ${bot.name}`)
.join("; ") || "none"}.`,
);
}
const computers = await grabBotComputers({ bot_id: worker.id, workspace_id });
+70
View File
@@ -0,0 +1,70 @@
import { z } from "zod";
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
import pushBotRemoved from "@/src/bots-queue/push-bot-removed";
import grabBotAudience from "@/src/functions/backend/db/access/grab-bot-audience";
import grabCrewWorkers from "@/src/functions/backend/db/bots/grab-crew-workers";
import deleteBot from "@/src/functions/backend/db/bots/delete-bot";
import logEvent from "@/src/functions/backend/logger";
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;
};
/**
* Lets the coordinator delete a crew member with everything it had: its
* conversations in its own chat and in every workspace, its tasks, its
* schedules and its workspace places. Its workspaces and computers stay.
* Irreversible, so it only runs when the user asked for that exact bot to
* go.
*/
export default function deleteBotTool({ user_id, workers }: Params): HarnessTool<Args> {
const crew = workers.map((bot) => `${bot.id}: ${bot.name}`).join("; ");
return {
name: "delete_bot",
description: `Delete a crew member and everything it had: its conversations in its own chat and in every workspace, its tasks, its schedules and its places in workspaces. Its workspaces and computers stay. This can't be undone. Only do it when the user explicitly asked for this exact bot to be deleted, never as cleanup or to make room, and say what is going with it. Crew: ${crew || "none"}.`,
schema: z.object({
bot_id: z.number().int(),
}),
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"}.`,
);
}
/**
* Read before the delete: its assignments and workspace places
* go with it, so who saw it can't be worked out afterwards.
*/
const audience = await grabBotAudience({ bot_id: worker.id });
const deleted = await deleteBot({ user_id, bot_id: worker.id });
pushBotRemoved({ bot_id: worker.id, user_ids: audience });
await logEvent({
title: "Bot deleted",
category: "system",
user_id: Number(user_id),
plain_text: `Bot deleted the bot ${deleted.name}.`,
metadata: { bot_id: worker.id },
});
return `${deleted.name || worker.name} is deleted, with its conversations, tasks, schedules and workspace places. The workspaces and computers it used stay.`;
},
};
}
@@ -0,0 +1,72 @@
import { z } from "zod";
import type { BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
import pushWorkspaceRemoved from "@/src/bots-queue/push-workspace-removed";
import grabWorkspaceAudience from "@/src/functions/backend/db/access/grab-workspace-audience";
import deleteWorkspace from "@/src/functions/backend/db/workspaces/delete-workspace";
import grabUserWorkspaces from "@/src/functions/backend/db/workspaces/grab-user-workspaces";
import logEvent from "@/src/functions/backend/logger";
import type { HarnessTool } from "../../types";
type Params = {
user_id: number | string;
/**
* The user's live workspaces.
*/
workspaces: BUN_SQLITE_COBBLE_WORKSPACES[];
};
type Args = {
workspace_id: number;
};
/**
* Lets the coordinator delete a workspace with its conversations, tasks,
* schedules and links. Its bots and computers stay, just not in it any
* more. Irreversible, so it only runs when the user asked for that exact
* workspace to go.
*/
export default function deleteWorkspaceTool({
user_id,
workspaces,
}: Params): HarnessTool<Args> {
const list = workspaces.map((w) => `${w.id}: ${w.name}`).join("; ");
return {
name: "delete_workspace",
description: `Delete a workspace with its conversations, tasks, schedules and links to bots and computers. Its bots and computers stay; they're just not in it any more. This can't be undone. Only do it when the user explicitly asked for this exact workspace to be deleted, never as cleanup or to make room, and say what is going with it. Workspaces: ${list || "none"}.`,
schema: z.object({
workspace_id: z.number().int(),
}),
run: async ({ args }) => {
const live = (await grabUserWorkspaces({ user_id })).filter((w) => !w.archived);
const workspace = live.find((w) => w.id == args.workspace_id);
if (!workspace?.id) {
throw new Error(
`No workspace has id ${args.workspace_id}. Workspaces: ${
live.map((w) => `${w.id}: ${w.name}`).join("; ") || "none"
}.`,
);
}
/**
* Read before the delete: its links go with it, so who saw it
* can't be worked out afterwards.
*/
const audience = await grabWorkspaceAudience({ workspace_id: workspace.id });
const deleted = await deleteWorkspace({ user_id, workspace_id: workspace.id });
pushWorkspaceRemoved({ workspace_id: workspace.id, user_ids: audience });
await logEvent({
title: "Workspace deleted",
category: "system",
user_id: Number(user_id),
plain_text: `Bot deleted the workspace ${deleted.name}.`,
metadata: { workspace_id: workspace.id },
});
return `${deleted.name || workspace.name} is deleted, with its conversations, tasks, schedules and links. The bots and computers that were in it stay.`;
},
};
}
+58
View File
@@ -0,0 +1,58 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import { z } from "zod";
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
import type { HarnessTool } from "../../types";
type Args = {
query?: string;
limit?: number;
};
/**
* Lets the coordinator look up crew members in the install: by id, name,
* role or core instructions, so it can find the bot behind a job before
* changing or deleting it. Read fresh every call, so a bot another reply
* just created is already there.
*/
export default function findBotTool(): HarnessTool<Args> {
return {
name: "find_bot",
description: `Find crew members: by id, name, the one-line role, or words in their core instructions. Returns each match's id, name, kind and role. With no query it lists the crew, up to limit. The coordinator is listed too but can't be deleted.`,
schema: z.object({
query: z.string().trim().max(200).optional(),
limit: z.number().int().min(1).max(50).optional(),
}),
run: async ({ args }) => {
const all =
(
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
sql: `SELECT * FROM bots ORDER BY id ASC`,
values: [],
})
).payload || [];
const query = (args.query || "").trim().toLowerCase();
const matches = (
query
? all.filter((bot) =>
[bot.id ? String(bot.id) : "", bot.name, bot.role, bot.system_prompt].some(
(field) => (field || "").toLowerCase().includes(query),
),
)
: all
).slice(0, args.limit ?? 20);
if (!matches.length) {
return `No crew member matches "${args.query || ""}".`;
}
return matches
.map(
(bot) =>
`#${bot.id} ${bot.name} (${bot.kind}${bot.role ? `: ${bot.role}` : ""})`,
)
.join("\n");
},
};
}
@@ -0,0 +1,58 @@
import { z } from "zod";
import grabUserWorkspaces from "@/src/functions/backend/db/workspaces/grab-user-workspaces";
import type { HarnessTool } from "../../types";
type Params = {
user_id: number | string;
};
type Args = {
query?: string;
limit?: number;
};
/**
* Lets the coordinator look up the user's live workspaces: by id, name,
* description or shared context, so it can find the one to change or
* delete. Read fresh every call, so a workspace another reply just
* created is already there.
*/
export default function findWorkspaceTool({ user_id }: Params): HarnessTool<Args> {
return {
name: "find_workspace",
description: `Find a live workspace: by id, name, description, or words in its shared context. Returns each match's id, name and description. With no query it lists them, up to limit.`,
schema: z.object({
query: z.string().trim().max(200).optional(),
limit: z.number().int().min(1).max(50).optional(),
}),
run: async ({ args }) => {
const live = (await grabUserWorkspaces({ user_id })).filter((w) => !w.archived);
const query = (args.query || "").trim().toLowerCase();
const matches = (
query
? live.filter((workspace) =>
[
workspace.id ? String(workspace.id) : "",
workspace.name,
workspace.description,
workspace.system_prompt,
].some((field) => (field || "").toLowerCase().includes(query)),
)
: live
).slice(0, args.limit ?? 20);
if (!matches.length) {
return `No workspace matches "${args.query || ""}".`;
}
return matches
.map(
(workspace) =>
`#${workspace.id} ${workspace.name}${workspace.description ? ` - ${workspace.description}` : ""}`,
)
.join("\n");
},
};
}
+41 -20
View File
@@ -1,6 +1,6 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-computers";
import grabCrewWorkers from "@/src/functions/backend/db/bots/grab-crew-workers";
import type { HarnessTool } from "../../types";
import askToTakeOverTool from "../computer/ask-to-take-over-tool";
import searchComputersTool from "../computer/search-computers-tool";
@@ -9,7 +9,13 @@ import assignComputerTool from "./assign-computer-tool";
import createBotTool from "./create-bot-tool";
import createWorkspaceTool from "./create-workspace-tool";
import delegateTaskTool from "./delegate-task-tool";
import deleteBotTool from "./delete-bot-tool";
import deleteWorkspaceTool from "./delete-workspace-tool";
import findBotTool from "./find-bot-tool";
import findWorkspaceTool from "./find-workspace-tool";
import grabBotTools from "./grab-bot-tools";
import removeBotFromWorkspaceTool from "./remove-bot-from-workspace-tool";
import updateBotTool from "./update-bot-tool";
import updateWorkspaceTool from "./update-workspace-tool";
import grabUserWorkspaces from "@/src/functions/backend/db/workspaces/grab-user-workspaces";
import setWorkspaceInstructionsTool from "./set-workspace-instructions-tool";
@@ -49,10 +55,13 @@ type Params = {
* for the coordinator (the super admin's alone) create_workspace and
* update_workspace (a workspace and its shared context), create_bot to
* grow the crew, delegate_task to every worker (the workspace's in a
* workspace) plus, in its own chat, add_bot_to_workspace and
* assign_computer (which workspace and which computers each crew member
* gets) or, in a workspace, set_bot_workspace_instructions. Anything else
* on a computer goes through a task, never mid-reply.
* workspace) plus, in its own chat, find_bot and find_workspace,
* add_bot_to_workspace and remove_bot_from_workspace, assign_computer,
* update_bot, delete_bot and delete_workspace (which workspace, which
* computers, and which name, role and core instructions each crew member
* gets, and what goes) or, in a workspace,
* set_bot_workspace_instructions. Anything else on a computer goes through
* a task, never mid-reply.
*/
export default async function grabBotChatTools({
bot,
@@ -93,16 +102,28 @@ export default async function grabBotChatTools({
tools.push(createBotTool({ user_id, workspaces, workspace_id }));
const workers =
(
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
sql: workspace_id
? `SELECT b.* FROM bots b JOIN workspace_bots wb ON wb.bot_id = b.id AND wb.workspace_id = ?
WHERE b.kind = 'worker' ORDER BY b.id ASC`
: `SELECT * FROM bots WHERE kind = 'worker' ORDER BY id ASC`,
values: workspace_id ? [workspace_id] : [],
})
).payload || [];
const workers = await grabCrewWorkers({ workspace_id });
if (workspace_id) {
if (!workers.length) return tools;
tools.push(delegateTaskTool({
user_id,
coordinator_id: bot_id,
session_id,
workspace_id,
coordinator_name: bot.name || "The coordinator",
workers,
}));
tools.push(setWorkspaceInstructionsTool({ user_id, workspace_id, workers }));
return tools;
}
tools.push(findBotTool(), findWorkspaceTool({ user_id }));
if (workspaces.length) {
tools.push(deleteWorkspaceTool({ user_id, workspaces }));
}
if (!workers.length) return tools;
@@ -117,10 +138,7 @@ export default async function grabBotChatTools({
}),
);
if (workspace_id) {
tools.push(setWorkspaceInstructionsTool({ user_id, workspace_id, workers }));
return tools;
}
tools.push(updateBotTool({ user_id, workers }), deleteBotTool({ user_id, workers }));
if (computers.length) {
const access = Object.fromEntries(
@@ -139,7 +157,10 @@ export default async function grabBotChatTools({
}
if (workspaces.length) {
tools.push(addBotToWorkspaceTool({ user_id, workers, workspaces }));
tools.push(
addBotToWorkspaceTool({ user_id, workers, workspaces }),
removeBotFromWorkspaceTool({ user_id, workers, workspaces }),
);
}
return tools;
@@ -0,0 +1,103 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import { z } from "zod";
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
import pushBotUpdated from "@/src/bots-queue/push-bot-updated";
import grabCrewWorkers from "@/src/functions/backend/db/bots/grab-crew-workers";
import removeBotFromWorkspace from "@/src/functions/backend/db/workspaces/remove-bot-from-workspace";
import grabUserWorkspaces from "@/src/functions/backend/db/workspaces/grab-user-workspaces";
import type { HarnessTool } from "../../types";
type Params = {
user_id: number | string;
/**
* The user's worker bots.
*/
workers: BUN_SQLITE_COBBLE_BOTS[];
/**
* The user's live workspaces.
*/
workspaces: BUN_SQLITE_COBBLE_WORKSPACES[];
};
type Args = {
bot_id: number;
workspace_id: number;
};
/**
* Lets the coordinator take a crew member out of one of the user's
* workspaces. The bot stays on the crew, and its conversation in that
* workspace is kept for if it's put back. Its access through the
* workspace's computers ends with it.
*/
export default function removeBotFromWorkspaceTool({
user_id,
workers,
workspaces,
}: Params): HarnessTool<Args> {
const crew = workers.map((bot) => `${bot.id}: ${bot.name}`).join("; ");
const list = workspaces.map((w) => `${w.id}: ${w.name}`).join("; ");
return {
name: "remove_bot_from_workspace",
description: `Take a crew member out of one of the user's workspaces. The bot stays on the crew and can be put back with add_bot_to_workspace; its conversation there is kept. It loses that workspace's context, models and computers, and the user stops seeing it in the workspace. Crew: ${crew || "none"}. Workspaces: ${list || "none"}.`,
schema: z.object({
bot_id: z.number().int(),
workspace_id: z.number().int(),
}),
run: async ({ args }) => {
const fresh = await grabCrewWorkers();
const live = (await grabUserWorkspaces({ user_id })).filter((w) => !w.archived);
const worker = fresh.find((bot) => bot.id == args.bot_id);
const workspace = live.find((w) => w.id == args.workspace_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"}.`,
);
}
if (!workspace?.id) {
throw new Error(
`No workspace has id ${args.workspace_id}. Workspaces: ${
live.map((w) => `${w.id}: ${w.name}`).join("; ") || "none"
}.`,
);
}
const present =
(
await BunSQLite.sql<{ present: number }>({
sql: `SELECT 1 AS present FROM workspace_bots WHERE workspace_id = ? AND bot_id = ? LIMIT 1`,
values: [workspace.id, worker.id],
})
).payload?.[0] || null;
if (!present) {
return `${worker.name} isn't in ${workspace.name}.`;
}
await removeBotFromWorkspace({
user_id,
workspace_id: workspace.id,
bot_id: worker.id,
});
/** The workspaces it's in now, so its crew lists drop it at once. */
const workspace_ids = (
(
await BunSQLite.sql<{ workspace_id: number }>({
sql: `SELECT workspace_id FROM workspace_bots WHERE bot_id = ?`,
values: [worker.id],
})
).payload || []
).map((row) => Number(row.workspace_id));
await pushBotUpdated({ bot_id: worker.id, workspace_ids });
return `${worker.name} is out of ${workspace.name}. It's still on the crew, and its conversation there is kept for if it's put back. It loses that workspace's context, models and computers.`;
},
};
}
@@ -1,6 +1,7 @@
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 updateWorkspaceBot from "@/src/functions/backend/db/workspaces/update-workspace-bot";
import type { HarnessTool } from "../../types";
@@ -37,10 +38,15 @@ export default function setWorkspaceInstructionsTool({
instructions: z.string().trim().max(AppData.PromptMaxLength),
}),
run: async ({ args }) => {
const worker = workers.find((bot) => bot.id == args.bot_id);
const fresh = await grabCrewWorkers({ workspace_id });
const worker = fresh.find((bot) => bot.id == args.bot_id);
if (!worker?.id) {
throw new Error(`No crew member here has id ${args.bot_id}. Crew here: ${crew}.`);
throw new Error(
`No crew member here has id ${args.bot_id}. Crew here: ${fresh
.map((bot) => `${bot.id}: ${bot.name}`)
.join("; ") || "none"}.`,
);
}
await updateWorkspaceBot({
+81
View File
@@ -0,0 +1,81 @@
import { z } from "zod";
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import type { BotHue } from "@/src/dict/bot-dict";
import grabCrewWorkers from "@/src/functions/backend/db/bots/grab-crew-workers";
import updateBot from "@/src/functions/backend/db/bots/update-bot";
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 hue and picture stay as they are.
* 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 ?? "",
hue: (worker.hue || "ember") as BotHue,
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." : ""}`;
},
};
}