feat: add omx orchestration backend selection
This commit is contained in:
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user