Add computer switching, crew reporting, job steps and markdown chat
- Bots have a current computer (bots.computer_id), switched from the chat header beside the model button or by clicking a computer the bot names ([computer:ID]); tasks run on it by default - search_computers runs one read-only command on all of a bot's computers; chat replies know their computers (compose-computers-note) - The coordinator can use every computer and grants or revokes crew access with assign_computer - Delegated jobs record requested_by_bot_id; the worker's result or failure is reported back in the coordinator's conversation - bots_queue_updates records each model turn of a job, pushed live and shown in the working-on panel - Live relay: every process pushes new messages, job steps and bot changes from the database to its own sockets, so work done elsewhere shows live - Bot messages render as markdown (react-markdown + remark-gfm, no raw HTML) with twui components; rail previews are plain text - Fix header button overflow in the model and computer selects - Bump @moduletrace/twui to 0.1.2 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { z } from "zod";
|
||||
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import grabComputerMcpUrl from "@/src/functions/backend/db/computers/grab-computer-mcp-url";
|
||||
import grabComputerEnvs from "@/src/functions/backend/db/envs/grab-computer-envs";
|
||||
import computerRef from "@/src/utils/computer-ref";
|
||||
import redactSecrets from "@/src/utils/redact-secrets";
|
||||
import type { HarnessTool } from "../../types";
|
||||
import runComputerCommand from "./run-computer-command";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* The bot's computers, the current one first (`grabBotComputers`).
|
||||
*/
|
||||
computers: BUN_SQLITE_COBBLE_COMPUTERS[];
|
||||
};
|
||||
|
||||
type Args = {
|
||||
command: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets a bot look for something across every computer it has: one shell
|
||||
* command runs on each at once (`runComputerCommand`), and the outputs come
|
||||
* back labelled with each computer's ref (`computerRef`), secrets redacted,
|
||||
* so the bot can tell the user exactly which computer holds what.
|
||||
*/
|
||||
export default function searchComputersTool({
|
||||
bot_id,
|
||||
computers,
|
||||
}: Params): HarnessTool<Args> {
|
||||
return {
|
||||
name: "search_computers",
|
||||
description: `Run one read-only shell command (bash) on every computer you have at once, to find which one holds something: a file, a running app, a logged-in browser session, a setting. Each computer's output comes back under its ref, e.g. ${computerRef({ id: computers[0]?.id || 1 })}. For a logged-in site, look for its domain in the browser profiles' cookie databases (copy the file first, since the browser locks it) and list only host names, never cookie values. Don't change anything with it; use start_task for that. Output past ${AppData.ComputerSearchMaxOutputChars} characters per computer is cut, so filter in the command.`,
|
||||
schema: z.object({
|
||||
command: z.string().min(1).max(4000),
|
||||
}),
|
||||
run: async ({ args, signal }) => {
|
||||
const results = await Promise.all(
|
||||
computers.map(async (computer) => {
|
||||
const label = `## ${computerRef({ id: computer.id || 0 })} ${computer.name || "Computer"}`;
|
||||
|
||||
try {
|
||||
const target = await grabComputerMcpUrl({ computer_id: computer.id || 0 });
|
||||
|
||||
if (!target) return `${label}\nNot reachable: it has no container.`;
|
||||
|
||||
const [result, envs] = await Promise.all([
|
||||
runComputerCommand({
|
||||
base_url: target.base_url,
|
||||
command: args.command,
|
||||
timeout_ms: AppData.ComputerSearchTimeoutMs,
|
||||
signal,
|
||||
}),
|
||||
grabComputerEnvs({ computer_id: computer.id || 0, bot_id }),
|
||||
]);
|
||||
|
||||
const output = redactSecrets({
|
||||
text: result.output.trim(),
|
||||
secrets: envs.filter((env) => env.secret),
|
||||
});
|
||||
const cut = output.length > AppData.ComputerSearchMaxOutputChars;
|
||||
|
||||
return [
|
||||
label,
|
||||
`Exit code: ${result.exit_code ?? "unknown"}`,
|
||||
output ? output.slice(0, AppData.ComputerSearchMaxOutputChars) : "(no output)",
|
||||
cut ? `(output cut at ${AppData.ComputerSearchMaxOutputChars} characters)` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
} catch (error) {
|
||||
return `${label}\nNot reachable: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return results.join("\n\n");
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user