agent-ecosystem/src/main/services/team/provisioning/TeamProvisioningLaunchCompatibility.ts

123 lines
4.6 KiB
TypeScript

import { fromProvisioningMembers } from '@features/team-runtime-lanes';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
import type { TeamCreateRequest } from '@shared/types';
export type TeamLaunchCompatibilityLevel = 'ready' | 'repairable' | 'unsafe';
export type TeamLaunchCompatibilityRosterSource = 'members-meta' | 'config' | 'inboxes' | 'missing';
export type TeamLaunchCompatibilityRepairAction = 'materialize-members-meta';
export interface TeamLaunchCompatibilityReport {
level: TeamLaunchCompatibilityLevel;
rosterSource: TeamLaunchCompatibilityRosterSource;
members: TeamCreateRequest['members'];
warnings: string[];
blockers: string[];
repairAction?: TeamLaunchCompatibilityRepairAction;
}
export function isOpenCodeLegacyProvisioningRequest(request: {
providerId?: unknown;
members?: readonly { providerId?: unknown; provider?: unknown }[];
}): boolean {
return (
normalizeOptionalTeamProviderId(request.providerId) === 'opencode' ||
(request.members ?? []).some(
(member) =>
normalizeOptionalTeamProviderId(member.providerId) === 'opencode' ||
normalizeOptionalTeamProviderId(member.provider) === 'opencode'
)
);
}
export function isPureOpenCodeProvisioningRequest(request: {
providerId?: unknown;
members?: readonly { providerId?: unknown; provider?: unknown }[];
}): boolean {
if (!isOpenCodeLegacyProvisioningRequest(request)) {
return false;
}
const rootProviderId = normalizeOptionalTeamProviderId(request.providerId);
if (rootProviderId && rootProviderId !== 'opencode') {
return false;
}
return (request.members ?? []).every((member) => {
const memberProviderId =
normalizeOptionalTeamProviderId(member.providerId) ??
normalizeOptionalTeamProviderId(member.provider);
return !memberProviderId || memberProviderId === 'opencode';
});
}
export function getOpenCodeMixedProviderProvisioningError(): string {
return (
'This OpenCode mixed-team request is outside the current support scope. ' +
'Supported mixed teams keep the lead on Anthropic or Codex. OpenCode-led mixed teams still remain blocked in this phase.'
);
}
export function getMixedLaunchFallbackRecoveryError(): string {
return 'This old mixed team is missing stable member metadata. Open Edit Team and save the roster once before launching.';
}
export function assertOpenCodeNotLaunchedThroughLegacyProvisioning(request: {
providerId?: unknown;
members?: readonly { providerId?: unknown; provider?: unknown }[];
}): void {
if (!isOpenCodeLegacyProvisioningRequest(request)) {
return;
}
const lanePlan = fromProvisioningMembers(
normalizeOptionalTeamProviderId(request.providerId),
(request.members ?? []).map((member, index) => ({
name: `member-${index + 1}`,
providerId:
normalizeOptionalTeamProviderId(member.providerId) ??
normalizeOptionalTeamProviderId(member.provider),
}))
);
if (!lanePlan.ok) {
throw new Error(lanePlan.message || getOpenCodeMixedProviderProvisioningError());
}
if (!isPureOpenCodeProvisioningRequest(request)) {
return;
}
throw new Error(
'OpenCode team launch is not enabled in the legacy Claude stream-json provisioning path. ' +
'Use the gated OpenCode runtime adapter once production launch is enabled.'
);
}
export function mergeProvisioningWarnings(
existing: string[] | undefined,
nextWarning: string | null
): string[] | undefined {
if (!nextWarning) return existing;
const merged = (existing ?? []).filter((warning) => warning !== nextWarning);
merged.push(nextWarning);
return merged.length > 0 ? merged : undefined;
}
const DETERMINISTIC_BOOTSTRAP_LARGE_TEAM_WARNING_THRESHOLD = 8;
const DETERMINISTIC_BOOTSTRAP_MAX_PRIMARY_MEMBERS = 20;
export function buildLargeDeterministicBootstrapWarning(memberCount: number): string | null {
if (memberCount <= DETERMINISTIC_BOOTSTRAP_LARGE_TEAM_WARNING_THRESHOLD) {
return null;
}
return (
`Large Codex team launch: ${memberCount} primary teammates will bootstrap in one runtime. ` +
`Launches above ${DETERMINISTIC_BOOTSTRAP_LARGE_TEAM_WARNING_THRESHOLD} teammates can be slower and more likely to hit provider rate limits or bootstrap timeouts.`
);
}
export function assertDeterministicBootstrapPrimaryMemberLimit(memberCount: number): void {
if (memberCount <= DETERMINISTIC_BOOTSTRAP_MAX_PRIMARY_MEMBERS) {
return;
}
throw new Error(
`Codex deterministic bootstrap currently supports up to ${DETERMINISTIC_BOOTSTRAP_MAX_PRIMARY_MEMBERS} primary teammates; this team has ${memberCount}. Reduce primary teammates or move extra OpenCode members to secondary lanes.`
);
}