Add the harness, envs, per-bot models and chat attachments

- Harness: providers, model catalog synced hourly, automatic free picks,
  cooldowns, tool loop with context trimming, MCP computer tools
- Bots queue runs replies and tasks through the harness; jobs wait
  (attempt refunded) while every model is resting
- Envs: Settings > Environment, scoped general/workspace/computer,
  written to computers and redacted from tool output
- Models: Settings > Models, per-harness provider select defaulting to
  OpenRouter, and a models modal on each bot's chat (the coordinator's
  edits the defaults)
- Chat: thinking row while a reply runs, file attachments (upload,
  paste, drop) that replies read as images, audio and document text
- Fix media uploads failing after a user's first file, and private
  files being served from the public media path

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 03:38:38 +01:00
co-authored by Claude Opus 5.5
parent d49f524fe8
commit 6a2665dae9
223 changed files with 12342 additions and 162 deletions
+96
View File
@@ -0,0 +1,96 @@
import HarnessError from "../harness-error";
import grabHarnessAdapter from "../providers/grab-harness-adapter";
import resolveHarnessCandidates from "../resolve/resolve-harness-candidates";
import withCandidates from "../resolve/with-candidates";
import type {
HarnessCandidate,
HarnessChatResult,
HarnessMessage,
HarnessToolDefinition,
} from "../types";
import prepareMessagesForModel from "./prepare-messages-for-model";
import stripThinkTags from "./strip-think-tags";
export type GenerateTextParams = {
messages: HarnessMessage[];
tools?: HarnessToolDefinition[];
/**
* Resolved for the text harness when not given.
*/
candidates?: HarnessCandidate[];
/**
* Tried first: the model the run already uses.
*/
prefer_key?: string;
max_tokens?: number;
temperature?: number;
signal?: AbortSignal;
/**
* Media readings shared across calls of one run.
*/
media_cache?: Map<string, string>;
/**
* The bot the turn is for: its own models are used where it has them.
*/
bot_id?: number | string;
};
export type GenerateTextResult = HarnessChatResult & {
candidate: HarnessCandidate;
};
/**
* One model turn on the text harness, falling back across its models.
* Media the model can't take is read by the helper harnesses first.
* Reasoning tags are stripped from the answer.
*/
export default async function generateText({
messages,
tools,
candidates,
prefer_key,
max_tokens,
temperature,
signal = new AbortController().signal,
media_cache = new Map(),
bot_id,
}: GenerateTextParams): Promise<GenerateTextResult> {
const { result, candidate } = await withCandidates({
harness: "text",
candidates:
candidates || (await resolveHarnessCandidates({ harness: "text", bot_id })),
prefer_key,
run: async (candidate) => {
const answer = await grabHarnessAdapter({
kind: candidate.provider.kind,
}).chat({
candidate,
messages: await prepareMessagesForModel({
messages,
model: candidate.model,
cache: media_cache,
signal,
bot_id,
}),
tools,
max_tokens,
temperature,
signal,
});
const text = answer.text ? stripThinkTags({ text: answer.text }) : "";
if (!text && !answer.tool_calls.length) {
throw new HarnessError({
message: `The model returned an empty answer.`,
retryable: true,
candidate_key: candidate.key,
});
}
return { ...answer, text: text || null };
},
});
return { ...result, candidate };
}