server/agents-turn.ts
Rama main · réplica de lectura del repositorio original.
// The agents server on one turn of a conversation. Providers cache a prompt from its start, tool definitions first, so a
// tool that came or went between two turns of one conversation would re-bill the whole conversation at the uncached
// price: what the server lists comes from the conversation alone, and a turn that may not use a listed tool is told why
// when it calls it.
import { AGENTS_PROXY_ENV } from "./agents-bridge-switch.ts";
import { roleToolEnv } from "./chief-level.ts";
/** The facts of a conversation that decide its agents tools. A settings change may change them; a turn never does. */
export interface AgentsConversation {
threadId: string;
bot: NonNullable<Parameters<typeof roleToolEnv>[0]> & { id: string; computer?: string; memoryEnabled?: boolean; xResearch?: boolean };
/** A room's thread: rooms refuse widgets, pages and a computer pick. */
room: boolean;
/** The bot works on its team's cloud computer, so it has no computer of its own to pick. */
teamComputer: boolean;
/** Only bots or automation drive it (`botDrivenConversation`): no turn in it is a person's own message to pick a computer for. */
botDriven?: boolean;
/** `x`: a research credential is held (a treg key or a plan's); `research`: research works for it too. */
host: { skillAuthoring: boolean; sharedComputers: boolean; cloudHome: boolean; team: boolean; voiceNotes: boolean; x: boolean; research: boolean; imageGeneration: boolean; meetings?: boolean };
}
/** What one turn may do. Its bearer carries this to the harness, which checks it on every call. */
export interface AgentsTurn {
generation: string;
depth: number;
/** In a room only the first answer to a person's message may list or write skills. */
skillAuthoring: boolean;
roomHandoffId?: string;
deliversSavedFiles?: boolean;
}
const flag = (on: boolean) => (on ? "1" : "0");
export function agentsServerEnv(conversation: AgentsConversation, turn: AgentsTurn, bearer: (turn: AgentsTurn) => string, harnessUrl: string): Record<string, string> {
const { bot, host } = conversation;
return {
...AGENTS_PROXY_ENV,
OMB_HARNESS_URL: harnessUrl,
OMB_BOT_ID: bot.id,
OMB_THREAD_ID: conversation.threadId,
OMB_COMMS_TOKEN: bearer(turn),
OMB_TURN_DEPTH: String(turn.depth),
OMB_SKILL_AUTHORING_ENABLED: flag(host.skillAuthoring),
OMB_MEMORY_ENABLED: flag(bot.memoryEnabled !== false),
OMB_SHARED_COMPUTERS_ENABLED: flag(host.sharedComputers),
OMB_CLOUD_HOME: flag(host.cloudHome), OMB_TEAM_PEOPLE: flag(host.team), OMB_ROOM: flag(conversation.room),
OMB_VOICE_NOTES: flag(host.voiceNotes),
OMB_X_RESEARCH: flag(host.x && bot.xResearch !== false),
OMB_RESEARCH: flag(host.x && host.research && bot.xResearch !== false),
...roleToolEnv(bot),
OMB_IMAGE_GENERATION: flag(host.imageGeneration),
OMB_MEETINGS: flag(host.meetings === true && !conversation.room),
OMB_COMPUTER_SELECTION: flag(!conversation.room && bot.computer !== "off" && !conversation.teamComputer && !conversation.botDriven),
};
}
/** A conversation a bot opened, the Bot API's, or a routine's run: a fact of the conversation, so its tool list stays the same turn to turn. */
export function botDrivenConversation(task: { openedBy?: unknown; botApi?: boolean; routineRunId?: string } | undefined): boolean {
return Boolean(task && (task.openedBy || task.botApi || task.routineRunId));
}
/** What started a direct turn, in the words a refusal uses. */
export type TurnKind = "person" | "routine" | "webhook" | "api" | "handoff" | "continuation";
export function directTurnKind(turn: {
automationSource?: string; commsDepth?: number; coordination?: unknown; peerAsk?: unknown;
cardContinuation?: boolean; computerSelectionContinuation?: boolean; unattended?: boolean; compactOnly?: boolean;
}): TurnKind {
if (turn.automationSource === "webhook") return "webhook";
if (turn.automationSource === "api") return "api";
if (turn.automationSource !== undefined) return "routine";
if (turn.commsDepth || turn.coordination || turn.peerAsk) return "handoff";
if (turn.cardContinuation || turn.computerSelectionContinuation || turn.unattended || turn.compactOnly) return "continuation";
return "person";
}
const THIS_TURN: Record<Exclude<TurnKind, "person">, string> = {
routine: "This turn is a routine run",
webhook: "This turn was started by a webhook",
api: "This turn came in through the Bot API",
handoff: "This turn is a teammate's hand-off",
continuation: "This turn continues earlier work",
};
export const COMPUTER_OFF_REFUSAL = "This bot's Works on setting is Off, so it has no computer to pick. Work without one, or ask the person to choose a place in this bot's settings.";
/** select_computer on a turn that may not pick, `kind` being what started it. */
export function computerPickRefusal(kind: TurnKind | undefined): string {
if (kind === undefined || kind === "person") return "Picking a computer works only once per request, on a person's own message. Carry on with the computer already in use.";
return `Picking a computer works only on a person's own message. ${THIS_TURN[kind]}, so carry on with the computer already in use.`;
}
/** skills_list or skill_manage on a room turn that is not the first answer to a person's message. */
export function roomSkillsRefusal(handoff: boolean): string {
return `In a room, only the first bot answering a person's message can list or write skills. ${handoff ? THIS_TURN.handoff : "This turn is a follow-up"}, so carry on without changing skills.`;
}