Add bot types with a permission matrix, and workspace admin bots
A bot's powers came from bots.kind alone, so only the coordinator could hand work to other bots. bot_types now works like user_types: a row with no workspace holds everywhere (the coordinator), one with a workspace only there (a workspace admin), and no row is a worker. bots.kind stays the legacy marker of the one coordinator for who sees it. The matrices in src/dict/bot-types-dict.ts say what each type may do, whom it may direct and who grants it, read by one generic interpreter (rbac/type-assignment.ts) that userTypeAssignment now wraps too. Every run reads its powers once (grabBotRunPowers) and the chat tools, task runs and task tools gate on them instead of kind; a run's powers are its type's and its user's together. In the workspace it leads, an admin hands tasks to the workers there (results relayed back to it), sets their instructions, adds tasks to their lists and keeps the workspace's context when its user may. Super admins and admins grant it from the workspace's settings or the chat header's menu, and the coordinator with set_bot_type. Admins are marked with a filled green shield beside their name across the workspace, kept live by pushBotUpdated's admin_workspace_ids. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -13,10 +13,10 @@ function coordinatorTool({
|
||||
}: { workspace_id?: number; workers?: BUN_SQLITE_COBBLE_BOTS[]; workspaces?: BUN_SQLITE_COBBLE_WORKSPACES[] } = {}) {
|
||||
return delegateTaskTool({
|
||||
user_id: 1,
|
||||
coordinator_id: 1,
|
||||
requester_id: 1,
|
||||
session_id: 10,
|
||||
workspace_id: workspace_id ?? null,
|
||||
coordinator_name: "Coordinator",
|
||||
requester_name: "Coordinator",
|
||||
workers,
|
||||
workspaces,
|
||||
});
|
||||
@@ -69,4 +69,20 @@ describe("delegateTaskTool", () => {
|
||||
}),
|
||||
).rejects.not.toThrow(/runs in that workspace/);
|
||||
});
|
||||
|
||||
test("a workspace admin can only hand work to the bots it may direct", async () => {
|
||||
const tool = delegateTaskTool({
|
||||
user_id: 1,
|
||||
requester_id: 7,
|
||||
session_id: 10,
|
||||
workspace_id: 3,
|
||||
requester_name: "Lead",
|
||||
workers: [worker],
|
||||
grabTargets: async () => [worker],
|
||||
});
|
||||
|
||||
await expect(
|
||||
tool.run({ args: { bot_id: 8, title: "Dig", instructions: "x" }, signal }),
|
||||
).rejects.toThrow(/No crew member has id 8 in workspace 3. Crew: 5: Researcher/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,26 +16,33 @@ import type { HarnessTool } from "../../types";
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
/**
|
||||
* The coordinator: the worker's result comes back to its conversation.
|
||||
* The bot handing the work over (the coordinator, or a workspace's
|
||||
* admin): the worker's result comes back to its conversation.
|
||||
*/
|
||||
coordinator_id: number | string;
|
||||
requester_id: number | string;
|
||||
/**
|
||||
* The coordinator's session the task is handed over from.
|
||||
* The requester's session the task is handed over from.
|
||||
*/
|
||||
session_id: number | string;
|
||||
/**
|
||||
* The workspace the coordinator is working in: the worker works on it
|
||||
* The workspace the requester is working in: the worker works on it
|
||||
* there, in its conversation in the workspace.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
* The coordinator's name, shown in the worker's conversation.
|
||||
* The requester's name, shown in the worker's conversation.
|
||||
*/
|
||||
coordinator_name: string;
|
||||
requester_name: string;
|
||||
/**
|
||||
* The worker bots it can hand work to (in a workspace, its members).
|
||||
*/
|
||||
workers: BUN_SQLITE_COBBLE_BOTS[];
|
||||
/**
|
||||
* Reads who it may hand work to when the task is handed over, so a bot
|
||||
* added earlier in the run is there. `grabCrewWorkers` by default (the
|
||||
* coordinator); a workspace admin's are `grabDirectableBots`.
|
||||
*/
|
||||
grabTargets?: (params: { workspace_id: number | null }) => Promise<BUN_SQLITE_COBBLE_BOTS[]>;
|
||||
/**
|
||||
* The user's live workspaces, for the picker. In a workspace there's
|
||||
* nothing to pick: the task runs there.
|
||||
@@ -58,6 +65,7 @@ type Args = {
|
||||
/**
|
||||
* Lets the coordinator hand a task to a worker bot: in its own chat, or in
|
||||
* one of the user's workspaces, which decides the work's context entirely.
|
||||
* A workspace admin gets it too, for the workers in its workspace.
|
||||
* The worker runs it on the computer it's on there (if it has one) and
|
||||
* posts the result in that conversation, where a note says who asked; the
|
||||
* result (or failure) is also reported back in the coordinator's
|
||||
@@ -65,12 +73,13 @@ type Args = {
|
||||
*/
|
||||
export default function delegateTaskTool({
|
||||
user_id,
|
||||
coordinator_id,
|
||||
requester_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
coordinator_name,
|
||||
requester_name,
|
||||
workers,
|
||||
workspaces = [],
|
||||
grabTargets = ({ workspace_id }) => grabCrewWorkers({ workspace_id }),
|
||||
}: Params): HarnessTool<Args> {
|
||||
const crew = workers
|
||||
.map((bot) => `${bot.id}: ${bot.name}${bot.role ? ` (${bot.role})` : ""}`)
|
||||
@@ -117,7 +126,7 @@ export default function delegateTaskTool({
|
||||
? Number(args.workspace_id)
|
||||
: null;
|
||||
|
||||
const fresh = await grabCrewWorkers({ workspace_id: target_workspace_id });
|
||||
const fresh = await grabTargets({ workspace_id: target_workspace_id });
|
||||
const worker = fresh.find((bot) => bot.id == args.bot_id);
|
||||
|
||||
if (!worker?.id) {
|
||||
@@ -162,7 +171,7 @@ export default function delegateTaskTool({
|
||||
title: args.title,
|
||||
instructions: args.instructions,
|
||||
computer_id: computers[0]?.id || undefined,
|
||||
requested_by_bot_id: coordinator_id,
|
||||
requested_by_bot_id: requester_id,
|
||||
requested_by_session_id: session_id,
|
||||
});
|
||||
|
||||
@@ -172,7 +181,7 @@ export default function delegateTaskTool({
|
||||
bot_id: worker.id,
|
||||
session_id: Number(worker_session.id),
|
||||
workspace_id: target_workspace_id,
|
||||
text: `${coordinator_name} asked me to: **${args.title}**`,
|
||||
text: `${requester_name} asked me to: **${args.title}**`,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import workspaceRef from "@/src/utils/workspace-ref";
|
||||
import grabBotsTypes from "@/src/functions/backend/db/bot-types/grab-bots-types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import { z } from "zod";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
@@ -18,7 +20,7 @@ type Args = {
|
||||
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 ref (its id, e.g. [bot:3]), name, kind and role. With no query it lists the crew, up to limit. The coordinator is listed too but can't be deleted.`,
|
||||
description: `Find crew members: by id, name, the one-line role, or words in their core instructions. Returns each match's ref (its id, e.g. [bot:3]), name, kind, role and the workspaces it's admin of. 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(),
|
||||
@@ -53,11 +55,16 @@ export default function findBotTool(): HarnessTool<Args> {
|
||||
return `No crew member matches "${args.query || ""}".`;
|
||||
}
|
||||
|
||||
const types = await grabBotsTypes({ bot_ids: matches.map((bot) => bot.id) });
|
||||
|
||||
return matches
|
||||
.map(
|
||||
(bot) =>
|
||||
`${botRef({ id: bot.id || 0 })} ${bot.name} (${bot.kind}${bot.role ? `: ${bot.role}` : ""})\n## Instructions:\n${bot.system_prompt}`,
|
||||
)
|
||||
.map((bot) => {
|
||||
const led = (types.get(Number(bot.id)) || [])
|
||||
.filter((bt) => bt.bot_type == "admin" && bt.workspace_id)
|
||||
.map((bt) => workspaceRef({ id: Number(bt.workspace_id) }));
|
||||
|
||||
return `${botRef({ id: bot.id || 0 })} ${bot.name} (${bot.kind}${bot.role ? `: ${bot.role}` : ""})${led.length ? `, admin of ${led.join(", ")}` : ""}\n## Instructions:\n${bot.system_prompt}`;
|
||||
})
|
||||
.join("\n");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,9 +23,16 @@ import grabUserWorkspaces from "@/src/functions/backend/db/workspaces/grab-user-
|
||||
import setWorkspaceInstructionsTool from "./set-workspace-instructions-tool";
|
||||
import startTaskTool from "./start-task-tool";
|
||||
import computerRunDirs from "@/src/utils/computer-work-dirs";
|
||||
import type { BotRunPowers } from "@/src/functions/backend/db/bot-types/grab-bot-run-powers";
|
||||
import grabWorkspaceAdminTools, { type WorkspaceAdminTools } from "./grab-workspace-admin-tools";
|
||||
import setBotTypeTool from "./set-bot-type-tool";
|
||||
|
||||
type Params = {
|
||||
bot: BUN_SQLITE_COBBLE_BOTS;
|
||||
/**
|
||||
* What the bot's types let it do on this run (`grabBotRunPowers`).
|
||||
*/
|
||||
powers: BotRunPowers;
|
||||
user_id: number | string;
|
||||
/**
|
||||
* The bot's computers where it runs, the current one first
|
||||
@@ -58,6 +65,11 @@ type Params = {
|
||||
* user to confirm.
|
||||
*/
|
||||
onPropose?: (params: { ref: string }) => void;
|
||||
/**
|
||||
* Called with what a workspace admin leads, when the run is one, so
|
||||
* the reply can say so in its prompt.
|
||||
*/
|
||||
onWorkspaceAdmin?: (params: WorkspaceAdminTools) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -76,11 +88,15 @@ type Params = {
|
||||
* computers, and which name, role and core instructions each crew member
|
||||
* gets, and what goes) and the install's skills, MCP servers and custom
|
||||
* tools (`grabCoordinatorExtensionTools`) or, in a workspace,
|
||||
* set_bot_workspace_instructions. Anything else on a computer goes through
|
||||
* a task, never mid-reply.
|
||||
* set_bot_workspace_instructions, plus set_bot_type to make a worker a
|
||||
* workspace's admin. A workspace admin, in the workspace it leads, gets
|
||||
* `grabWorkspaceAdminTools`. Every gate reads the bot's types (`powers`),
|
||||
* never `bots.kind`. Anything else on a computer goes through a task,
|
||||
* never mid-reply.
|
||||
*/
|
||||
export default async function grabBotChatTools({
|
||||
bot,
|
||||
powers,
|
||||
user_id,
|
||||
computers,
|
||||
session_id,
|
||||
@@ -89,6 +105,7 @@ export default async function grabBotChatTools({
|
||||
scopes = [],
|
||||
onTakeOver,
|
||||
onPropose,
|
||||
onWorkspaceAdmin,
|
||||
}: Params): Promise<HarnessTool[]> {
|
||||
const bot_id = bot.id || 0;
|
||||
const tools = [
|
||||
@@ -98,7 +115,8 @@ export default async function grabBotChatTools({
|
||||
workspace_id,
|
||||
computers,
|
||||
session_id,
|
||||
is_coordinator: bot.kind == "coordinator",
|
||||
is_coordinator: powers.is_coordinator,
|
||||
admin_workspace_ids: powers.admin_workspace_ids,
|
||||
scopes,
|
||||
})),
|
||||
startTaskTool({
|
||||
@@ -121,7 +139,16 @@ export default async function grabBotChatTools({
|
||||
: []),
|
||||
];
|
||||
|
||||
if (bot.kind != "coordinator") return tools;
|
||||
if (!powers.is_coordinator) {
|
||||
if (!workspace_id || !powers.is_workspace_admin) return tools;
|
||||
|
||||
const admin = await grabWorkspaceAdminTools({ bot, user_id, workspace_id, powers, session_id, scopes });
|
||||
|
||||
onWorkspaceAdmin?.(admin);
|
||||
tools.push(...admin.tools);
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
const workspaces = (await grabUserWorkspaces({ user_id })).filter((w) => !w.archived);
|
||||
|
||||
@@ -140,13 +167,18 @@ export default async function grabBotChatTools({
|
||||
|
||||
tools.push(delegateTaskTool({
|
||||
user_id,
|
||||
coordinator_id: bot_id,
|
||||
requester_id: bot_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
coordinator_name: bot.name || "The coordinator",
|
||||
requester_name: bot.name || "The coordinator",
|
||||
workers,
|
||||
}));
|
||||
tools.push(setWorkspaceInstructionsTool({ user_id, workspace_id, workers }));
|
||||
|
||||
if (powers.can("set_bot_types")) {
|
||||
tools.push(setBotTypeTool({ user_id, bot_id, workers, workspaces, workspace_id }));
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
@@ -165,10 +197,10 @@ export default async function grabBotChatTools({
|
||||
tools.push(
|
||||
delegateTaskTool({
|
||||
user_id,
|
||||
coordinator_id: bot_id,
|
||||
requester_id: bot_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
coordinator_name: bot.name || "The coordinator",
|
||||
requester_name: bot.name || "The coordinator",
|
||||
workers,
|
||||
workspaces,
|
||||
}),
|
||||
@@ -197,6 +229,10 @@ export default async function grabBotChatTools({
|
||||
addBotToWorkspaceTool({ user_id, workers, workspaces }),
|
||||
removeBotFromWorkspaceTool({ user_id, workers, workspaces }),
|
||||
);
|
||||
|
||||
if (powers.can("set_bot_types")) {
|
||||
tools.push(setBotTypeTool({ user_id, bot_id, workers, workspaces }));
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
|
||||
@@ -33,6 +33,10 @@ type Params = {
|
||||
* add to any bot's or workspace's list.
|
||||
*/
|
||||
is_coordinator?: boolean;
|
||||
/**
|
||||
* The workspaces the bot is admin of (`grabBotRunPowers`).
|
||||
*/
|
||||
admin_workspace_ids?: number[];
|
||||
/**
|
||||
* The files the run reaches (`grabRunScopes`), for the picture it may
|
||||
* set from one.
|
||||
@@ -52,6 +56,7 @@ export default async function grabBotTools({
|
||||
computers = [],
|
||||
session_id,
|
||||
is_coordinator,
|
||||
admin_workspace_ids,
|
||||
scopes = [],
|
||||
}: Params): Promise<HarnessTool[]> {
|
||||
return [
|
||||
@@ -70,6 +75,7 @@ export default async function grabBotTools({
|
||||
user_id,
|
||||
bot_id,
|
||||
is_coordinator: Boolean(is_coordinator),
|
||||
admin_workspace_ids,
|
||||
workspace_id,
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import type { BotRunPowers } from "@/src/functions/backend/db/bot-types/grab-bot-run-powers";
|
||||
import grabDirectableBots from "@/src/functions/backend/db/bot-types/grab-directable-bots";
|
||||
import grabCrewRole from "@/src/functions/backend/db/access/grab-crew-role";
|
||||
import grabUserWorkspace from "@/src/functions/backend/db/workspaces/grab-user-workspace";
|
||||
import type { StorageScope } from "@/src/utils/storage-access";
|
||||
import type { HarnessTool } from "../../types";
|
||||
import delegateTaskTool from "./delegate-task-tool";
|
||||
import setWorkspaceInstructionsTool from "./set-workspace-instructions-tool";
|
||||
import updateWorkspaceTool from "./update-workspace-tool";
|
||||
|
||||
type Params = {
|
||||
bot: BUN_SQLITE_COBBLE_BOTS;
|
||||
user_id: number | string;
|
||||
workspace_id: number | string;
|
||||
powers: BotRunPowers;
|
||||
/**
|
||||
* The session being answered: delegated work reports back to it. Left
|
||||
* out (a task run), nothing is handed over.
|
||||
*/
|
||||
session_id?: number | string;
|
||||
scopes?: StorageScope[];
|
||||
};
|
||||
|
||||
export type WorkspaceAdminTools = {
|
||||
tools: HarnessTool[];
|
||||
/**
|
||||
* The bots it may direct there.
|
||||
*/
|
||||
members: BUN_SQLITE_COBBLE_BOTS[];
|
||||
workspace: BUN_SQLITE_COBBLE_WORKSPACES | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The tools a workspace admin leads its workspace with, each only while
|
||||
* its types allow it there (`BotRunPowers.can`): delegate_task and
|
||||
* set_bot_workspace_instructions for the members it may direct
|
||||
* (`grabDirectableBots`), and update_workspace for this workspace alone,
|
||||
* only when the user the run works for may change it too (a crew manager).
|
||||
*/
|
||||
export default async function grabWorkspaceAdminTools({
|
||||
bot,
|
||||
user_id,
|
||||
workspace_id,
|
||||
powers,
|
||||
session_id,
|
||||
scopes = [],
|
||||
}: Params): Promise<WorkspaceAdminTools> {
|
||||
const workspace = await grabUserWorkspace({ workspace_id, user_id });
|
||||
|
||||
if (!workspace?.id || workspace.archived || !powers.is_workspace_admin) {
|
||||
return { tools: [], members: [], workspace: null };
|
||||
}
|
||||
|
||||
const bot_id = bot.id || 0;
|
||||
const tools: HarnessTool[] = [];
|
||||
const members = await grabDirectableBots({ bot_id, workspace_id });
|
||||
const grabTargets = () => grabDirectableBots({ bot_id, workspace_id });
|
||||
|
||||
if (members.length && session_id && powers.can("delegate_task")) {
|
||||
tools.push(
|
||||
delegateTaskTool({
|
||||
user_id,
|
||||
requester_id: bot_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
requester_name: bot.name || "The workspace's admin",
|
||||
workers: members,
|
||||
grabTargets,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (members.length && powers.can("set_member_instructions")) {
|
||||
tools.push(setWorkspaceInstructionsTool({ user_id, workspace_id, workers: members, grabTargets }));
|
||||
}
|
||||
|
||||
if (powers.can("edit_workspace") && (await grabCrewRole({ user_id })) != "member") {
|
||||
tools.push(updateWorkspaceTool({ user_id, workspaces: [workspace], workspace_id, scopes }));
|
||||
}
|
||||
|
||||
return { tools, members, workspace };
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { z } from "zod";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import grabBotTypes from "@/src/functions/backend/db/bot-types/grab-bot-types";
|
||||
import removeBotType from "@/src/functions/backend/db/bot-types/remove-bot-type";
|
||||
import setBotType from "@/src/functions/backend/db/bot-types/set-bot-type";
|
||||
import botRef from "@/src/utils/bot-ref";
|
||||
import pushBotUpdated from "@/src/bots-queue/push-bot-updated";
|
||||
import type { HarnessTool } from "../../types";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
/**
|
||||
* The coordinator: it must be allowed as well as the user
|
||||
* (`BotTypeAssignmentMatrix`).
|
||||
*/
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* The workers it may make admins.
|
||||
*/
|
||||
workers: BUN_SQLITE_COBBLE_BOTS[];
|
||||
/**
|
||||
* The workspaces they can lead.
|
||||
*/
|
||||
workspaces: BUN_SQLITE_COBBLE_WORKSPACES[];
|
||||
/**
|
||||
* The workspace the conversation is in: the one meant when no id is
|
||||
* given.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
};
|
||||
|
||||
type Args = {
|
||||
bot_id: number;
|
||||
workspace_id?: number;
|
||||
admin: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets the coordinator make a worker the admin of a workspace it works in,
|
||||
* or take that away. An admin hands tasks to the other bots there, sets
|
||||
* their instructions there, runs the workspace's task list and keeps its
|
||||
* context (`BotTypePermissionMatrix`). Read back before it says it's done.
|
||||
*/
|
||||
export default function setBotTypeTool({ user_id, bot_id, workers, workspaces, workspace_id }: Params): HarnessTool<Args> {
|
||||
const crew = workers.map((bot) => `${bot.id}: ${bot.name}`).join("; ");
|
||||
const places = workspaces.map((w) => `${w.id}: ${w.name}`).join("; ");
|
||||
|
||||
return {
|
||||
name: "set_bot_type",
|
||||
description: `Make a worker the admin of a workspace it works in (admin: true), or take that away (admin: false). A workspace admin leads the bots there: it hands them tasks with delegate_task, sets their instructions there, runs the workspace's task list and keeps its context. Only do it once the user asked for it or agreed. Crew: ${crew || "none"}. Workspaces: ${places || "none"}.`,
|
||||
schema: z.object({
|
||||
bot_id: z.number().int(),
|
||||
workspace_id: z
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.describe(workspace_id ? `Defaults to this workspace (${workspace_id}).` : `The workspace it leads.`),
|
||||
admin: z.boolean(),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const target_workspace_id = args.workspace_id || (workspace_id ? Number(workspace_id) : null);
|
||||
|
||||
if (!target_workspace_id) {
|
||||
throw new Error(`Name the workspace: ${places || "none"}.`);
|
||||
}
|
||||
|
||||
const change = {
|
||||
user_id,
|
||||
actor_bot_id: bot_id,
|
||||
bot_id: args.bot_id,
|
||||
bot_type: "admin" as const,
|
||||
workspace_id: target_workspace_id,
|
||||
};
|
||||
|
||||
const { bot, workspace } = args.admin ? await setBotType(change) : await removeBotType(change);
|
||||
|
||||
const saved = (await grabBotTypes({ bot_id: args.bot_id })).some(
|
||||
(bt) => bt.bot_type == "admin" && bt.workspace_id == target_workspace_id,
|
||||
);
|
||||
|
||||
if (saved != args.admin) {
|
||||
throw new Error(`The change didn't take: check the bot is still in the workspace and try again.`);
|
||||
}
|
||||
|
||||
await pushBotUpdated({ bot_id: args.bot_id });
|
||||
|
||||
return args.admin
|
||||
? `${botRef({ id: bot.id || 0 })} is now the admin of "${workspace.name}". It gets the admin's tools from its next reply there.`
|
||||
: `${botRef({ id: bot.id || 0 })} is no longer the admin of "${workspace.name}".`;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,12 @@ type Params = {
|
||||
* The workspace's crew members (not the coordinator).
|
||||
*/
|
||||
workers: BUN_SQLITE_COBBLE_BOTS[];
|
||||
/**
|
||||
* Reads whose instructions it may set when it's called.
|
||||
* `grabCrewWorkers` by default (the coordinator); a workspace admin's
|
||||
* are `grabDirectableBots`.
|
||||
*/
|
||||
grabTargets?: () => Promise<BUN_SQLITE_COBBLE_BOTS[]>;
|
||||
};
|
||||
|
||||
type Args = {
|
||||
@@ -20,13 +26,14 @@ type Args = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets the coordinator set a crew member's instructions for this
|
||||
* workspace: how that bot should do its job here.
|
||||
* Lets the coordinator, or the workspace's admin, set a crew member's
|
||||
* instructions for this workspace: how that bot should do its job here.
|
||||
*/
|
||||
export default function setWorkspaceInstructionsTool({
|
||||
user_id,
|
||||
workspace_id,
|
||||
workers,
|
||||
grabTargets = () => grabCrewWorkers({ workspace_id }),
|
||||
}: Params): HarnessTool<Args> {
|
||||
const crew = workers.map((bot) => `${bot.id}: ${bot.name}`).join("; ");
|
||||
|
||||
@@ -38,7 +45,7 @@ export default function setWorkspaceInstructionsTool({
|
||||
instructions: z.string().trim().max(AppData.PromptMaxLength),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const fresh = await grabCrewWorkers({ workspace_id });
|
||||
const fresh = await grabTargets();
|
||||
const worker = fresh.find((bot) => bot.id == args.bot_id);
|
||||
|
||||
if (!worker?.id) {
|
||||
|
||||
@@ -23,6 +23,15 @@ type Params = {
|
||||
*/
|
||||
workers?: BUN_SQLITE_COBBLE_BOTS[];
|
||||
workspaces?: BUN_SQLITE_COBBLE_WORKSPACES[];
|
||||
/**
|
||||
* The workspaces the bot is admin of (`grabBotRunPowers`).
|
||||
*/
|
||||
admin_workspace_ids?: number[];
|
||||
/**
|
||||
* In a workspace it's admin of: the other bots there, whose own lists
|
||||
* it may add to.
|
||||
*/
|
||||
members?: BUN_SQLITE_COBBLE_BOTS[];
|
||||
};
|
||||
|
||||
type Args = {
|
||||
@@ -38,18 +47,21 @@ type Place = { bot_id?: number; workspace_id?: number };
|
||||
/**
|
||||
* Worked out from the arguments and who is asking: where the task goes, or
|
||||
* why it can't. A worker may only add to its own list or the workspace it
|
||||
* works in; the coordinator to any worker's, any workspace's, or its own.
|
||||
* works in; a workspace admin also to its members' own lists there; the
|
||||
* coordinator to any worker's, any workspace's, or its own.
|
||||
*/
|
||||
function resolvePlace({
|
||||
args,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
is_coordinator,
|
||||
members = [],
|
||||
}: {
|
||||
args: Args;
|
||||
bot_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
is_coordinator: boolean;
|
||||
members?: BUN_SQLITE_COBBLE_BOTS[];
|
||||
}): Place | string {
|
||||
const target_bot = args.bot_id ? Number(args.bot_id) : null;
|
||||
const target_workspace = args.workspace_id ? Number(args.workspace_id) : null;
|
||||
@@ -64,8 +76,12 @@ function resolvePlace({
|
||||
return { bot_id: Number(bot_id) };
|
||||
}
|
||||
|
||||
if (target_bot && target_bot != Number(bot_id)) {
|
||||
return `You can only add tasks to your own list (#${bot_id}).`;
|
||||
const is_member = members.some((member) => member.id == target_bot);
|
||||
|
||||
if (target_bot && target_bot != Number(bot_id) && !is_member) {
|
||||
return members.length
|
||||
? `You can only add tasks to your own list (#${bot_id}), this workspace's, or a member's here (${members.map((m) => m.id).join(", ")}).`
|
||||
: `You can only add tasks to your own list (#${bot_id}).`;
|
||||
}
|
||||
|
||||
if (target_workspace && Number(workspace_id || 0) != target_workspace) {
|
||||
@@ -93,15 +109,18 @@ export default function createTaskTool({
|
||||
is_coordinator,
|
||||
workers = [],
|
||||
workspaces = [],
|
||||
admin_workspace_ids = [],
|
||||
members = [],
|
||||
}: Params): HarnessTool<Args> {
|
||||
const crew = workers.map((worker) => `${worker.id}: ${worker.name}`).join("; ");
|
||||
const places = workspaces.map((workspace) => `${workspace.id}: ${workspace.name}`).join("; ");
|
||||
const team = members.map((member) => `${member.id}: ${member.name}`).join("; ");
|
||||
|
||||
return {
|
||||
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, 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.`,
|
||||
: `Add a task to a list: ${workspace_id ? `this workspace's shared one (leave the ids out), or your own (bot_id: ${bot_id})${team ? `, or, as this workspace's admin, a member's own (${team})` : ""}` : `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 +130,9 @@ 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; leaving it out has the same effect here.`,
|
||||
: team
|
||||
? `Your own id (${bot_id}) or a member's here (${team}), to put it on that 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()
|
||||
@@ -125,13 +146,14 @@ export default function createTaskTool({
|
||||
status: z.enum(TaskStatusValues).optional().describe(`Defaults to to do`),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const place = resolvePlace({ args, bot_id, workspace_id, is_coordinator });
|
||||
const place = resolvePlace({ args, bot_id, workspace_id, is_coordinator, members });
|
||||
|
||||
if (typeof place == "string") return place;
|
||||
|
||||
const existing = await grabTasks({
|
||||
bot_id,
|
||||
is_coordinator,
|
||||
admin_workspace_ids,
|
||||
only_bot_id: place.bot_id,
|
||||
only_workspace_id: place.workspace_id,
|
||||
open_only: true,
|
||||
@@ -158,7 +180,7 @@ export default function createTaskTool({
|
||||
created_by_bot_id: bot_id,
|
||||
});
|
||||
|
||||
return `Task #${task.id} "${task.title}" added ${place.workspace_id ? "to this workspace's list" : "to your list"}. Check it with list_tasks before you mention it to the user.`;
|
||||
return `Task #${task.id} "${task.title}" added ${place.workspace_id ? "to this workspace's list" : place.bot_id == Number(bot_id) ? "to your list" : `to bot ${place.bot_id}'s list`}. Check it with list_tasks before you mention it to the user.`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ type Params = {
|
||||
* to any list and reaches every folder.
|
||||
*/
|
||||
is_coordinator: boolean;
|
||||
/**
|
||||
* The workspaces the bot is admin of (`grabBotRunPowers`): in one of
|
||||
* them, create_task can also add to the other members' own lists.
|
||||
*/
|
||||
admin_workspace_ids?: number[];
|
||||
workspace_id?: number | string | null;
|
||||
};
|
||||
|
||||
@@ -23,15 +28,18 @@ type Params = {
|
||||
* shared to-do list, one per bot and one per workspace, so a chat reply, a
|
||||
* task and a scheduled run all read it, move it along and file what they
|
||||
* produced. The coordinator's create_task may target any worker or
|
||||
* workspace; its update_task may link any file it can reach.
|
||||
* workspace; its update_task may link any file it can reach. A workspace
|
||||
* admin's may add to the other members' own lists there.
|
||||
*/
|
||||
export default async function grabBotTaskTools({
|
||||
user_id,
|
||||
bot_id,
|
||||
is_coordinator,
|
||||
admin_workspace_ids = [],
|
||||
workspace_id,
|
||||
}: Params): Promise<HarnessTool[]> {
|
||||
let workers: BUN_SQLITE_COBBLE_BOTS[] = [];
|
||||
let members: BUN_SQLITE_COBBLE_BOTS[] = [];
|
||||
let workspaces: BUN_SQLITE_COBBLE_WORKSPACES[] = [];
|
||||
let all_workspace_ids: number[] = [];
|
||||
|
||||
@@ -39,6 +47,8 @@ export default async function grabBotTaskTools({
|
||||
workers = await grabCrewWorkers({ workspace_id });
|
||||
workspaces = (await grabUserWorkspaces({ user_id })).filter((workspace) => !workspace.archived);
|
||||
all_workspace_ids = workspaces.map((workspace) => Number(workspace.id));
|
||||
} else if (workspace_id && admin_workspace_ids.includes(Number(workspace_id))) {
|
||||
members = (await grabCrewWorkers({ workspace_id })).filter((member) => member.id != Number(bot_id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,8 +64,17 @@ export default async function grabBotTaskTools({
|
||||
});
|
||||
|
||||
return [
|
||||
listTasksTool({ user_id, bot_id, is_coordinator, workspace_id }),
|
||||
createTaskTool({ user_id, bot_id, workspace_id, is_coordinator, workers, workspaces }),
|
||||
updateTaskTool({ user_id, bot_id, is_coordinator, scopes }),
|
||||
listTasksTool({ user_id, bot_id, is_coordinator, admin_workspace_ids, workspace_id }),
|
||||
createTaskTool({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
is_coordinator,
|
||||
workers,
|
||||
workspaces,
|
||||
admin_workspace_ids,
|
||||
members,
|
||||
}),
|
||||
updateTaskTool({ user_id, bot_id, is_coordinator, admin_workspace_ids, scopes }),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
is_coordinator: boolean;
|
||||
/**
|
||||
* The workspaces the bot is admin of (`grabBotRunPowers`).
|
||||
*/
|
||||
admin_workspace_ids?: number[];
|
||||
/**
|
||||
* The workspace the run is in, if any.
|
||||
*/
|
||||
@@ -27,7 +31,7 @@ type Args = {
|
||||
* workspace it works in, or for the coordinator the whole install. Used
|
||||
* both to plan work and to remind the user of what's pending.
|
||||
*/
|
||||
export default function listTasksTool({ bot_id, user_id, is_coordinator, workspace_id }: Params): HarnessTool<Args> {
|
||||
export default function listTasksTool({ bot_id, user_id, is_coordinator, admin_workspace_ids, workspace_id }: Params): HarnessTool<Args> {
|
||||
return {
|
||||
name: "list_tasks",
|
||||
description: `List tasks: yours, the ones in a workspace you work in, or ${is_coordinator ? "every task in the install" : "every task you can reach"}. Each line is its id, status, title, whose it is, its files and its latest note. Open ones come first (blocked, then doing, then to do). Use it to plan work, and to check what's still pending when the user says hello or asks what's next.`,
|
||||
@@ -50,6 +54,7 @@ export default function listTasksTool({ bot_id, user_id, is_coordinator, workspa
|
||||
const tasks = await grabTasks({
|
||||
bot_id,
|
||||
is_coordinator,
|
||||
admin_workspace_ids,
|
||||
only_bot_id: scope == "mine" ? Number(bot_id) : undefined,
|
||||
only_workspace_id,
|
||||
statuses: args.status ? [args.status] : undefined,
|
||||
|
||||
@@ -13,6 +13,10 @@ type Params = {
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
is_coordinator: boolean;
|
||||
/**
|
||||
* The workspaces the bot is admin of (`grabBotRunPowers`).
|
||||
*/
|
||||
admin_workspace_ids?: number[];
|
||||
/**
|
||||
* The run's folders, for linking a deliverable by its `[file:ID]` ref.
|
||||
*/
|
||||
@@ -33,7 +37,7 @@ type Args = {
|
||||
* a progress note, and file the deliverables it produced. One call does all
|
||||
* of them, so a step in the work is a step in the log.
|
||||
*/
|
||||
export default function updateTaskTool({ user_id, bot_id, is_coordinator, scopes = [] }: Params): HarnessTool<Args> {
|
||||
export default function updateTaskTool({ user_id, bot_id, is_coordinator, admin_workspace_ids, scopes = [] }: Params): HarnessTool<Args> {
|
||||
return {
|
||||
name: "update_task",
|
||||
description: `Move a task along. Set its status (blocked needs a reason: what the user has to do), log a note of what you did or found, and file the files it produced. Call it as the work advances, not only at the end; every note is what the user sees on the task. Mark done only once you've verified the work (see the check note): starting a task isn't finishing it. To file a deliverable, save it first with write_file, make_pdf or generate_image, then pass its [file:ID] number.`,
|
||||
@@ -60,7 +64,7 @@ export default function updateTaskTool({ user_id, bot_id, is_coordinator, scopes
|
||||
.describe(`The [file:ID] refs of files it works from`),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const task = await grabRunTask({ task_id: args.task_id, bot_id, is_coordinator });
|
||||
const task = await grabRunTask({ task_id: args.task_id, bot_id, is_coordinator, admin_workspace_ids });
|
||||
const said: string[] = [];
|
||||
|
||||
if (args.status || args.blocked_reason !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user