Scope workspace tasks to the bot that has them
A workspace's task now carries the bot that has it there (bot_id with workspace_id), or none while it's shared. A bot reaches its own list and, in its workspaces, its own tasks plus the shared ones; a workspace admin every task there. Bots create workspace tasks for themselves by default, take a shared one by starting it, and hand-overs record the new bot. Removing or deleting a bot makes its workspace tasks shared again. The conversation's Tasks modal shows that bot's tasks in the workspace. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -11,14 +11,14 @@ type Params = {
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* The workspace the run is in, if any: a bot adding a task there
|
||||
* defaults to the workspace's shared list.
|
||||
* The workspace the run is in, if any: a worker's task there stays in
|
||||
* it, its own by default.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
is_coordinator: boolean;
|
||||
/**
|
||||
* The coordinator's targets: every worker and live workspace. A worker
|
||||
* gets neither and may only add to its own list or the workspace it
|
||||
* gets neither and may only add to its own list or in the workspace it
|
||||
* works in.
|
||||
*/
|
||||
workers?: BUN_SQLITE_COBBLE_BOTS[];
|
||||
@@ -39,16 +39,19 @@ type Args = {
|
||||
description: string;
|
||||
bot_id?: number;
|
||||
workspace_id?: number;
|
||||
shared?: boolean;
|
||||
status?: TaskStatus;
|
||||
};
|
||||
|
||||
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; a workspace admin also to its members' own lists there; the
|
||||
* coordinator to any worker's, any workspace's, or its own.
|
||||
* Worked out from the arguments and who is asking: where the task goes and
|
||||
* who has it, or why it can't. Outside a workspace a worker adds only to
|
||||
* its own list. In one the task stays in that workspace: its own by
|
||||
* default, a member's as the workspace's admin, or shared (nobody's yet).
|
||||
* The coordinator adds to any worker's own list or any workspace, for one
|
||||
* of its bots or shared.
|
||||
*/
|
||||
function resolvePlace({
|
||||
args,
|
||||
@@ -66,41 +69,53 @@ function resolvePlace({
|
||||
const target_bot = args.bot_id ? Number(args.bot_id) : null;
|
||||
const target_workspace = args.workspace_id ? Number(args.workspace_id) : null;
|
||||
|
||||
if (target_bot && target_workspace) {
|
||||
return `A task belongs to a bot or a workspace, not both. Pick one.`;
|
||||
if (args.shared && target_bot) {
|
||||
return `A shared task is nobody's yet: leave bot_id out, or drop shared to give it to that bot.`;
|
||||
}
|
||||
|
||||
if (is_coordinator) {
|
||||
if (target_workspace) return { workspace_id: target_workspace };
|
||||
if (target_bot) return { bot_id: target_bot };
|
||||
const place_workspace = target_workspace || (workspace_id ? Number(workspace_id) : null);
|
||||
|
||||
if (place_workspace) {
|
||||
return target_bot ? { workspace_id: place_workspace, bot_id: target_bot } : { workspace_id: place_workspace };
|
||||
}
|
||||
|
||||
return { bot_id: target_bot || Number(bot_id) };
|
||||
}
|
||||
|
||||
if (!workspace_id) {
|
||||
if (target_workspace || args.shared) {
|
||||
return `You're not in a workspace here, so a task can only go on your own list (bot_id ${bot_id}).`;
|
||||
}
|
||||
|
||||
if (target_bot && target_bot != Number(bot_id)) {
|
||||
return `You can only add tasks to your own list (#${bot_id}).`;
|
||||
}
|
||||
|
||||
return { bot_id: Number(bot_id) };
|
||||
}
|
||||
|
||||
if (target_workspace && target_workspace != Number(workspace_id)) {
|
||||
return `You can only add tasks in this workspace (#${workspace_id}).`;
|
||||
}
|
||||
|
||||
if (args.shared) return { workspace_id: Number(workspace_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}).`;
|
||||
? `You can only give a task here to yourself (#${bot_id}) or a member (${members.map((m) => m.id).join(", ")}), or leave it shared.`
|
||||
: `You can only give a task here to yourself (#${bot_id}), or leave it shared.`;
|
||||
}
|
||||
|
||||
if (target_workspace && Number(workspace_id || 0) != target_workspace) {
|
||||
return `You can only add to this workspace's list (#${workspace_id || "none"}) or your own.`;
|
||||
}
|
||||
|
||||
if (target_workspace) return { workspace_id: target_workspace };
|
||||
if (target_bot) return { bot_id: target_bot };
|
||||
|
||||
/**
|
||||
* In a workspace, the workspace's list is the right home: work there is
|
||||
* shared work.
|
||||
*/
|
||||
return workspace_id ? { workspace_id: Number(workspace_id) } : { bot_id: Number(bot_id) };
|
||||
return { workspace_id: Number(workspace_id), bot_id: target_bot || Number(bot_id) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets a bot add a task to a list it works on: its own, or a workspace's.
|
||||
* The coordinator can add to any worker's list and any workspace's.
|
||||
* Lets a bot add a task to a list it works on: its own, or in a workspace
|
||||
* its own there (a member's as its admin) or a shared one. The coordinator
|
||||
* can add to any worker's list and any workspace.
|
||||
*/
|
||||
export default function createTaskTool({
|
||||
user_id,
|
||||
@@ -119,8 +134,8 @@ export default function createTaskTool({
|
||||
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})${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.`,
|
||||
? `Add a task: to a worker's own list (bot_id: ${crew || "no workers yet"}), to a workspace (workspace_id: ${places || "no workspaces yet"}) for one of its bots (with bot_id) or shared (without), or to your own list (leave both ids out${workspace_id ? "; here, leaving them out makes it a shared task in this workspace" : ""}). Write the description as a brief: what to do, where, and how you'll know it's done.`
|
||||
: `Add a task: ${workspace_id ? `in this workspace, yours by default${team ? `, a member's as this workspace's admin (bot_id: ${team})` : ""}, or shared (shared: true) for whichever bot here takes it up` : `on your own list (bot_id ${bot_id}, the default)`}. 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
|
||||
@@ -129,10 +144,10 @@ export default function createTaskTool({
|
||||
.describe(`The brief, in markdown: what to do, where, and what done looks like`),
|
||||
bot_id: z.number().int().optional().describe(
|
||||
is_coordinator
|
||||
? `Whose own list, when it isn't a workspace's. Leave out for your own.`
|
||||
? `Who has it: a worker's own list, or with workspace_id a bot working there. Leave out for your own list or a shared workspace task.`
|
||||
: 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.`,
|
||||
? `Who has it here: you (${bot_id}, the default) or a member (${team}).`
|
||||
: `Your own id (${bot_id}); leaving it out has the same effect.`,
|
||||
),
|
||||
workspace_id: z
|
||||
.number()
|
||||
@@ -140,9 +155,13 @@ export default function createTaskTool({
|
||||
.optional()
|
||||
.describe(
|
||||
is_coordinator
|
||||
? `A workspace's shared list, when it isn't a bot's own.`
|
||||
: `This workspace's shared list (${workspace_id || "none"}).`,
|
||||
? `The workspace it's in, when it isn't a bot's own list.`
|
||||
: `This workspace (${workspace_id || "none"}); leaving it out has the same effect.`,
|
||||
),
|
||||
shared: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(`In a workspace: nobody's yet, for whichever bot there takes it up. Defaults to false.`),
|
||||
status: z.enum(TaskStatusValues).optional().describe(`Defaults to to do`),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
@@ -154,8 +173,9 @@ export default function createTaskTool({
|
||||
bot_id,
|
||||
is_coordinator,
|
||||
admin_workspace_ids,
|
||||
only_bot_id: place.bot_id,
|
||||
only_bot_id: place.workspace_id ? undefined : place.bot_id,
|
||||
only_workspace_id: place.workspace_id,
|
||||
own_list: !place.workspace_id,
|
||||
open_only: true,
|
||||
});
|
||||
|
||||
@@ -180,7 +200,13 @@ 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" : 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.`;
|
||||
const where = place.workspace_id
|
||||
? `in workspace ${place.workspace_id}, ${!place.bot_id ? "shared" : place.bot_id == Number(bot_id) ? "yours" : `bot ${place.bot_id}'s`}`
|
||||
: place.bot_id == Number(bot_id)
|
||||
? "to your list"
|
||||
: `to bot ${place.bot_id}'s list`;
|
||||
|
||||
return `Task #${task.id} "${task.title}" added ${where}. Check it with list_tasks before you mention it to the user.`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type Params = {
|
||||
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.
|
||||
* them, create_task can also give tasks there to the other members.
|
||||
*/
|
||||
admin_workspace_ids?: number[];
|
||||
workspace_id?: number | string | null;
|
||||
@@ -25,11 +25,11 @@ type Params = {
|
||||
|
||||
/**
|
||||
* The task tools every run gets (`src/harness/tools/bot/tasks/`): the
|
||||
* 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. A workspace
|
||||
* admin's may add to the other members' own lists there.
|
||||
* to-do list (a bot's own, and in each workspace its tasks there and the
|
||||
* shared ones), 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. A workspace admin's may give tasks there to the other members.
|
||||
*/
|
||||
export default async function grabBotTaskTools({
|
||||
user_id,
|
||||
|
||||
@@ -34,12 +34,12 @@ type 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.`,
|
||||
description: `List tasks: yours (your own list and the ones you have in workspaces), the ones in ${workspace_id ? "this workspace" : "a workspace"}, or ${is_coordinator ? "every task in the install" : "every task you can reach (yours, and in your workspaces the shared ones nobody has yet)"}. 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.`,
|
||||
schema: z.object({
|
||||
scope: z
|
||||
.enum(["mine", "workspace", "all"])
|
||||
.optional()
|
||||
.describe(`"mine" for your own list, "workspace" for ${workspace_id ? "this workspace's" : "a workspace's (none open)"}, "all" for everything you reach. Defaults to "all".`),
|
||||
.describe(`"mine" for the tasks you have (your own list and in workspaces), "workspace" for ${workspace_id ? "this workspace's" : "a workspace's (none open)"}, "all" for everything you reach. Defaults to "all".`),
|
||||
status: z.enum(TaskStatusValues).optional(),
|
||||
open_only: z
|
||||
.boolean()
|
||||
@@ -74,9 +74,10 @@ export default function listTasksTool({ bot_id, user_id, is_coordinator, admin_w
|
||||
|
||||
return tasks
|
||||
.map((task) => {
|
||||
const who = task.bot_id ? task.bot_name || `bot ${task.bot_id}` : "shared, nobody's yet";
|
||||
const where = task.workspace_id
|
||||
? `in ${names.get(Number(task.workspace_id)) || `workspace ${task.workspace_id}`}`
|
||||
: `on ${task.bot_name || `bot ${task.bot_id}`}`;
|
||||
? `in ${names.get(Number(task.workspace_id)) || `workspace ${task.workspace_id}`}, ${who}`
|
||||
: `on ${who}'s list`;
|
||||
const bits = [
|
||||
`#${task.id}`,
|
||||
`[${task.status}]`,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type TaskStatus, TaskStatusValues } from "@/src/dict/task-dict";
|
||||
import addTaskNote from "@/src/functions/backend/db/tasks/add-task-note";
|
||||
import attachTaskFile from "@/src/functions/backend/db/tasks/attach-task-file";
|
||||
import grabRunTask from "@/src/functions/backend/db/tasks/grab-run-task";
|
||||
import moveTask from "@/src/functions/backend/db/tasks/move-task";
|
||||
import grabTaskMedia from "@/src/functions/backend/db/tasks/grab-task-media";
|
||||
import updateTask from "@/src/functions/backend/db/tasks/update-task";
|
||||
import type { StorageScope } from "@/src/utils/storage-access";
|
||||
@@ -40,7 +41,7 @@ type 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.`,
|
||||
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. Setting a shared workspace task to doing makes it yours. 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.`,
|
||||
schema: z.object({
|
||||
task_id: z.number().int(),
|
||||
status: z.enum(TaskStatusValues).optional(),
|
||||
@@ -67,6 +68,15 @@ export default function updateTaskTool({ user_id, bot_id, is_coordinator, admin_
|
||||
const task = await grabRunTask({ task_id: args.task_id, bot_id, is_coordinator, admin_workspace_ids });
|
||||
const said: string[] = [];
|
||||
|
||||
/**
|
||||
* A shared task in a workspace is nobody's until a bot starts on
|
||||
* it: then it's that bot's.
|
||||
*/
|
||||
if (args.status == "doing" && task.workspace_id && !task.bot_id && !is_coordinator) {
|
||||
await moveTask({ task_id: task.id!, bot_id, workspace_id: task.workspace_id });
|
||||
said.push(`it's yours now`);
|
||||
}
|
||||
|
||||
if (args.status || args.blocked_reason !== undefined) {
|
||||
if (args.status == "blocked" && !args.blocked_reason?.trim()) {
|
||||
return `Blocked needs a reason: say in blocked_reason what the user has to do.`;
|
||||
|
||||
Reference in New Issue
Block a user