Let a delegated task choose the workspace it runs in
delegate_task had no workspace argument: a task handed over from the coordinator's own chat always ran in the worker's own chat, on that worker's memory, files and directly assigned computers alone, even when the work belonged to a workspace. The coordinator could not say where the work goes, and the prompt never said the two differ. - delegate_task takes an optional workspace_id: the worker runs the task in that workspace's context, with its own instructions and memory there, the project's files and the workspace's computers, and the result lands in its conversation in that workspace. - A worker missing from that workspace is added first (pushBotCreated) and the reply says so. - Handed over from inside a workspace the task always runs there; a different workspace_id is refused instead of ignored. - The coordinator prompt, CoordinatorReplyNote and the tool description now spell the distinction out.
This commit is contained in:
@@ -196,7 +196,7 @@ Every folder on the data volume, browsable read-only: a bot's own, a workspace's
|
||||
- **Automatic picks** (`rank-auto-models`): only for the `automatic` harnesses in `Harnesses` (text, vision, image); transcription and speech have none and are not available until a model is picked (the model select reads "Not available", no fallback checkbox). Free models that can do the job (on the setting's provider when it has one; a provider without free models needs a picked model) (`model-serves-harness`: text needs tools), by vendor `AutoModelVendorPreference` (DeepSeek, Qwen, NVIDIA, Liquid), then other vendors that look competent (`AppData.HarnessAutoMin*`), then routers; within a vendor by intelligence index, then full-size before small, newest first.
|
||||
- **Calling:** `resolveHarnessCandidates` (pinned, then automatic) -> `withCandidates` tries them in order with process-wide cooldowns (`resolve/harness-cooldowns.ts`): rate limits and outages rest the model (until `Retry-After` when given), 401/402 rest the whole provider, 403/404 rest the model for an hour. Errors are `HarnessError` (`code` provider|unavailable|aborted, `retryable`), classified in `to-harness-error.ts`. Entry points: `text/generate-text.ts` (one turn), `text/run-text-loop.ts` (tool loop, sticks to one model, wraps up after `HarnessMaxToolSteps`, trims the oldest tool output before each turn to fit the model's context, `fit-transcript-to-context.ts`), `media/read-image.ts`, `media/read-media.ts` (documents locally via `fileToText`, images and audio through their harnesses), `audio/transcribe-audio.ts`, `audio/speak-text.ts` (returns the provider's audio as a stream, falling back to the next model only until one answers), `image/generate-image.ts`, `embeddings/embed-text.ts` (batches, then `fit-vector-dimensions.ts` pads or cuts each vector to `AppData.EmbeddingDimensions` and normalizes it, since a provider's width varies). A text model that can't take an image or audio part gets it read by the vision/transcription harness first (`prepare-messages-for-model.ts`).
|
||||
- **Per-bot models:** in a workspace, its `workspace_harness_settings` row wins (`source: "workspace"`). Otherwise a worker bot inherits the defaults per harness unless it has a `bot_harness_settings` row (same fields as `harness_settings`, unique per bot and harness). `grabHarnessSetting({ harness, bot_id })` returns the bot's row (`source: "bot"`) or the default; the coordinator always gets the default. Every entry point takes `bot_id` and passes it on (text loop -> media reading/transcription), so a bot's run uses its own models throughout. Saved by `saveBotHarnessSetting` (`inherit` deletes the row); the page data is `grabBotHarnessOverview`. A bot's chat header has a ghost model icon (`(partials)/header-icon-button.tsx`; the text model and provider show under the bot's name) that opens a modal with every harness (`(partials)/bot-models-modal.tsx`); the modal and `/admin/bots/[bot_id]/models` both use `(partials)/bot-harness-form.tsx`. On the coordinator the modal edits the defaults themselves (`api/admin/harness/save-settings`, the same settings as Settings, under Models, without the providers): super admins only, the icon is disabled for everyone else. Every one of these wraps `settings/models/(partials)/harness-settings-form.tsx`: per harness a provider select first (`harness-provider-select.tsx`: any provider, one provider, or on a bot "Same as the default"), then the model select narrowed to it. Removing a provider (`deleteHarnessProvider`) puts default and bot settings that picked it or its models back on any provider (`save` prop; "Same as the default" shows when the overview item has `default`).
|
||||
- **Tools:** `HarnessTool` (built per run with its bot bound): own tools validate with a zod `schema`; tools described elsewhere pass a JSON schema as `parameters` and validate themselves. `run-tool-call.ts` reports bad calls and failures back to the model as `Error: ...`, and caps every tool call at `AppData.HarnessToolTimeoutMs` (aborted, its result handed back as an error the model can work around). Bot tools in `src/harness/tools/storage/`: `list_files`, `read_file`, `write_file`, `search_files`, `move_file`, `delete_file`, `ingest_file`, `copy_file`, `generate_image`, `make_pdf` (`grabBotFileTools`, every run, see Bot files and folders). Bot tools in `src/harness/tools/bot/`: `update_memory`, `update_instructions` (`grabBotTools`, every run), `start_task` and, for the coordinator with workers, `delegate_task` (`grabBotChatTools`, chat replies only; the job's `requested_by_bot_id` is the coordinator, and `relayJobToRequester` posts the worker's result or failure in its conversation, pushed as `relay` by `pushBotNotice`), `ask_user_to_take_over` (see Take-over), plus for the coordinator `create_workspace` and `update_workspace` (a new workspace with its shared context, or changing one; both also in its tasks, so a task that gathers material can write it into the workspace), `create_bot` (add a worker with its name, role and instructions, and join it to a workspace straight away) and, in its own chat, `add_bot_to_workspace` (put an existing worker in one of the user's workspaces) and `remove_bot_from_workspace` (take it out, keeping its conversation there), `assign_computer` (grant or revoke a worker's direct access, `add/remove-bot-to/from-computer`; access through a workspace stays and the result says so), `update_bot` (change a worker's name, role or core instructions, `src/functions/backend/db/bots/update-bot.ts`), `find_bot` and `find_workspace` (look the crew, or the live workspaces, up by id, name, role/description or instructions/context), `delete_bot` (a worker with its conversations, tasks, schedules and workspace places; the coordinator can't go) and `delete_workspace` (its conversations, tasks, schedules and links; its bots and computers stay). `search_computers` (`src/harness/tools/computer/search-computers-tool.ts`, chat replies and tasks) runs one read-only command on all of a bot's computers at once over the sandbox REST API (`run-computer-command.ts`), each output under its `[computer:ID]`, secrets redacted, capped by `AppData.ComputerSearch*`.
|
||||
- **Tools:** `HarnessTool` (built per run with its bot bound): own tools validate with a zod `schema`; tools described elsewhere pass a JSON schema as `parameters` and validate themselves. `run-tool-call.ts` reports bad calls and failures back to the model as `Error: ...`, and caps every tool call at `AppData.HarnessToolTimeoutMs` (aborted, its result handed back as an error the model can work around). Bot tools in `src/harness/tools/storage/`: `list_files`, `read_file`, `write_file`, `search_files`, `move_file`, `delete_file`, `ingest_file`, `copy_file`, `generate_image`, `make_pdf` (`grabBotFileTools`, every run, see Bot files and folders). Bot tools in `src/harness/tools/bot/`: `update_memory`, `update_instructions` (`grabBotTools`, every run), `start_task` and, for the coordinator with workers, `delegate_task` (`grabBotChatTools`, chat replies only; a task handed over in the coordinator's own chat runs in the worker's own chat unless it names a `workspace_id`, which puts it in that workspace: the worker's workspace instructions and memory, the workspace's context, files and computers apply, and it needs membership, added first via `addBotToWorkspace` when missing; handed over from inside a workspace it always runs there and a different `workspace_id` is refused; the job's `requested_by_bot_id` is the coordinator, and `relayJobToRequester` posts the worker's result or failure in its conversation, pushed as `relay` by `pushBotNotice`), `ask_user_to_take_over` (see Take-over), plus for the coordinator `create_workspace` and `update_workspace` (a new workspace with its shared context, or changing one; both also in its tasks, so a task that gathers material can write it into the workspace), `create_bot` (add a worker with its name, role and instructions, and join it to a workspace straight away) and, in its own chat, `add_bot_to_workspace` (put an existing worker in one of the user's workspaces) and `remove_bot_from_workspace` (take it out, keeping its conversation there), `assign_computer` (grant or revoke a worker's direct access, `add/remove-bot-to/from-computer`; access through a workspace stays and the result says so), `update_bot` (change a worker's name, role or core instructions, `src/functions/backend/db/bots/update-bot.ts`), `find_bot` and `find_workspace` (look the crew, or the live workspaces, up by id, name, role/description or instructions/context), `delete_bot` (a worker with its conversations, tasks, schedules and workspace places; the coordinator can't go) and `delete_workspace` (its conversations, tasks, schedules and links; its bots and computers stay). `search_computers` (`src/harness/tools/computer/search-computers-tool.ts`, chat replies and tasks) runs one read-only command on all of a bot's computers at once over the sandbox REST API (`run-computer-command.ts`), each output under its `[computer:ID]`, secrets redacted, capped by `AppData.ComputerSearch*`.
|
||||
- **MCP:** `src/harness/mcp/`: `connectMcpServer` (Streamable HTTP, SSE fallback), `grabMcpTools` (each server tool becomes a `HarnessTool` with its JSON schema), `mcpResultToText` (text as is, images read by the vision harness, `isError` thrown). Computers are reached this way: `openComputerTools` (`src/harness/tools/computer/`) connects to the computer's AIO sandbox at `grabComputerMcpUrl` (the `MAIN_COMPUTER_URL` app setting for the main computer, for a dev server outside docker, e.g. `http://localhost:8170`, else `http://<hostname>:<port>/mcp`, or its IP address when the hostname can't be reached), leaves out `ComputerMcpExcludedTools`, and sets the computer's envs (see Envs).
|
||||
- **MCPs and custom tools (extensions):** installs add their own MCP servers (`mcp_servers`, Settings, under MCPs) and tools (`custom_tools`, Settings, under Tools). A bot gets the ones given to it (`bot_mcp_servers`, `bot_custom_tools`) and, in a workspace, that workspace's (`workspace_mcp_servers`, `workspace_custom_tools`); `botExtensionsSql` (`src/utils/bot-extensions-sql.ts`, tested) resolves a run's, `grabBotExtensions` reads them. `all_bots` ("every bot by default") only decides what bots are given: turning it on gives it to every bot (`attachToAllBots`, the coordinator included) and every new bot gets it (`attachDefaultExtensions`, from `createBot` and `ensureCoordinatorBot`); turning it off takes it from no one, and a bot that drops one keeps it dropped (`src/scripts/attach-every-bot-extensions.ts`, safe to rerun, moved installs from the old "reaches every bot" meaning). Super admins define them. Super admins and admins (the super admin alone for the coordinator, `assertBotExtensionManager`) give and take them per bot on the bot's settings page and under its chat's composer (`composer-tools.tsx`: one "N tools" button (`composer-tools-modal.tsx`) counting everything it can use there, which opens a modal listing them (a workspace's not removable) with the add picker, so ten of them don't fill the composer; beside it `composer-computer-button.tsx` opens the bot's current computer in the viewer), and per workspace on the workspace's settings page; the settings pages share `settings/(partials)/extensions-group.tsx`, all through `useExtensions` (`src/hooks/`), `grabBotExtensionsOverview` / `grabWorkspaceExtensionsOverview` (`bot_extensions` / `workspace_extensions` page props, both on `grabExtensionsOverview`) and `api/admin/bots/` and `api/admin/workspaces/` `add-extension`, `remove-extension` (`setBotExtension`, `setWorkspaceExtension`); the MCPs and Tools pages' Access modal sets bots and workspaces in bulk (`setExtensionAccess`, an admin never touches the coordinator's or `all_bots`). Helpers in `src/functions/backend/db/extensions/`, API in `api/admin/mcps/` and `api/admin/tools/`, zod in `src/schemas/extension-schema.ts`.
|
||||
- **Skills** are the third extension kind (`skills`, Settings, under Skills; `bot_skills` / `workspace_skills`, same `all_bots` rules, `ExtensionTables.kind = "skill"`). A skill is a runbook: `name`, a unique `slug` a bot loads it by, a one-line `description` (when it applies) and the markdown `body`, capped by `AppData.SkillBodyMaxChars`. Unlike an MCP server or a tool it has no `status`/`last_error`: nothing connects or runs, and it never reports a failure (`ExtensionRow` and `ComposerToolRow` key the state line off `kind == "skill"`). A run sees only the slug and description; `openBotSkills` (`src/harness/tools/skills/`, fed by the `skills` that `grabBotExtensions` already reads) adds one `load_skill` tool that returns the body, cut at `AppData.SkillBodyShownChars` with a line saying so, and throws the valid slugs for an unknown one. Slugs are checked by `checkSkillSlug` (`src/utils/check-skill-slug.ts`, tested); `load_skill` is in `ReservedToolNames` so a custom tool can't shadow it. The settings page is `api/admin/skills/` (`save-skill`, `delete-skill` super admin only, `set-skill-access`), `grabSkillsOverview`; `deleteSkill` deletes the two link tables itself, since foreign keys are off outside the server process. `openBotExtensions` (`src/harness/tools/extensions/`) turns them into harness tools for chat replies and tasks (built-ins win a name clash; `close()` in the runners' `finally`; `note` tells the bot about a server it can't use). Templates (`fillTemplate`, `src/utils/fill-template.ts`): `{{args.x}}` and `{{env.KEY}}`, envs from `grabRunEnvs` (the bot's own beat the workspace's, which beat general; app settings never, no need to be set on computers); secret values are redacted from every result.
|
||||
|
||||
@@ -27,6 +27,9 @@ Permission to draft is not permission to send, permission to analyze is not perm
|
||||
## 2. Delegate well
|
||||
|
||||
- Pick by role, instructions and tools, not by name.
|
||||
- Say where the work runs, and mean it. \`delegate_task\` takes an optional \`workspace_id\`, and that one argument decides the whole context the worker has: its own chat, or a workspace. A task in the worker's own chat has only that worker's own memory, files and directly assigned computers. A task in a workspace has the workspace's shared context, the worker's instructions and memory for that workspace, the project's files, and the workspace's computers as well as the worker's. The result also lands in a different conversation: the worker's own chat, or its conversation in that workspace beside the rest of that project. Choose by where the work belongs, not by which is easier to type.
|
||||
- Handed over from a conversation inside a workspace, the task always runs in that workspace and \`workspace_id\` is not accepted: say so, don't pretend you moved it.
|
||||
- Delegating into a workspace the worker isn't in adds it to that workspace as a side effect. Say that in your reply when it happens; don't add members silently for a one-off.
|
||||
- Give a full brief: objective, inputs, deliverable, acceptance criteria, constraints.
|
||||
- Check \`list_tasks\` first. Never give two bots the same job, and don't restart work an open task already covers.
|
||||
- Track what comes back, judge it against the request, and integrate it. Don't pass a worker's unchecked claim on as fact.
|
||||
|
||||
@@ -115,7 +115,7 @@ Every action counts here: saving memory or instructions, creating or changing a
|
||||
/**
|
||||
* Added to the coordinator's chat prompt: it hands work to the crew.
|
||||
*/
|
||||
export const CoordinatorReplyNote = `As the coordinator, hand work that fits a crew member's role to them with delegate_task. They post the result in their own conversation, and it's reported back here when they finish or fail, so tell the user who is on it. When a report comes back, pass on what matters to the user, follow up on anything left, and propose what comes next: the next piece of work, which bot would take it, or a question that settles the direction. Once the user agrees, you can also grow the crew with create_bot, put a worker in a workspace with add_bot_to_workspace, and create or change a workspace with create_workspace and update_workspace. When you name a crew member, write its ref instead of its name, e.g. ${botRef({ id: 3 })}: the user sees it as the bot's name, linked to its conversation.`;
|
||||
export const CoordinatorReplyNote = `As the coordinator, hand work that fits a crew member's role to them with delegate_task. They post the result in their own conversation, and it's reported back here when they finish or fail, so tell the user who is on it and where: a task with no workspace_id runs in that bot's own chat, on its own memory, files and directly assigned computers; one with workspace_id runs in the workspace, on the workspace's context, files and computers as well as the bot's, and lands in that project's conversation. Never swap one for the other without saying so. When a report comes back, pass on what matters to the user, follow up on anything left, and propose what comes next: the next piece of work, which bot would take it, or a question that settles the direction. Once the user agrees, you can also grow the crew with create_bot, put a worker in a workspace with add_bot_to_workspace, and create or change a workspace with create_workspace and update_workspace. When you name a crew member, write its ref instead of its name, e.g. ${botRef({ id: 3 })}: the user sees it as the bot's name, linked to its conversation.`;
|
||||
|
||||
/**
|
||||
* Added to the coordinator's chat prompt instead while it has no workers,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import delegateTaskTool from "./delegate-task-tool";
|
||||
|
||||
const worker = { id: 5, name: "Researcher", role: "research", kind: "worker" } as BUN_SQLITE_COBBLE_BOTS;
|
||||
const workspace = { id: 3, name: "Launch", archived: 0 } as BUN_SQLITE_COBBLE_WORKSPACES;
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
function coordinatorTool({
|
||||
workspace_id,
|
||||
workers = [worker],
|
||||
workspaces = [workspace],
|
||||
}: { workspace_id?: number; workers?: BUN_SQLITE_COBBLE_BOTS[]; workspaces?: BUN_SQLITE_COBBLE_WORKSPACES[] } = {}) {
|
||||
return delegateTaskTool({
|
||||
user_id: 1,
|
||||
coordinator_id: 1,
|
||||
session_id: 10,
|
||||
workspace_id: workspace_id ?? null,
|
||||
coordinator_name: "Coordinator",
|
||||
workers,
|
||||
workspaces,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The tool's own confines, kept to what needs no database: the argument it
|
||||
* accepts and the refusal that keeps a workspace hand-over in its
|
||||
* workspace. Every run that gets past those reaches the DB.
|
||||
*/
|
||||
describe("delegateTaskTool", () => {
|
||||
test("takes an optional workspace the task runs in", () => {
|
||||
const tool = coordinatorTool();
|
||||
|
||||
expect(tool.name).toBe("delegate_task");
|
||||
expect(tool.schema?.safeParse({ bot_id: 5, title: "Dig", instructions: "Read the docs." }).success).toBe(true);
|
||||
expect(
|
||||
tool.schema?.safeParse({ bot_id: 5, title: "Dig", instructions: "Read the docs.", workspace_id: 3 }).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("names the workspaces it can hand work into", () => {
|
||||
expect(coordinatorTool().description).toContain("3: Launch");
|
||||
});
|
||||
|
||||
test("in a workspace, says the task runs there and offers no other", () => {
|
||||
const tool = coordinatorTool({ workspace_id: 3 });
|
||||
|
||||
expect(tool.description).toContain("runs there");
|
||||
expect(tool.schema?.safeParse({ bot_id: 5, title: "Dig", instructions: "x", workspace_id: 9 }).success).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("refuses to move a hand-over out of the workspace it came from", async () => {
|
||||
await expect(
|
||||
coordinatorTool({ workspace_id: 3 }).run({
|
||||
args: { bot_id: 5, title: "Dig", instructions: "x", workspace_id: 9 },
|
||||
signal,
|
||||
}),
|
||||
).rejects.toThrow(/runs in that workspace/);
|
||||
});
|
||||
|
||||
test("accepts the workspace it came from", async () => {
|
||||
/** Reaches the DB and fails there for a test id, never on the guard. */
|
||||
await expect(
|
||||
coordinatorTool({ workspace_id: 3 }).run({
|
||||
args: { bot_id: 5, title: "Dig", instructions: "x", workspace_id: 3 },
|
||||
signal,
|
||||
}),
|
||||
).rejects.not.toThrow(/runs in that workspace/);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import pushBotCreated from "@/src/bots-queue/push-bot-created";
|
||||
import pushBotNotice from "@/src/bots-queue/push-bot-notice";
|
||||
import enqueueBotJob from "@/src/functions/backend/db/bots-queue/enqueue-bot-job";
|
||||
import grabOpenBotTask from "@/src/functions/backend/db/bots-queue/grab-open-bot-task";
|
||||
import addBotToWorkspace from "@/src/functions/backend/db/workspaces/add-bot-to-workspace";
|
||||
import grabUserWorkspaces from "@/src/functions/backend/db/workspaces/grab-user-workspaces";
|
||||
import grabWorkspaceBots from "@/src/functions/backend/db/workspaces/grab-workspace-bots";
|
||||
import insertBotMessage from "@/src/functions/backend/db/bots/insert-bot-message";
|
||||
import grabCrewWorkers from "@/src/functions/backend/db/bots/grab-crew-workers";
|
||||
import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-computers";
|
||||
@@ -32,19 +36,32 @@ type Params = {
|
||||
* The worker bots it can hand work to (in a workspace, its members).
|
||||
*/
|
||||
workers: BUN_SQLITE_COBBLE_BOTS[];
|
||||
/**
|
||||
* The user's live workspaces, for the picker. In a workspace there's
|
||||
* nothing to pick: the task runs there.
|
||||
*/
|
||||
workspaces?: BUN_SQLITE_COBBLE_WORKSPACES[];
|
||||
};
|
||||
|
||||
type Args = {
|
||||
bot_id: number;
|
||||
title: string;
|
||||
instructions: string;
|
||||
/**
|
||||
* The workspace the worker runs the task in, from the coordinator's own
|
||||
* chat. Left out, the worker runs it in its own chat, on its own
|
||||
* memory and its direct computers only.
|
||||
*/
|
||||
workspace_id?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets the coordinator hand a task to a worker bot. The worker runs it on
|
||||
* the computer it's on (if it has one) and posts the result in its own
|
||||
* conversation, where a note says who asked; the result (or failure) is
|
||||
* also reported back in the coordinator's conversation.
|
||||
* 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.
|
||||
* 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
|
||||
* conversation.
|
||||
*/
|
||||
export default function delegateTaskTool({
|
||||
user_id,
|
||||
@@ -53,33 +70,79 @@ export default function delegateTaskTool({
|
||||
workspace_id,
|
||||
coordinator_name,
|
||||
workers,
|
||||
workspaces = [],
|
||||
}: Params): HarnessTool<Args> {
|
||||
const crew = workers
|
||||
.map((bot) => `${bot.id}: ${bot.name}${bot.role ? ` (${bot.role})` : ""}`)
|
||||
.join("; ");
|
||||
const places = workspaces
|
||||
.map((w) => `${w.id}: ${w.name}`)
|
||||
.join("; ");
|
||||
|
||||
return {
|
||||
name: "delegate_task",
|
||||
description: `Hand a task to a crew member. They run it in the background and post the result in their own conversation; it's also reported back here when they finish or fail. Write the instructions as a full brief: what to do, where, and how they'll know it's done. Crew: ${crew}.`,
|
||||
description: `Hand a task to a crew member. They run it in the background and post the result in their own conversation; it's also reported back here when they finish or fail. Write the instructions as a full brief: what to do, where, and how they'll know it's done. Crew: ${crew}.${
|
||||
workspace_id
|
||||
? ` You are working in a workspace, so the task runs there: in this workspace's context.`
|
||||
: ` Leave workspace_id out to run it in the crew member's own chat: only its own memory, files and directly assigned computers apply there, and the user finds the result by talking to it on its own. Add workspace_id to run it in one of the user's workspaces instead: the workspace's shared context, the member's instructions and memory there, its files and its computers (the workspace's own and the member's) all apply, and the result lands in its conversation in that workspace, beside the project it belongs to. Pick a workspace when the work belongs to that project; the member is added to it when it isn't in it yet. Workspaces: ${places || "none"}.`
|
||||
}`,
|
||||
schema: z.object({
|
||||
bot_id: z.number().int(),
|
||||
title: z.string().min(1).max(80),
|
||||
instructions: z.string().min(1).max(8000),
|
||||
workspace_id: z
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.describe(
|
||||
workspace_id
|
||||
? "Not available here: a task handed over in a workspace runs in that workspace."
|
||||
: "Run the task in this workspace: its shared context, memory, files and computers apply. Leave it out to run it in the crew member's own chat.",
|
||||
),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const fresh = await grabCrewWorkers({ workspace_id });
|
||||
/**
|
||||
* In a workspace the task can't leave it: the run is already
|
||||
* there, and that is the whole point of the hand-over.
|
||||
*/
|
||||
if (workspace_id && args.workspace_id && args.workspace_id != Number(workspace_id)) {
|
||||
throw new Error(
|
||||
`A task handed over from a workspace runs in that workspace. Leave workspace_id out, or hand it over from your own chat.`,
|
||||
);
|
||||
}
|
||||
|
||||
const target_workspace_id = workspace_id
|
||||
? Number(workspace_id)
|
||||
: args.workspace_id
|
||||
? Number(args.workspace_id)
|
||||
: null;
|
||||
|
||||
const fresh = await grabCrewWorkers({ workspace_id: target_workspace_id });
|
||||
const worker = fresh.find((bot) => bot.id == args.bot_id);
|
||||
|
||||
if (!worker?.id) {
|
||||
throw new Error(
|
||||
`No crew member has id ${args.bot_id}. Crew: ${fresh
|
||||
.map((bot) => `${bot.id}: ${bot.name}`)
|
||||
.join("; ") || "none"}.`,
|
||||
`No crew member has id ${args.bot_id}${
|
||||
target_workspace_id ? ` in workspace ${target_workspace_id}` : ""
|
||||
}. Crew: ${
|
||||
fresh.map((bot) => `${bot.id}: ${bot.name}`).join("; ") || "none"
|
||||
}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const computers = await grabBotComputers({ bot_id: worker.id, workspace_id });
|
||||
const worker_session = await ensureBotSession({ user_id, bot_id: worker.id, workspace_id });
|
||||
const joined = target_workspace_id
|
||||
? await ensureWorkerInWorkspace({ user_id, worker, target_workspace_id })
|
||||
: "";
|
||||
|
||||
const computers = await grabBotComputers({
|
||||
bot_id: worker.id,
|
||||
workspace_id: target_workspace_id,
|
||||
});
|
||||
const worker_session = await ensureBotSession({
|
||||
user_id,
|
||||
bot_id: worker.id,
|
||||
workspace_id: target_workspace_id,
|
||||
});
|
||||
const open = await grabOpenBotTask({
|
||||
bot_id: worker.id,
|
||||
session_id: Number(worker_session.id),
|
||||
@@ -108,12 +171,58 @@ export default function delegateTaskTool({
|
||||
message: await insertBotMessage({
|
||||
bot_id: worker.id,
|
||||
session_id: Number(worker_session.id),
|
||||
workspace_id: workspace_id ? Number(workspace_id) : null,
|
||||
workspace_id: target_workspace_id,
|
||||
text: `${coordinator_name} asked me to: **${args.title}**`,
|
||||
}),
|
||||
});
|
||||
|
||||
return `Task #${job_id} is with ${worker.name}. Their result will be in their conversation and reported back here.`;
|
||||
return `Task #${job_id} is with ${worker.name}${
|
||||
target_workspace_id ? ` in workspace ${target_workspace_id}` : " in their own chat"
|
||||
}.${joined} Their result will be in that conversation and reported back here.`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type EnsureParams = {
|
||||
user_id: number | string;
|
||||
worker: BUN_SQLITE_COBBLE_BOTS;
|
||||
target_workspace_id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A worker can only run a task in a workspace it's in, and a workspace's
|
||||
* members were already listed by `grabCrewWorkers`. Anyone else is added
|
||||
* first, since delegation is exactly the moment the broker decides who
|
||||
* does the work where.
|
||||
*/
|
||||
async function ensureWorkerInWorkspace({
|
||||
user_id,
|
||||
worker,
|
||||
target_workspace_id,
|
||||
}: EnsureParams) {
|
||||
if (!worker.id) return "";
|
||||
|
||||
const workspaces = (await grabUserWorkspaces({ user_id })).filter((w) => !w.archived);
|
||||
const workspace = workspaces.find((w) => w.id == target_workspace_id);
|
||||
|
||||
if (!workspace) {
|
||||
throw new Error(
|
||||
`No workspace has id ${target_workspace_id}. Workspaces: ${
|
||||
workspaces.map((w) => `${w.id}: ${w.name}`).join("; ") || "none"
|
||||
}. Hand the task over without workspace_id to run it in the crew member's own chat.`,
|
||||
);
|
||||
}
|
||||
|
||||
const members = await grabWorkspaceBots({ user_id, workspace_id: target_workspace_id });
|
||||
|
||||
if (members.some((bot) => bot.id == worker.id)) return "";
|
||||
|
||||
await addBotToWorkspace({
|
||||
user_id,
|
||||
workspace_id: target_workspace_id,
|
||||
bot_id: Number(worker.id),
|
||||
});
|
||||
await pushBotCreated({ bot: worker });
|
||||
|
||||
return ` ${worker.name} wasn't in "${workspace.name}", so it was added to it first.`;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,9 @@ type Params = {
|
||||
* 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, find_bot and find_workspace,
|
||||
* workspace, where the task runs; from its own chat, in the worker's own
|
||||
* chat or in a workspace it names) plus, in its own chat, find_bot and
|
||||
* find_workspace,
|
||||
* add_bot_to_workspace and remove_bot_from_workspace, assign_computer,
|
||||
* update_bot, delete_bot and delete_workspace (which workspace, which
|
||||
* computers, and which name, role and core instructions each crew member
|
||||
@@ -150,6 +152,7 @@ export default async function grabBotChatTools({
|
||||
workspace_id,
|
||||
coordinator_name: bot.name || "The coordinator",
|
||||
workers,
|
||||
workspaces,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user