Add tasks: the crew's shared to-do list
Bots and workspaces each keep a list of work tracked until it's done, and the bots move it along themselves as they work. - tasks (one collection per bot or per workspace), task_notes (the progress log) and task_media (a deliverable or a reference file, its bytes a private media row served at /media/private/<id>) - statuses todo/doing/blocked/done/cancelled, with blocked_reason while only the user can unblock it - visibility as SQL in src/utils/task-access: a user's own bots and workspaces, a run's own list plus its workspaces, everything for the coordinator; every tool and endpoint resolves through it - list_tasks, create_task and update_task on every run, so a chat reply, a task and a routine all read the list, log progress and file what they produced - TasksNote on every run and TaskReminderNote in chat, so a greeting or a proposed next step brings pending work up in one measured line - tasks group on the workspace dashboard and a tasks modal in the conversation header, sharing one form, row and detail panel - a bot's or workspace's own chat uploads are read before the delete so none are stranded; bot files keep their folder Also corrects the README's RAM guidance and table alignment.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { z } from "zod";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { type TaskStatus, TaskStatusValues } from "@/src/dict/task-dict";
|
||||
import grabTasks from "@/src/functions/backend/db/tasks/grab-tasks";
|
||||
import type { HarnessTool } from "../../../types";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
is_coordinator: boolean;
|
||||
/**
|
||||
* The workspace the run is in, if any.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
};
|
||||
|
||||
type Args = {
|
||||
scope?: "mine" | "workspace" | "all";
|
||||
status?: TaskStatus;
|
||||
open_only?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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> {
|
||||
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.`,
|
||||
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".`),
|
||||
status: z.enum(TaskStatusValues).optional(),
|
||||
open_only: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(`Only the open ones (to do, doing, blocked). Defaults to false.`),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const scope = args.scope || "all";
|
||||
const only_workspace_id =
|
||||
scope == "workspace" && workspace_id ? Number(workspace_id) : undefined;
|
||||
|
||||
const tasks = await grabTasks({
|
||||
bot_id,
|
||||
is_coordinator,
|
||||
only_bot_id: scope == "mine" ? Number(bot_id) : undefined,
|
||||
only_workspace_id,
|
||||
statuses: args.status ? [args.status] : undefined,
|
||||
open_only: args.open_only || false,
|
||||
limit: AppData.TaskListToolLimit,
|
||||
});
|
||||
|
||||
if (!tasks.length) {
|
||||
return args.open_only
|
||||
? `You have no open tasks${args.status ? ` with that status` : ""}.`
|
||||
: `No tasks${args.status ? ` with that status` : ""}.`;
|
||||
}
|
||||
|
||||
const names = await grabWorkspaceNames({
|
||||
workspace_ids: tasks.map((task) => Number(task.workspace_id || 0)).filter(Boolean),
|
||||
});
|
||||
|
||||
return tasks
|
||||
.map((task) => {
|
||||
const where = task.workspace_id
|
||||
? `in ${names.get(Number(task.workspace_id)) || `workspace ${task.workspace_id}`}`
|
||||
: `on ${task.bot_name || `bot ${task.bot_id}`}`;
|
||||
const bits = [
|
||||
`#${task.id}`,
|
||||
`[${task.status}]`,
|
||||
task.title,
|
||||
`(${where})`,
|
||||
Number(task.media_count || 0) ? `${task.media_count} file(s)` : "",
|
||||
task.status == "blocked" && task.blocked_reason ? `blocked: ${task.blocked_reason}` : "",
|
||||
task.latest_note ? `latest: ${oneLine({ text: task.latest_note })}` : "",
|
||||
];
|
||||
|
||||
return `- ${bits.filter(Boolean).join(" ")}`;
|
||||
})
|
||||
.join("\n");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A note or reason on one line, so a list stays a list.
|
||||
*/
|
||||
function oneLine({ text }: { text: string }) {
|
||||
return String(text).replace(/\s+/g, " ").trim().slice(0, 160);
|
||||
}
|
||||
|
||||
async function grabWorkspaceNames({ workspace_ids }: { workspace_ids: number[] }) {
|
||||
const names = new Map<number, string>();
|
||||
|
||||
if (!workspace_ids.length) return names;
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_WORKSPACES>({
|
||||
sql: `SELECT id, name FROM workspaces WHERE id IN (${workspace_ids.map(() => "?").join(", ")})`,
|
||||
values: workspace_ids,
|
||||
});
|
||||
|
||||
for (const workspace of res.payload || []) {
|
||||
if (workspace.id) names.set(Number(workspace.id), workspace.name || `workspace ${workspace.id}`);
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
Reference in New Issue
Block a user