feat: add omx orchestration backend selection

This commit is contained in:
kris
2026-04-03 03:17:12 +08:00
parent 60f5e2d7d6
commit ec45bed59f
18 changed files with 1993 additions and 20 deletions

View File

@@ -6,6 +6,13 @@ import { publishBossEvent } from "@/lib/boss-events";
import type { VerificationDeliveryMode } from "@/lib/boss-mail";
import { getFixedVerificationCode, getVerificationDeliveryMode } from "@/lib/boss-mail";
import { getPublishedOtaAsset } from "@/lib/boss-ota";
import { BOSS_NATIVE_ORCHESTRATOR } from "@/lib/execution/backends/boss-native-orchestrator";
import {
OMX_TEAM_BACKEND,
getOmxTeamBackendSelectionState,
type OmxTeamBackendSelectionState,
} from "@/lib/execution/backends/omx-team-backend";
import { selectOrchestrationBackend } from "@/lib/execution/orchestration-backend-selector";
export type DeviceStatus = "online" | "abnormal" | "offline";
export type DeviceSource = "production" | "demo";
@@ -144,6 +151,8 @@ export type DispatchPlanStatus =
| "dispatched";
export type DispatchExecutionStatus = "queued" | "running" | "completed" | "failed";
export type ReasoningEffort = "low" | "medium" | "high";
export type OrchestrationBackendId = import("@/lib/execution/orchestration-backend").OrchestrationBackendId;
export type OrchestrationBackendOverride = "omx-team";
export interface UserSettings {
liveUpdates: boolean;
@@ -305,6 +314,7 @@ export interface Project {
createdByAgent: boolean;
collaborationMode: "development" | "approval_required";
approvalState: "not_required" | "pending_agent" | "pending_user" | "approved" | "rejected";
orchestrationBackendOverride?: OrchestrationBackendOverride;
agentControls?: ProjectAgentControls;
unreadCount: number;
riskLevel: RiskLevel;
@@ -334,6 +344,10 @@ export interface DispatchPlan {
status: DispatchPlanStatus;
targets: DispatchPlanTarget[];
summary: string;
requestedOrchestrationBackendId?: OrchestrationBackendOverride;
orchestrationBackendId?: OrchestrationBackendId;
orchestrationBackendLabel?: string;
orchestrationFallbackReason?: string;
createdAt: string;
confirmedAt?: string;
confirmedBy?: string;
@@ -347,6 +361,8 @@ export interface DispatchExecution {
targetProjectId: string;
targetThreadId: string;
deviceId: string;
orchestrationBackendId?: OrchestrationBackendId;
orchestrationBackendLabel?: string;
status: DispatchExecutionStatus;
createdAt: string;
completedAt?: string;
@@ -382,6 +398,23 @@ export interface ProjectAgentControls {
updatedAt: string;
}
export interface ProjectOrchestrationBackendChoice {
backendId: OrchestrationBackendId;
label: string;
selectable: boolean;
current: boolean;
}
export interface ProjectOrchestrationBackendState {
projectId: string;
requestedBackendId: OrchestrationBackendId;
currentBackendId: OrchestrationBackendId;
currentBackendLabel: string;
requestedBackendLabel: string;
availableChoices: ProjectOrchestrationBackendChoice[];
omxAvailability: OmxTeamBackendSelectionState["availability"];
}
export interface UserProjectAgentControls {
account: string;
projectId: string;
@@ -607,6 +640,8 @@ export interface MasterAgentTask {
targetThreadDisplayName?: string;
targetCodexThreadRef?: string;
targetCodexFolderRef?: string;
orchestrationBackendId?: OrchestrationBackendId;
orchestrationBackendLabel?: string;
deviceImportDraftId?: string;
status: MasterAgentTaskStatus;
requestedAt: string;
@@ -1726,6 +1761,12 @@ function parseBackendOverride(value: unknown) {
return { kind: "set" as const, value: "claw-runtime" as const };
}
function normalizeOrchestrationBackendOverride(
value: unknown,
): OrchestrationBackendOverride | undefined {
return value === "omx-team" ? "omx-team" : undefined;
}
function normalizeStringSet(values: string[]) {
return dedupeStrings(values.map((value) => value.trim()).filter(Boolean)).sort((a, b) => a.localeCompare(b));
}
@@ -1913,6 +1954,20 @@ function normalizeDispatchPlan(raw: Partial<DispatchPlan>, fallback?: DispatchPl
status: raw.status ?? fallback?.status ?? "pending_user_confirmation",
targets,
summary: raw.summary ?? fallback?.summary ?? "",
requestedOrchestrationBackendId: normalizeOrchestrationBackendOverride(
raw.requestedOrchestrationBackendId ?? fallback?.requestedOrchestrationBackendId,
),
orchestrationBackendId:
raw.orchestrationBackendId === "omx-team" || raw.orchestrationBackendId === "boss-native-orchestrator"
? raw.orchestrationBackendId
: fallback?.orchestrationBackendId ?? "boss-native-orchestrator",
orchestrationBackendLabel:
trimToDefined(raw.orchestrationBackendLabel) ??
trimToDefined(fallback?.orchestrationBackendLabel) ??
(raw.orchestrationBackendId === "omx-team" ? "OMX Team Runtime" : "Boss Native Orchestrator"),
orchestrationFallbackReason:
trimToDefined(raw.orchestrationFallbackReason) ??
trimToDefined(fallback?.orchestrationFallbackReason),
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
confirmedAt: raw.confirmedAt ?? fallback?.confirmedAt,
confirmedBy: raw.confirmedBy ?? fallback?.confirmedBy,
@@ -1936,6 +1991,14 @@ function normalizeDispatchExecution(
targetProjectId: raw.targetProjectId ?? fallback?.targetProjectId ?? "",
targetThreadId: raw.targetThreadId ?? fallback?.targetThreadId ?? "",
deviceId: raw.deviceId ?? fallback?.deviceId ?? "",
orchestrationBackendId:
raw.orchestrationBackendId === "omx-team" || raw.orchestrationBackendId === "boss-native-orchestrator"
? raw.orchestrationBackendId
: fallback?.orchestrationBackendId ?? "boss-native-orchestrator",
orchestrationBackendLabel:
trimToDefined(raw.orchestrationBackendLabel) ??
trimToDefined(fallback?.orchestrationBackendLabel) ??
(raw.orchestrationBackendId === "omx-team" ? "OMX Team Runtime" : "Boss Native Orchestrator"),
status: raw.status ?? fallback?.status ?? "queued",
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
completedAt: raw.completedAt ?? fallback?.completedAt,
@@ -2714,6 +2777,7 @@ function normalizeProject(raw: Partial<Project>, fallback?: Project): Project {
createdByAgent: raw.createdByAgent ?? false,
collaborationMode: raw.collaborationMode ?? "development",
approvalState: raw.approvalState ?? "not_required",
orchestrationBackendOverride: normalizeOrchestrationBackendOverride(raw.orchestrationBackendOverride),
agentControls: normalizeProjectAgentControls(raw.agentControls),
};
project.groupMembers = ensureArray(raw.groupMembers, []).map((member) =>
@@ -2846,6 +2910,11 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
targetProjectId: task.targetProjectId,
targetThreadId: task.targetThreadId,
targetThreadDisplayName: task.targetThreadDisplayName,
orchestrationBackendId:
task.orchestrationBackendId === "omx-team" || task.orchestrationBackendId === "boss-native-orchestrator"
? task.orchestrationBackendId
: undefined,
orchestrationBackendLabel: task.orchestrationBackendLabel,
deviceImportDraftId: task.deviceImportDraftId,
status: task.status ?? "queued",
requestedAt: task.requestedAt ?? nowIso(),
@@ -3584,6 +3653,33 @@ export async function getProject(projectId: string) {
return state.projects.find((project) => project.id === projectId) ?? null;
}
export async function updateProjectOrchestrationBackendOverride(input: {
projectId: string;
requestedBy: string;
orchestrationBackendOverride?: OrchestrationBackendOverride;
}) {
return mutateState((state) => {
const project = state.projects.find((item) => item.id === input.projectId);
if (!project) {
throw new Error("PROJECT_NOT_FOUND");
}
if (!project.isGroup) {
throw new Error("PROJECT_NOT_GROUP_CHAT");
}
requireDispatchActorSession(state, input.requestedBy);
const nextOverride = input.orchestrationBackendOverride;
if (project.orchestrationBackendOverride === nextOverride) {
return project;
}
project.orchestrationBackendOverride = nextOverride;
project.updatedAt = nowIso();
project.threadMeta.updatedAt = project.updatedAt;
return project;
});
}
export async function hasPersistedProject(projectId: string) {
const rawState = await loadPersistedStateRaw();
return Array.isArray(rawState.projects) && rawState.projects.some((project) => project?.id === projectId);
@@ -3731,6 +3827,87 @@ export async function updateProjectAgentControls(
});
}
function projectOrchestrationRequestedBackendId(project: Project): OrchestrationBackendId {
return project.orchestrationBackendOverride ?? "boss-native-orchestrator";
}
async function buildProjectOrchestrationBackendState(
project: Project,
): Promise<ProjectOrchestrationBackendState> {
const requestedBackendId = projectOrchestrationRequestedBackendId(project);
const omxSelection = await getOmxTeamBackendSelectionState();
const currentBackend = await selectOrchestrationBackend({
requestedBackendId,
omx: omxSelection,
});
const nativeBackend = await BOSS_NATIVE_ORCHESTRATOR.describe();
const omxBackend = await OMX_TEAM_BACKEND.describe();
const availableChoices: ProjectOrchestrationBackendChoice[] = [
{
backendId: nativeBackend.backendId as OrchestrationBackendId,
label: nativeBackend.label,
selectable: true,
current: currentBackend.backendId === nativeBackend.backendId,
},
{
backendId: omxBackend.backendId as OrchestrationBackendId,
label: omxBackend.label,
selectable: omxSelection.selectable,
current: currentBackend.backendId === omxBackend.backendId,
},
];
return {
projectId: project.id,
requestedBackendId,
currentBackendId: currentBackend.backendId as OrchestrationBackendId,
currentBackendLabel:
availableChoices.find((choice) => choice.backendId === currentBackend.backendId)?.label ??
nativeBackend.label,
requestedBackendLabel:
availableChoices.find((choice) => choice.backendId === requestedBackendId)?.label ??
nativeBackend.label,
availableChoices,
omxAvailability: omxSelection.availability,
};
}
export async function getProjectOrchestrationBackendState(
projectId: string,
): Promise<ProjectOrchestrationBackendState | null> {
const state = await readState();
const project = state.projects.find((item) => item.id === projectId);
if (!project) {
return null;
}
return buildProjectOrchestrationBackendState(project);
}
export async function updateProjectOrchestrationBackend(
projectId: string,
requestedBackendId: OrchestrationBackendId,
) {
return mutateStateIfChanged(async (state) => {
const project = state.projects.find((item) => item.id === projectId);
if (!project) {
throw new Error("PROJECT_NOT_FOUND");
}
const nextOverride =
requestedBackendId === "boss-native-orchestrator" ? undefined : "omx-team";
const currentRequestedBackendId = projectOrchestrationRequestedBackendId(project);
if (currentRequestedBackendId === requestedBackendId && project.orchestrationBackendOverride === nextOverride) {
return { result: project.orchestrationBackendOverride ?? null, changed: false };
}
project.orchestrationBackendOverride = nextOverride;
const updatedAt = nowIso();
project.updatedAt = updatedAt;
project.threadMeta.updatedAt = updatedAt;
return { result: project.orchestrationBackendOverride ?? null, changed: true };
});
}
export async function getDevice(deviceId: string) {
const state = await readState();
return state.devices.find((device) => device.id === deviceId) ?? null;
@@ -4914,6 +5091,8 @@ export async function queueMasterAgentTask(payload: {
targetThreadDisplayName?: string;
targetCodexThreadRef?: string;
targetCodexFolderRef?: string;
orchestrationBackendId?: OrchestrationBackendId;
orchestrationBackendLabel?: string;
}) {
const task = await mutateState((state) => {
const task: MasterAgentTask = {
@@ -4941,6 +5120,8 @@ export async function queueMasterAgentTask(payload: {
targetThreadDisplayName: payload.targetThreadDisplayName,
targetCodexThreadRef: payload.targetCodexThreadRef,
targetCodexFolderRef: payload.targetCodexFolderRef,
orchestrationBackendId: payload.orchestrationBackendId,
orchestrationBackendLabel: payload.orchestrationBackendLabel,
status: "queued",
requestedAt: nowIso(),
};
@@ -4961,6 +5142,10 @@ export async function createDispatchPlan(input: {
requestedBy: string;
summary?: string;
targets: DispatchPlanTarget[];
requestedOrchestrationBackendId?: OrchestrationBackendOverride;
orchestrationBackendId?: OrchestrationBackendId;
orchestrationBackendLabel?: string;
orchestrationFallbackReason?: string;
}) {
return mutateState((state) => {
return upsertDispatchPlanInState(state, input);
@@ -4975,12 +5160,22 @@ function upsertDispatchPlanInState(
requestedBy: string;
summary?: string;
targets: DispatchPlanTarget[];
requestedOrchestrationBackendId?: OrchestrationBackendOverride;
orchestrationBackendId?: OrchestrationBackendId;
orchestrationBackendLabel?: string;
orchestrationFallbackReason?: string;
},
) {
const groupProjectId = input.groupProjectId.trim();
const requestMessageId = input.requestMessageId.trim();
const requestedBy = input.requestedBy.trim();
const summary = input.summary?.trim() ?? "";
const requestedOrchestrationBackendId = input.requestedOrchestrationBackendId;
const orchestrationBackendId = input.orchestrationBackendId ?? "boss-native-orchestrator";
const orchestrationBackendLabel =
trimToDefined(input.orchestrationBackendLabel) ??
(orchestrationBackendId === "omx-team" ? "OMX Team Runtime" : "Boss Native Orchestrator");
const orchestrationFallbackReason = trimToDefined(input.orchestrationFallbackReason);
if (!groupProjectId) throw new Error("DISPATCH_PLAN_GROUP_PROJECT_REQUIRED");
if (!requestMessageId) throw new Error("DISPATCH_PLAN_REQUEST_MESSAGE_REQUIRED");
@@ -4997,7 +5192,11 @@ function upsertDispatchPlanInState(
const payloadMatches =
existing.requestedBy === requestedBy &&
existing.summary === summary &&
sameDispatchPlanTargets(existing.targets, validatedTargets);
sameDispatchPlanTargets(existing.targets, validatedTargets) &&
existing.requestedOrchestrationBackendId === requestedOrchestrationBackendId &&
existing.orchestrationBackendId === orchestrationBackendId &&
existing.orchestrationBackendLabel === orchestrationBackendLabel &&
existing.orchestrationFallbackReason === orchestrationFallbackReason;
if (!payloadMatches) {
throw new Error("DISPATCH_PLAN_RETRY_MISMATCH");
}
@@ -5015,6 +5214,10 @@ function upsertDispatchPlanInState(
status: "pending_user_confirmation",
targets: validatedTargets,
summary,
requestedOrchestrationBackendId,
orchestrationBackendId,
orchestrationBackendLabel,
orchestrationFallbackReason,
createdAt: nowIso(),
};
state.dispatchPlans.unshift(plan);
@@ -5168,6 +5371,10 @@ export async function createDispatchExecutionsFromPlan(input: {
if (!sameStringSet(existingTargetIds, canonicalTargetProjectIds)) {
throw new Error("DISPATCH_EXECUTION_SET_MISMATCH");
}
for (const execution of existingExecutions) {
execution.orchestrationBackendId = execution.orchestrationBackendId ?? plan.orchestrationBackendId;
execution.orchestrationBackendLabel = execution.orchestrationBackendLabel ?? plan.orchestrationBackendLabel;
}
if (plan.status !== "dispatched") {
plan.status = "dispatched";
}
@@ -5192,6 +5399,8 @@ export async function createDispatchExecutionsFromPlan(input: {
targetProjectId: target.projectId,
targetThreadId: target.threadId,
deviceId: target.deviceId,
orchestrationBackendId: plan.orchestrationBackendId,
orchestrationBackendLabel: plan.orchestrationBackendLabel,
status: "queued",
createdAt,
};
@@ -5267,6 +5476,8 @@ function ensureDispatchExecutionTaskInState(
existing.targetThreadDisplayName = existing.targetThreadDisplayName ?? target.threadDisplayName;
existing.targetCodexThreadRef = existing.targetCodexThreadRef ?? target.codexThreadRef;
existing.targetCodexFolderRef = existing.targetCodexFolderRef ?? target.codexFolderRef;
existing.orchestrationBackendId = existing.orchestrationBackendId ?? execution.orchestrationBackendId;
existing.orchestrationBackendLabel = existing.orchestrationBackendLabel ?? execution.orchestrationBackendLabel;
existing.executionPrompt =
existing.executionPrompt ||
buildDispatchExecutionPrompt({
@@ -5299,6 +5510,8 @@ function ensureDispatchExecutionTaskInState(
targetThreadDisplayName: target.threadDisplayName,
targetCodexThreadRef: target.codexThreadRef,
targetCodexFolderRef: target.codexFolderRef,
orchestrationBackendId: execution.orchestrationBackendId,
orchestrationBackendLabel: execution.orchestrationBackendLabel,
status: "queued",
requestedAt: nowIso(),
};
@@ -5346,6 +5559,10 @@ export async function confirmDispatchPlanAndCreateExecutions(input: {
if (!sameStringSet(existingTargetIds, canonicalTargetProjectIds)) {
throw new Error("DISPATCH_EXECUTION_SET_MISMATCH");
}
for (const execution of existingExecutions) {
execution.orchestrationBackendId = execution.orchestrationBackendId ?? plan.orchestrationBackendId;
execution.orchestrationBackendLabel = execution.orchestrationBackendLabel ?? plan.orchestrationBackendLabel;
}
if (plan.status !== "dispatched") {
plan.status = "dispatched";
}
@@ -5368,6 +5585,8 @@ export async function confirmDispatchPlanAndCreateExecutions(input: {
targetProjectId: target.projectId,
targetThreadId: target.threadId,
deviceId: target.deviceId,
orchestrationBackendId: plan.orchestrationBackendId,
orchestrationBackendLabel: plan.orchestrationBackendLabel,
status: "queued",
createdAt,
};
@@ -5684,6 +5903,10 @@ export async function completeMasterAgentTask(payload: {
dispatchPlan?: {
summary?: string;
targets: DispatchPlanTarget[];
requestedOrchestrationBackendId?: OrchestrationBackendOverride;
orchestrationBackendId?: OrchestrationBackendId;
orchestrationBackendLabel?: string;
orchestrationFallbackReason?: string;
};
}) {
const result = await mutateState((state) => {
@@ -5776,6 +5999,10 @@ export async function completeMasterAgentTask(payload: {
requestedBy: task.requestedByAccount,
summary: payload.dispatchPlan.summary,
targets: payload.dispatchPlan.targets,
requestedOrchestrationBackendId: payload.dispatchPlan.requestedOrchestrationBackendId,
orchestrationBackendId: payload.dispatchPlan.orchestrationBackendId,
orchestrationBackendLabel: payload.dispatchPlan.orchestrationBackendLabel,
orchestrationFallbackReason: payload.dispatchPlan.orchestrationFallbackReason,
});
}
} else if (task.taskType === "device_import_resolution") {

View File

@@ -34,7 +34,9 @@ import {
createClawBackend,
getClawBackendSelectionState,
} from "@/lib/execution/backends/claw-backend";
import { getOmxTeamBackendSelectionState } from "@/lib/execution/backends/omx-team-backend";
import { listExecutionBackendChoices, selectExecutionBackend } from "@/lib/execution/backend-selector";
import { selectOrchestrationBackend } from "@/lib/execution/orchestration-backend-selector";
import { resolveRuntimeRelevantMemories } from "@/lib/execution/memory-resolver";
import type { RelevantMemory } from "@/lib/execution/memory-resolver";
import { buildExecutionPrompt } from "@/lib/execution/prompt-assembler";
@@ -1223,6 +1225,25 @@ type GroupDispatchRecommendationResult =
error: string;
};
async function resolveGroupOrchestrationBackend(project: Project) {
const requestedBackendId = project.orchestrationBackendOverride;
const omx = await getOmxTeamBackendSelectionState();
const selectedBackend = await selectOrchestrationBackend({
requestedBackendId,
omx,
});
const description = await selectedBackend.describe();
return {
requestedBackendId,
orchestrationBackendId: description.backendId,
orchestrationBackendLabel: description.label,
orchestrationFallbackReason:
requestedBackendId === "omx-team" && description.backendId !== "omx-team"
? omx.availability.reasonLabel
: undefined,
};
}
async function resolveGroupDispatchPlanTask(taskId: string): Promise<GroupDispatchRecommendationResult> {
const task = await getMasterAgentTask(taskId);
if (!task) {
@@ -1246,6 +1267,7 @@ async function resolveGroupDispatchPlanTask(taskId: string): Promise<GroupDispat
if (targets.length === 0) {
throw new Error("GROUP_DISPATCH_TARGETS_REQUIRED");
}
const orchestrationBackend = await resolveGroupOrchestrationBackend(project);
const completedTask = await completeMasterAgentTask({
taskId: task.taskId,
@@ -1254,6 +1276,10 @@ async function resolveGroupDispatchPlanTask(taskId: string): Promise<GroupDispat
dispatchPlan: {
summary: summarizeGroupDispatchPlan(task.requestText, targets),
targets,
requestedOrchestrationBackendId: orchestrationBackend.requestedBackendId,
orchestrationBackendId: orchestrationBackend.orchestrationBackendId,
orchestrationBackendLabel: orchestrationBackend.orchestrationBackendLabel,
orchestrationFallbackReason: orchestrationBackend.orchestrationFallbackReason,
},
});
@@ -1296,6 +1322,8 @@ export async function queueGroupDispatchPlan(params: {
throw new Error("PROJECT_NOT_GROUP_CHAT");
}
const orchestrationBackend = await resolveGroupOrchestrationBackend(project);
const task = await queueMasterAgentTask({
projectId: project.id,
taskType: "group_dispatch_plan",
@@ -1305,6 +1333,8 @@ export async function queueGroupDispatchPlan(params: {
requestedBy: params.requestedBy,
requestedByAccount: params.requestedBy,
deviceId: state.user.boundDeviceId || "mac-studio",
orchestrationBackendId: orchestrationBackend.orchestrationBackendId,
orchestrationBackendLabel: orchestrationBackend.orchestrationBackendLabel,
});
return resolveGroupDispatchPlanTask(task.taskId);

View File

@@ -3,7 +3,13 @@ import {
OMX_TEAM_BACKEND,
type OmxTeamBackendSelectionState,
} from "@/lib/execution/backends/omx-team-backend";
import type { OrchestrationBackend } from "@/lib/execution/orchestration-backend";
import {
labelForOrchestrationBackend,
normalizeOrchestrationBackendId,
type OrchestrationBackend,
type OrchestrationBackendChoiceView,
type OrchestrationBackendSelectionState,
} from "@/lib/execution/orchestration-backend";
export interface OrchestrationBackendSelectionInput {
requestedBackendId?: string;
@@ -24,10 +30,27 @@ function isReadyBackend(
return true;
}
function getRequestedBackendId(input: OrchestrationBackendSelectionInput) {
return normalizeOrchestrationBackendId(input.requestedBackendId);
}
function isSelectableBackend(
backendId: string,
input: OrchestrationBackendSelectionInput,
) {
if (backendId === OMX_TEAM_BACKEND.backendId) {
return input.omx?.selectable ?? false;
}
return true;
}
export async function selectOrchestrationBackend(
input: OrchestrationBackendSelectionInput = {},
): Promise<OrchestrationBackendChoice> {
return (await listOrchestrationBackendChoices(input))[0] ?? BOSS_NATIVE_ORCHESTRATOR;
const resolution = await resolveOrchestrationBackendSelection(input);
return resolution.currentBackendId === OMX_TEAM_BACKEND.backendId
? OMX_TEAM_BACKEND
: BOSS_NATIVE_ORCHESTRATOR;
}
export async function listOrchestrationBackendChoices(
@@ -60,4 +83,45 @@ export async function listOrchestrationBackendChoices(
return ordered;
}
export async function listOrchestrationBackendChoiceViews(
input: OrchestrationBackendSelectionInput = {},
): Promise<OrchestrationBackendChoiceView[]> {
const requestedBackendId = getRequestedBackendId(input);
const omxSelectable = isSelectableBackend(OMX_TEAM_BACKEND.backendId, input);
const choices = await listOrchestrationBackendChoices(input);
return choices.map((backend) => ({
backendId: backend.backendId,
label: labelForOrchestrationBackend(backend.backendId),
selectable: backend.backendId === OMX_TEAM_BACKEND.backendId ? omxSelectable : true,
current: backend.backendId === requestedBackendId && (!omxSelectable || backend.backendId === requestedBackendId)
? true
: backend.backendId === BOSS_NATIVE_ORCHESTRATOR.backendId && requestedBackendId !== OMX_TEAM_BACKEND.backendId,
}));
}
export async function resolveOrchestrationBackendSelection(
input: OrchestrationBackendSelectionInput = {},
): Promise<OrchestrationBackendSelectionState> {
const requestedBackendId = getRequestedBackendId(input);
const omxAvailability = input.omx?.availability;
const omxSelectable = input.omx?.selectable ?? false;
const omxRequested = requestedBackendId === OMX_TEAM_BACKEND.backendId;
const currentBackendId =
omxRequested && omxSelectable ? OMX_TEAM_BACKEND.backendId : BOSS_NATIVE_ORCHESTRATOR.backendId;
return {
requestedBackendId,
currentBackendId,
resolvedAt: new Date().toISOString(),
...(omxAvailability ? { omxAvailability } : {}),
...(omxRequested && !omxSelectable
? {
fallbackReason: "omx-team unavailable",
fallbackReasonLabel:
omxAvailability?.reasonLabel ??
"OMX Team Runtime 当前不可用,已自动回退到 Boss Native Orchestrator。",
}
: {}),
};
}
export const selectOrchestrationBackendForTesting = selectOrchestrationBackend;

View File

@@ -1,4 +1,34 @@
import type { OmxTeamBackendAvailability } from "@/lib/execution/backends/omx-team-config";
export type OrchestrationBackendId = "boss-native-orchestrator" | "omx-team";
export interface OrchestrationBackend {
backendId: string;
describe(): Promise<{ backendId: string; label: string }>;
backendId: OrchestrationBackendId;
describe(): Promise<{ backendId: OrchestrationBackendId; label: string }>;
}
export interface OrchestrationBackendChoiceView {
backendId: OrchestrationBackendId;
label: string;
selectable: boolean;
current: boolean;
}
export interface OrchestrationBackendSelectionState {
requestedBackendId: OrchestrationBackendId;
currentBackendId: OrchestrationBackendId;
resolvedAt: string;
fallbackReason?: string;
fallbackReasonLabel?: string;
omxAvailability?: OmxTeamBackendAvailability;
}
export function normalizeOrchestrationBackendId(
backendId?: string | null,
): OrchestrationBackendId {
return backendId?.trim() === "omx-team" ? "omx-team" : "boss-native-orchestrator";
}
export function labelForOrchestrationBackend(backendId: OrchestrationBackendId) {
return backendId === "omx-team" ? "OMX Team Runtime" : "Boss Native Orchestrator";
}