Give every task a bot: drop shared workspace tasks
A workspace task with no bot was seen by every bot working there. Now a task can't be saved without a bot, a worker reaches only its own (the workspace admin and the coordinator still reach all), the workspace page picks who has a new task, and a bot leaving a workspace or deleted passes its tasks there to the workspace's admin bot, else the coordinator (also at boot for rows without a bot). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -39,19 +39,18 @@ type Args = {
|
||||
description: string;
|
||||
bot_id?: number;
|
||||
workspace_id?: number;
|
||||
shared?: boolean;
|
||||
status?: TaskStatus;
|
||||
};
|
||||
|
||||
type Place = { bot_id?: number; workspace_id?: number };
|
||||
type Place = { bot_id: number; workspace_id?: number };
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* who has it, or why it can't. Every task belongs to a bot. Outside a
|
||||
* workspace a worker adds only to its own list. In one the task stays in
|
||||
* that workspace: its own by default, or a member's as the workspace's
|
||||
* admin. The coordinator adds to any worker's own list or any workspace,
|
||||
* for one of its bots or itself.
|
||||
*/
|
||||
function resolvePlace({
|
||||
args,
|
||||
@@ -69,22 +68,18 @@ 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 (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) {
|
||||
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 { workspace_id: place_workspace, bot_id: target_bot || Number(bot_id) };
|
||||
}
|
||||
|
||||
return { bot_id: target_bot || Number(bot_id) };
|
||||
}
|
||||
|
||||
if (!workspace_id) {
|
||||
if (target_workspace || args.shared) {
|
||||
if (target_workspace) {
|
||||
return `You're not in a workspace here, so a task can only go on your own list (bot_id ${bot_id}).`;
|
||||
}
|
||||
|
||||
@@ -99,14 +94,12 @@ function resolvePlace({
|
||||
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 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.`;
|
||||
? `You can only give a task here to yourself (#${bot_id}) or a member (${members.map((m) => m.id).join(", ")}).`
|
||||
: `You can only give a task here to yourself (#${bot_id}).`;
|
||||
}
|
||||
|
||||
return { workspace_id: Number(workspace_id), bot_id: target_bot || Number(bot_id) };
|
||||
@@ -114,8 +107,8 @@ function resolvePlace({
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* its own there (a member's as its admin). The coordinator can add to any
|
||||
* worker's list and any workspace. Every task belongs to a bot.
|
||||
*/
|
||||
export default function createTaskTool({
|
||||
user_id,
|
||||
@@ -134,8 +127,8 @@ export default function createTaskTool({
|
||||
return {
|
||||
name: "create_task",
|
||||
description: is_coordinator
|
||||
? `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.`,
|
||||
? `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; without, it's yours there), or to your own list (leave both ids out${workspace_id ? "; here, leaving them out makes it yours in this workspace" : ""}). Every task belongs to one bot. 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 ? `, or a member's as this workspace's admin (bot_id: ${team})` : ""}` : `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
|
||||
@@ -144,7 +137,7 @@ 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
|
||||
? `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.`
|
||||
? `Who has it: a worker's own list, or with workspace_id a bot working there. Leave out to have it yourself.`
|
||||
: team
|
||||
? `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.`,
|
||||
@@ -158,10 +151,6 @@ export default function createTaskTool({
|
||||
? `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 }) => {
|
||||
@@ -201,7 +190,7 @@ export default function createTaskTool({
|
||||
});
|
||||
|
||||
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`}`
|
||||
? `in workspace ${place.workspace_id}, ${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`;
|
||||
|
||||
@@ -25,8 +25,8 @@ type Params = {
|
||||
|
||||
/**
|
||||
* The task tools every run gets (`src/harness/tools/bot/tasks/`): the
|
||||
* 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,
|
||||
* to-do list (every task belongs to one bot: its own list and its tasks in
|
||||
* each workspace; an admin's whole 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 give tasks there to the other members.
|
||||
|
||||
@@ -27,14 +27,15 @@ type Args = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets a bot read the tasks it can work on: its own list and every
|
||||
* workspace it works in, or for the coordinator the whole install. Used
|
||||
* Lets a bot read the tasks it can work on: its own (its list and what it
|
||||
* has in workspaces), every task of a workspace it's admin of, 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, admin_workspace_ids, workspace_id }: Params): HarnessTool<Args> {
|
||||
return {
|
||||
name: "list_tasks",
|
||||
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.`,
|
||||
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 as a workspace's admin every task there)"}. 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"])
|
||||
@@ -74,7 +75,7 @@ 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 who = task.bot_name || `bot ${task.bot_id}`;
|
||||
const where = task.workspace_id
|
||||
? `in ${names.get(Number(task.workspace_id)) || `workspace ${task.workspace_id}`}, ${who}`
|
||||
: `on ${who}'s list`;
|
||||
|
||||
@@ -4,7 +4,6 @@ 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";
|
||||
@@ -41,7 +40,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. 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.`,
|
||||
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.`,
|
||||
schema: z.object({
|
||||
task_id: z.number().int(),
|
||||
status: z.enum(TaskStatusValues).optional(),
|
||||
@@ -68,15 +67,6 @@ 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