Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78503208e8 | ||
|
|
8890d80e6e |
@@ -115,6 +115,7 @@ Enables SQLite foreign keys (`enableForeignKeys`), `bunextInit()`, `ensureDocker
|
||||
- Handler `src/bots-queue/index.ts`: separate reply/task slots, heartbeat, per-attempt deadline (`AppData.BotJobAttemptTimeoutMs`), one task per computer and one reply per session (`createKeyLock`). Work in `run-bot-job.ts` -> `run-bot-reply.ts` / `run-bot-task.ts`. Throw `BotJobFatalError` for failures retrying can't fix.
|
||||
- Every model turn is a step in `bots_queue_updates` (tool names, never arguments; `describe-job-step.ts`, labels in `JobStepToolLabels`), pushed as `server:bot-job-update`.
|
||||
- **A bot can't vouch for itself:** a run's tool calls (`tool_uses`, names and ok, never arguments) go to the wrap-up turn when steps run out and onto the result message as `bot_messages.tool_log`; history follows each recorded bot message with an `[App record: ...]` line (`botMessagesToHarness`), and copies of that line in an answer are stripped (`stripAppRecords`).
|
||||
- **`start_task`/`delegate_task` jobs are runs, not tasks:** their results say `Run #<job id>` and that `list_tasks` doesn't show them (bots sent users to the tracker for them). `start_task` refuses a second open run in its session on the same computer unless `separate` (it would only wait behind it) and says how many runs are ahead on that computer.
|
||||
- **Replies take in messages sent mid-run** (`incoming`, `IncomingMessagesNote`, redraft); separate requests are acknowledged with `send_message` (interim). A queued reply finishes silently only when a finished reply already read every user message (`hasUnansweredMessage`), never by who wrote last.
|
||||
|
||||
### Live updates
|
||||
@@ -155,9 +156,9 @@ A secret URL, `POST /api/hooks/<token>` (`src/pages/api/hooks/[token].ts`, publi
|
||||
|
||||
### Envs
|
||||
|
||||
Set in Settings > Environment (super admin), not `.env`. `envs` (private; encrypted `value`; `paradigm` general|workspace|computer|bot|user; one per key and scope, enforced in `saveEnv`). Cached per process. App settings (`EnvDict` in `src/dict/env-dict.ts`) via `grabEnv` (general value, else `process.env`); `ProcessOnlyEnvKeys` can't be saved. Precedence for runs: user > bot > computer > workspace > general (`resolveScopedEnvs`, `grabRunEnvs`; pass the run's `user_id`). Written to `~/.cobble/env` and `env.json` on the computer each run; bots see keys and descriptions, never values; every computer tool result goes through `redactSecrets`.
|
||||
Set in Settings > Environment (super admin), not `.env`. `envs` (private; encrypted `value`; `paradigm` general|workspace|computer|bot|user; one per key and scope, enforced in `saveEnv`). Cached per process. App settings (`EnvDict` in `src/dict/env-dict.ts`) via `grabEnv` (general value, else `process.env`); `ProcessOnlyEnvKeys` can't be saved. Precedence for runs: user > bot > computer > workspace > general (`resolveScopedEnvs`, `grabRunEnvs`; pass the run's `user_id`). Written to `~/.cobble/env` and `env.json` on the computer each task run (only task tools load them; `search_computers` doesn't); bots see keys and descriptions, never values (tasks in their prompt, replies through `composeEnvsNote` from `grabReachableEnvs`, so they never ask for or test a key that's set); every computer tool result goes through `redactSecrets`.
|
||||
|
||||
**Secrets never pass through the model.** A `user` env is one user's secret for one bot (`saveUserSecret`, reach-checked), set from the chat, listed on Settings > Secrets (own) and Environment (everyone's, remove only). In: `request_secret` -> `secret_requests` -> `[secret-request:ID]` card (masked input, `answer-request`), then a chat message naming only the key. Out: `capture_secret` (task computer tool: a file via `/v1/file/download` or a command's output, saved straight to a `user` env and added to the run's redaction list) and `show_secret` (an env by key) give `[secret:ID]`; the chip fetches the value only on click (`api/admin/secrets/reveal-secret`, `canRevealEnv`: a personal one only its own user, anything else only a super admin). Never put a value in a message, page prop, log or tool result. `SecretsNote` tells bots never to ask for secrets in chat.
|
||||
**Secrets never pass through the model.** A `user` env is one user's secret for one bot (`saveUserSecret`, reach-checked), set from the chat, listed on Settings > Secrets (own) and Environment (everyone's, remove only). In: `request_secret` (refused for a key that already reaches the run, unless the user asked to `replace` it) -> `secret_requests` -> `[secret-request:ID]` card (masked input, `answer-request`), then a chat message naming only the key. Out: `capture_secret` (task computer tool: a file via `/v1/file/download` or a command's output, saved straight to a `user` env and added to the run's redaction list) and `show_secret` (an env by key) give `[secret:ID]`; the chip fetches the value only on click (`api/admin/secrets/reveal-secret`, `canRevealEnv`: a personal one only its own user, anything else only a super admin). Never put a value in a message, page prop, log or tool result. `SecretsNote` tells bots never to ask for secrets in chat.
|
||||
|
||||
### PWA and notifications
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cobble",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import composeEnvsNote from "./compose-envs-note";
|
||||
|
||||
describe("composeEnvsNote", () => {
|
||||
test("lists the computer's envs and the tools-only ones apart", () => {
|
||||
const note = composeEnvsNote({
|
||||
on_computer: [{ key: "DB_PASSWORD", description: "Production database" }],
|
||||
tools_only: [{ key: "API_TOKEN", description: "" }],
|
||||
has_computer: true,
|
||||
});
|
||||
|
||||
expect(note).toContain("- DB_PASSWORD: Production database");
|
||||
expect(note).toContain("- API_TOKEN");
|
||||
expect(note).not.toContain("- API_TOKEN:");
|
||||
expect(note.indexOf("DB_PASSWORD")).toBeLessThan(note.indexOf("Only in tools"));
|
||||
expect(note).toContain("Don't ask the user for them");
|
||||
});
|
||||
|
||||
test("never mentions values", () => {
|
||||
const note = composeEnvsNote({ on_computer: [{ key: "K", description: "d" }], tools_only: [], has_computer: true });
|
||||
|
||||
expect(note).not.toContain("Only in tools");
|
||||
expect(note).toContain("you never see values");
|
||||
});
|
||||
|
||||
test("says so when nothing is set", () => {
|
||||
expect(composeEnvsNote({ on_computer: [], tools_only: [], has_computer: false })).toContain("No envs are set");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
type Env = { key: string; description: string };
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Set on the bot's current computer for its tasks (`grabReachableEnvs`).
|
||||
*/
|
||||
on_computer: Env[];
|
||||
/**
|
||||
* Only filled into tools' templates.
|
||||
*/
|
||||
tools_only: Env[];
|
||||
/**
|
||||
* Whether the bot has a computer, so tasks can use the computer's envs.
|
||||
*/
|
||||
has_computer: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Added to a chat reply so the bot knows which envs it already has (keys
|
||||
* and descriptions, never values). Without it a reply can't tell, asks the
|
||||
* user for keys that are set (`request_secret`), or "checks" them with
|
||||
* `show_secret` or a shell where they aren't loaded.
|
||||
*/
|
||||
export default function composeEnvsNote({ on_computer, tools_only, has_computer }: Params) {
|
||||
const list = (envs: Env[]) =>
|
||||
envs.map((env) => `- ${env.key}${env.description ? `: ${env.description}` : ""}`).join("\n");
|
||||
|
||||
if (!on_computer.length && !tools_only.length) {
|
||||
return `# Your envs\n\nNo envs are set for you here. If you need a secret, ask for it with request_secret.`;
|
||||
}
|
||||
|
||||
return [
|
||||
`# Your envs\n\nThese are already set for you here (keys and descriptions; you never see values). Don't ask the user for them, and don't call show_secret or run printenv to check them: this list is the check.`,
|
||||
on_computer.length
|
||||
? `On your computer, as $KEY in a task's shell and code (start_task; search_computers doesn't load them):\n${list(on_computer)}`
|
||||
: "",
|
||||
tools_only.length
|
||||
? `${has_computer ? "Only in tools" : "In tools"}, as {{env.KEY}}${has_computer ? " (not set on computers)" : ""}:\n${list(tools_only)}`
|
||||
: "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
}
|
||||
@@ -35,6 +35,8 @@ import openBotKnowledgebases from "@/src/harness/tools/knowledgebases/open-bot-k
|
||||
import grabKnowledgebaseContext from "@/src/harness/tools/knowledgebases/grab-knowledgebase-context";
|
||||
import botMessagesToHarness from "./bot-messages-to-harness";
|
||||
import composeComputersNote from "./compose-computers-note";
|
||||
import composeEnvsNote from "./compose-envs-note";
|
||||
import grabReachableEnvs from "@/src/functions/backend/db/envs/grab-reachable-envs";
|
||||
import grabSessionCarryOver from "./grab-session-carry-over";
|
||||
import continueInNewSessionTool from "@/src/harness/tools/bot/continue-in-new-session-tool";
|
||||
import searchPastSessionsTool from "@/src/harness/tools/bot/sessions/search-past-sessions-tool";
|
||||
@@ -145,6 +147,17 @@ export default async function runBotReply({
|
||||
const prompt = await grabBotPrompt({ bot_id: bot.id, user_id, workspace_id });
|
||||
const computers = await grabBotComputers({ bot_id: bot.id, workspace_id });
|
||||
|
||||
/**
|
||||
* The envs the reply has (keys only), so it doesn't ask for or test
|
||||
* ones that are set.
|
||||
*/
|
||||
const envs = await grabReachableEnvs({
|
||||
bot_id: bot.id!,
|
||||
user_id,
|
||||
workspace_id,
|
||||
computer_id: computers[0]?.id || null,
|
||||
});
|
||||
|
||||
/**
|
||||
* A summary that can't be written now (no model free) is left out
|
||||
* rather than holding up the reply.
|
||||
@@ -312,6 +325,7 @@ export default async function runBotReply({
|
||||
TaskReminderNote,
|
||||
ActionCheckNote,
|
||||
SecretsNote,
|
||||
composeEnvsNote({ ...envs, has_computer: computers.length > 0 }),
|
||||
GraphicsNote,
|
||||
workspace_id ? WorkspaceLearningNote : "",
|
||||
composeComputersNote({
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
computer_id: number | string;
|
||||
/**
|
||||
* Left out of the count, e.g. the job just queued.
|
||||
*/
|
||||
except_job_id?: number | string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* How many tasks are queued or working on a computer (any bot's, any
|
||||
* user's). Tasks run one at a time per computer, so a new one waits for
|
||||
* all of them.
|
||||
*/
|
||||
export default async function countComputerTaskJobs({ computer_id, except_job_id }: Params) {
|
||||
const res = await BunSQLite.sql<{ n: number }>({
|
||||
sql: `SELECT COUNT(*) AS n FROM bots_queue
|
||||
WHERE computer_id = ? AND kind = 'task' AND status IN ('pending', 'processing') AND id != ?`,
|
||||
values: [Number(computer_id), Number(except_job_id) || 0],
|
||||
});
|
||||
|
||||
return Number(res.payload?.[0]?.n) || 0;
|
||||
}
|
||||
@@ -3,21 +3,35 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
session_id: number | string;
|
||||
title: string;
|
||||
/**
|
||||
* Only a task with this title. Without it, any open task.
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* Only a task on this computer.
|
||||
*/
|
||||
computer_id?: number | string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The bot's task in this session with this title that's still queued or
|
||||
* working, if any, so the same work isn't started twice.
|
||||
* The bot's latest task in this session that's still queued or working
|
||||
* (with this title, on this computer, when given), if any, so the same
|
||||
* work isn't started twice.
|
||||
*/
|
||||
export default async function grabOpenBotTask({ bot_id, session_id, title }: Params) {
|
||||
const res = await BunSQLite.sql<{ id: number; status: string }>({
|
||||
sql: `SELECT id, status FROM bots_queue
|
||||
export default async function grabOpenBotTask({ bot_id, session_id, title, computer_id }: Params) {
|
||||
const res = await BunSQLite.sql<{ id: number; status: string; title: string | null }>({
|
||||
sql: `SELECT id, status, title FROM bots_queue
|
||||
WHERE bot_id = ? AND session_id = ? AND kind = 'task'
|
||||
AND status IN ('pending', 'processing')
|
||||
AND lower(trim(title)) = lower(trim(?))
|
||||
${title != undefined ? `AND lower(trim(title)) = lower(trim(?))` : ""}
|
||||
${computer_id ? `AND computer_id = ?` : ""}
|
||||
ORDER BY id DESC LIMIT 1`,
|
||||
values: [bot_id, session_id, title],
|
||||
values: [
|
||||
bot_id,
|
||||
session_id,
|
||||
...(title != undefined ? [title] : []),
|
||||
...(computer_id ? [Number(computer_id)] : []),
|
||||
],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import grabRunEnvs from "../extensions/grab-run-envs";
|
||||
import type { DecryptedEnv } from "./grab-all-envs";
|
||||
import grabComputerEnvs from "./grab-computer-envs";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
* The computer the run is on (the bot's current one), whose envs count.
|
||||
*/
|
||||
computer_id?: number | string | null;
|
||||
};
|
||||
|
||||
export type ReachableEnvs = {
|
||||
/**
|
||||
* Set on the computer for its tasks ($KEY in their shells and code).
|
||||
*/
|
||||
on_computer: DecryptedEnv[];
|
||||
/**
|
||||
* Only filled into tools' templates ({{env.KEY}}): not exposed to
|
||||
* computers, or there's no computer.
|
||||
*/
|
||||
tools_only: DecryptedEnv[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Every env that reaches one run, one per key: the computer's
|
||||
* (`grabComputerEnvs`) first, then the ones only tools read
|
||||
* (`grabRunEnvs`). What `show_secret` can show, what `request_secret`
|
||||
* mustn't ask for again, and what a reply is told it has.
|
||||
*/
|
||||
export default async function grabReachableEnvs({
|
||||
bot_id,
|
||||
user_id,
|
||||
workspace_id,
|
||||
computer_id,
|
||||
}: Params): Promise<ReachableEnvs> {
|
||||
const [on_computer, run] = await Promise.all([
|
||||
computer_id ? grabComputerEnvs({ computer_id, bot_id, user_id, workspace_id }) : [],
|
||||
grabRunEnvs({ bot_id, user_id, workspace_id }),
|
||||
]);
|
||||
const keys = new Set(on_computer.map((env) => env.key));
|
||||
|
||||
return { on_computer, tools_only: run.filter((env) => !keys.has(env.key)) };
|
||||
}
|
||||
@@ -219,7 +219,7 @@ export default function delegateTaskTool({
|
||||
|
||||
if (open) {
|
||||
throw new Error(
|
||||
`${worker.name} already has task #${open.id} "${title}" ${open.status == "pending" ? "queued" : "under way"}. Don't hand it over again; it's reported back here when they finish.`,
|
||||
`${worker.name} already has run #${open.id} "${title}" ${open.status == "pending" ? "queued" : "under way"}. Don't hand it over again; it's reported back here when they finish.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -244,9 +244,9 @@ export default function delegateTaskTool({
|
||||
}),
|
||||
});
|
||||
|
||||
return `Task #${job_id} is with ${worker.name}${
|
||||
return `Run #${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.`;
|
||||
} (a run, not a task on the to-do list: list_tasks doesn't show it).${joined} Their result will be in that conversation and reported back here.`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function grabBotSecretTools({
|
||||
}: Params): HarnessTool[] {
|
||||
return [
|
||||
...(session_id && onRequest
|
||||
? [requestSecretTool({ user_id, bot_id, session_id, workspace_id, in_task, onRequest })]
|
||||
? [requestSecretTool({ user_id, bot_id, session_id, workspace_id, computer_id, in_task, onRequest })]
|
||||
: []),
|
||||
...(onShow ? [showSecretTool({ user_id, bot_id, workspace_id, computer_id, onShow })] : []),
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import grabReachableEnvs from "@/src/functions/backend/db/envs/grab-reachable-envs";
|
||||
import createSecretRequest from "@/src/functions/backend/db/secret-requests/create-secret-request";
|
||||
import secretRequestRef from "@/src/utils/secret-request-ref";
|
||||
import type { HarnessTool } from "../../../types";
|
||||
@@ -11,6 +12,10 @@ type Params = {
|
||||
*/
|
||||
session_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
* The computer the run is on, whose envs count as already set.
|
||||
*/
|
||||
computer_id?: number | string | null;
|
||||
/**
|
||||
* Whether the run is a task, which stops and reports rather than waits.
|
||||
*/
|
||||
@@ -25,6 +30,7 @@ type Args = {
|
||||
key: string;
|
||||
description: string;
|
||||
reason: string;
|
||||
replace?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -38,33 +44,51 @@ export default function requestSecretTool({
|
||||
bot_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
computer_id,
|
||||
in_task,
|
||||
onRequest,
|
||||
}: Params): HarnessTool<Args> {
|
||||
return {
|
||||
name: "request_secret",
|
||||
description: `Ask the user for a secret you need (a password, token or API key) through a masked card in the chat, never by asking them to type it into a message: what they type there goes to you and the model provider, the card's value never does. It's saved as their own secret for you, under key, and set as $KEY in your shell (and {{env.KEY}} for tools) on this user's later runs; you never see the value. Check your envs first: if the key is already set, don't ask. Their answer comes as a message saying it's saved${
|
||||
description: `Ask the user for a secret you need (a password, token or API key) through a masked card in the chat, never by asking them to type it into a message: what they type there goes to you and the model provider, the card's value never does. It's saved as their own secret for you, under key, and set as $KEY in your shell (and {{env.KEY}} for tools) on this user's later runs; you never see the value. A key listed in "Your envs" is already set (the workspace, the computer or the user may have saved it): don't ask for it, use it. Their answer comes as a message saying it's saved${
|
||||
in_task ? `. In a task, ask, then stop and report that you're waiting for it.` : `.`
|
||||
}`,
|
||||
schema: z.object({
|
||||
key: z.string().trim().min(1).max(128).describe("The env key to save it under, e.g. GITHUB_TOKEN"),
|
||||
description: z.string().trim().min(1).max(500).describe("What it is, shown on the card, e.g. \"GitHub personal access token with repo scope\""),
|
||||
reason: z.string().trim().min(10, "Say why you need it").max(2000).describe("Why you need it and what you'll do with it, shown on the card as your words"),
|
||||
replace: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Only when the user asked to replace a key you already have: their answer is saved as their own and wins over the existing one."),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const key = args.key.trim();
|
||||
|
||||
if (!args.replace) {
|
||||
const { on_computer, tools_only } = await grabReachableEnvs({ bot_id, user_id, workspace_id, computer_id });
|
||||
const existing = [...on_computer, ...tools_only].find((env) => env.key == key);
|
||||
|
||||
if (existing) {
|
||||
throw new Error(
|
||||
`${key} is already set for you here (${existing.paradigm == "user" ? "the user's own secret" : `a ${existing.paradigm} env`}), so no card was sent. Use it as $${key} in a task on your computer or {{env.${key}}} in tools. Ask again with replace only if the user wants a new value.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const id = await createSecretRequest({
|
||||
user_id,
|
||||
bot_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
key: args.key,
|
||||
key,
|
||||
description: args.description,
|
||||
reason: args.reason,
|
||||
});
|
||||
|
||||
onRequest({ ref: secretRequestRef({ id }) });
|
||||
|
||||
return `Asked the user for ${args.key.trim()}: the card is on your message. Their answer arrives as a message once it's saved; then it's $${args.key.trim()} on your computers. Never ask them to paste it in the chat.`;
|
||||
return `Asked the user for ${key}: the card is on your message. Their answer arrives as a message once it's saved; then it's $${key} on your computers. Never ask them to paste it in the chat.`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import grabComputerEnvs from "@/src/functions/backend/db/envs/grab-computer-envs";
|
||||
import grabRunEnvs from "@/src/functions/backend/db/extensions/grab-run-envs";
|
||||
import grabReachableEnvs from "@/src/functions/backend/db/envs/grab-reachable-envs";
|
||||
import secretRef from "@/src/utils/secret-ref";
|
||||
import type { HarnessTool } from "../../../types";
|
||||
|
||||
@@ -30,18 +29,14 @@ type Args = {
|
||||
export default function showSecretTool({ user_id, bot_id, workspace_id, computer_id, onShow }: Params): HarnessTool<Args> {
|
||||
return {
|
||||
name: "show_secret",
|
||||
description: `Show the user one of your envs (by key, e.g. a password or token they saved or you captured) without reading it: you get a [secret:ID] ref to put in your message, which the user sees masked with Reveal and Copy. Never print a secret with a command to show it. Only its owner (for a personal secret) or a super admin can reveal it.`,
|
||||
description: `Show the user one of your envs (by key, e.g. a password or token they saved or you captured) without reading it: you get a [secret:ID] ref to put in your message, which the user sees masked with Reveal and Copy. Only when the user wants to see it: to check which envs you have, read "Your envs" in your instructions instead. Never print a secret with a command to show it. Only its owner (for a personal secret) or a super admin can reveal it.`,
|
||||
schema: z.object({
|
||||
key: z.string().trim().min(1).max(128),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const key = args.key.trim();
|
||||
const on_computer = computer_id
|
||||
? await grabComputerEnvs({ computer_id, bot_id, user_id, workspace_id })
|
||||
: [];
|
||||
const env =
|
||||
on_computer.find((e) => e.key == key) ||
|
||||
(await grabRunEnvs({ bot_id, user_id, workspace_id })).find((e) => e.key == key);
|
||||
const { on_computer, tools_only } = await grabReachableEnvs({ bot_id, user_id, workspace_id, computer_id });
|
||||
const env = [...on_computer, ...tools_only].find((e) => e.key == key);
|
||||
|
||||
if (!env) return `No env called ${key} reaches you here. Ask the user with request_secret if you need it.`;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
|
||||
import enqueueBotJob from "@/src/functions/backend/db/bots-queue/enqueue-bot-job";
|
||||
import countComputerTaskJobs from "@/src/functions/backend/db/bots-queue/count-computer-task-jobs";
|
||||
import grabOpenBotTask from "@/src/functions/backend/db/bots-queue/grab-open-bot-task";
|
||||
import type { HarnessTool } from "../../types";
|
||||
|
||||
@@ -25,11 +26,17 @@ type Args = {
|
||||
title: string;
|
||||
instructions: string;
|
||||
computer_id?: number;
|
||||
separate?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets a bot queue long-running work for itself. The task runs in the
|
||||
* background with the computer's tools and posts its result to the chat.
|
||||
* It's a run in the queue (`bots_queue`), not a task on the to-do list
|
||||
* (`tasks`), so it's called a run: told "Task #52", bots and users look
|
||||
* for it in `list_tasks` and don't find it. While a run is open in the
|
||||
* session on that computer a new one is refused unless it's separate
|
||||
* work, since it would only wait behind it (one task per computer).
|
||||
*/
|
||||
export default function startTaskTool({
|
||||
user_id,
|
||||
@@ -44,7 +51,7 @@ export default function startTaskTool({
|
||||
|
||||
return {
|
||||
name: "start_task",
|
||||
description: `Queue work that takes a while or needs a computer. It runs in the background and its result is posted in this chat. Write the instructions as a full brief for yourself: what to do, where, and how you'll know it's done. ${
|
||||
description: `Queue work that takes a while or needs a computer. It runs in the background and its result is posted in this chat. It's a run, not a task on the to-do list: list_tasks never shows it, so don't send the user there to find it. Write the instructions as a full brief for yourself: what to do, where, and how you'll know it's done. Tasks run one at a time per computer, so while one of yours is open here, a new one only waits behind it: don't start the same work again with another title. ${
|
||||
computers.length
|
||||
? `Computers you can use: ${listed}. Defaults to ${computers[0]!.id}, the one you're on.`
|
||||
: `You have no computer, so the task can't run commands or browse.`
|
||||
@@ -53,18 +60,30 @@ export default function startTaskTool({
|
||||
title: z.string().min(1).max(80).describe("A short label, e.g. \"Check the backup logs\""),
|
||||
instructions: z.string().min(1).max(8000),
|
||||
computer_id: z.number().int().optional(),
|
||||
separate: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Only for different work while another run of yours is open here on that computer, never to retry or redo it."),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const open = await grabOpenBotTask({ bot_id, session_id, title: args.title });
|
||||
const same = await grabOpenBotTask({ bot_id, session_id, title: args.title });
|
||||
|
||||
if (open) {
|
||||
if (same) {
|
||||
throw new Error(
|
||||
`Task #${open.id} "${args.title}" is already ${open.status == "pending" ? "queued" : "under way"}. Don't start it again; its result will be posted here.`,
|
||||
`Run #${same.id} "${args.title}" is already ${same.status == "pending" ? "queued" : "under way"}. Don't start it again; its result will be posted here.`,
|
||||
);
|
||||
}
|
||||
|
||||
const computer_id = args.computer_id ?? computers[0]?.id ?? undefined;
|
||||
|
||||
const open = args.separate ? null : await grabOpenBotTask({ bot_id, session_id, computer_id });
|
||||
|
||||
if (open) {
|
||||
throw new Error(
|
||||
`Run #${open.id} "${open.title || "Task"}" is still ${open.status == "pending" ? "queued" : "under way"} here on that computer, and a new one would only wait behind it. If this is the same work, don't start it again: its result will be posted here (the user can Stop it to start over). Only for different work, call start_task again with separate.`,
|
||||
);
|
||||
}
|
||||
|
||||
const job_id = await enqueueBotJob({
|
||||
user_id,
|
||||
bot_id,
|
||||
@@ -75,7 +94,13 @@ export default function startTaskTool({
|
||||
computer_id: computer_id || undefined,
|
||||
});
|
||||
|
||||
return `Task #${job_id} queued; you'll work on it yourself in the background. Tell the user in one sentence, in the first person, what you're doing (e.g. "I'm checking your inbox now. I'll report back when I'm done."); the result will be posted here when it's done.`;
|
||||
const ahead = computer_id ? await countComputerTaskJobs({ computer_id, except_job_id: job_id }) : 0;
|
||||
|
||||
return `Run #${job_id} queued; you'll work on it yourself in the background. It's not on the task list (list_tasks), so never tell the user to look for it there. ${
|
||||
ahead
|
||||
? `The computer has ${ahead} other ${ahead == 1 ? "run" : "runs"} queued or working (they go one at a time), so it waits for ${ahead == 1 ? "it" : "them"} first: tell the user it's queued behind other work, not that it's running. `
|
||||
: ""
|
||||
}Tell the user in one sentence, in the first person, what you're doing (e.g. "I'm checking your inbox now. I'll report back when I'm done."); the result will be posted here when it's done.`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function searchComputersTool({
|
||||
}: 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. It starts in your folder on each computer (${dirs.cwd}, also ~). 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.`,
|
||||
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. It starts in your folder on each computer (${dirs.cwd}, also ~). Your envs aren't loaded here (only in a task's shell), so it can't tell whether one is set: "Your envs" in your instructions says that. 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),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user