Let the coordinator create workspaces and write findings in tasks

create_workspace gives the coordinator a way to start a workspace with its shared context, and the tool list is shared, so a new id can be used by create_bot or add_bot_to_workspace in the same reply.

A coordinator's tasks now get create_workspace and update_workspace too: the research task that had the material could only report it, never write it into the workspace, so the coordinator promised an update it had no turn to make.

A workspace created from chat is pushed as server:workspace-created, from the tool and from the live relay, so the rail shows it without a reload.
This commit is contained in:
2026-10-19 14:32:47 +01:00
parent 1cafb40eef
commit 550aaeabac
12 changed files with 268 additions and 16 deletions
@@ -0,0 +1,90 @@
import { z } from "zod";
import type { BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import pushWorkspaceCreated from "@/src/bots-queue/push-workspace-created";
import logEvent from "@/src/functions/backend/logger";
import createWorkspace from "@/src/functions/backend/db/workspaces/create-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 live workspaces, which the new one is also added to at
* run time so later tool calls in the same reply can use its id.
*/
workspaces: BUN_SQLITE_COBBLE_WORKSPACES[];
};
type Args = {
name: string;
description?: string;
context?: string;
};
/**
* Lets the coordinator create a workspace: a place a group of bots works
* from shared context, with its own computers and models. Creating one is
* a structural change, so it only runs once the user has asked for it or
* agreed to it.
*/
export default function createWorkspaceTool({ user_id, workspaces }: Params): HarnessTool<Args> {
const list = workspaces.map((w) => `${w.id}: ${w.name}`).join("; ");
return {
name: "create_workspace",
description: `Create a workspace: a place where a group of bots works on one thing, sharing a context every bot there gets in its prompt, its computers and its models. Give it a short name, one line on what it's for, and the context (markdown: what the work is, who it's for, what everyone there should know). It starts empty: put bots in it with add_bot_to_workspace, or pass its id to create_bot. Only do this when the user has asked for it or agreed to it.${list ? ` Workspaces that already exist: ${list}.` : ""}`,
schema: z.object({
name: z.string().trim().min(1).max(60),
description: z.string().trim().max(240).optional(),
context: z.string().trim().max(AppData.PromptMaxLength).optional(),
}),
run: async ({ args }) => {
const existing = workspaces.find(
(w) => w.name?.trim().toLowerCase() == args.name.trim().toLowerCase(),
);
if (existing?.id) {
throw new Error(
`There's already a workspace called ${existing.name} (#${existing.id}). Change it with update_workspace instead, or pick another name.`,
);
}
const workspace_id = Number(
await createWorkspace({
user_id,
name: args.name,
description: args.description,
system_prompt: args.context,
}),
);
const created = (await grabUserWorkspaces({ user_id })).find(
(workspace) => workspace.id == workspace_id,
);
/**
* The tool list is built once per reply: adding the row here
* lets create_bot and add_bot_to_workspace use the new id in
* the same turn.
*/
if (created) {
workspaces.push(created);
}
await logEvent({
title: "Workspace created",
category: "system",
user_id: Number(user_id),
plain_text: `Bot created the workspace ${args.name}.`,
metadata: { workspace_id },
});
if (created) {
await pushWorkspaceCreated({ workspace: created });
}
return `Workspace #${workspace_id} "${args.name}" is ready. The user sees it in their rail. Add bots to it with add_bot_to_workspace, or pass ${workspace_id} to create_bot; its context can change later with update_workspace.`;
},
};
}
+10 -6
View File
@@ -7,6 +7,7 @@ import searchComputersTool from "../computer/search-computers-tool";
import addBotToWorkspaceTool from "./add-bot-to-workspace-tool";
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 grabBotTools from "./grab-bot-tools";
import updateWorkspaceTool from "./update-workspace-tool";
@@ -45,12 +46,13 @@ type Params = {
* in a workspace switching its default computer), start_task for longer
* work, search_computers to find what's on its computers,
* ask_user_to_take_over when only the user can do something on one, and
* for the coordinator (the super admin's alone) 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.
* 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.
*/
export default async function grabBotChatTools({
bot,
@@ -83,6 +85,8 @@ export default async function grabBotChatTools({
const workspaces = (await grabUserWorkspaces({ user_id })).filter((w) => !w.archived);
tools.push(createWorkspaceTool({ user_id, workspaces }));
if (workspaces.length) {
tools.push(updateWorkspaceTool({ user_id, workspaces, workspace_id }));
}