TranslationEngine.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. /**
  2. * TranslationEngine - Orchestrates all mappers for agent conversion
  3. *
  4. * The TranslationEngine coordinates ToolMapper, PermissionMapper, ModelMapper,
  5. * ContextMapper, and CapabilityMatrix to provide complete agent translation
  6. * between OAC and other platforms.
  7. *
  8. * @example
  9. * ```ts
  10. * const engine = new TranslationEngine();
  11. * const result = engine.translate(agent, 'cursor');
  12. * // => { agent: translatedAgent, warnings: [...], compatible: true }
  13. * ```
  14. */
  15. import type {
  16. OpenAgent,
  17. AgentFrontmatter,
  18. ToolAccess,
  19. PermissionMap,
  20. ContextReference,
  21. SkillReference,
  22. } from "../types.js";
  23. import {
  24. mapToolAccessFromOAC,
  25. mapToolAccessToOAC,
  26. } from "../mappers/ToolMapper.js";
  27. import {
  28. mapPermissionsFromOAC,
  29. mapPermissionsToOAC,
  30. type DegradationStrategy,
  31. type BinaryPermissions,
  32. } from "../mappers/PermissionMapper.js";
  33. import {
  34. mapModelFromOAC,
  35. mapModelToOAC,
  36. } from "../mappers/ModelMapper.js";
  37. import {
  38. mapContextReferencesFromOAC,
  39. mapContextPathToOAC,
  40. mapSkillsToClaudeFormat,
  41. mapSkillsFromClaudeFormat,
  42. } from "../mappers/ContextMapper.js";
  43. import {
  44. analyzeCompatibility,
  45. getToolCapabilities,
  46. type Platform,
  47. type CompatibilityResult,
  48. } from "../core/CapabilityMatrix.js";
  49. // ============================================================================
  50. // Types
  51. // ============================================================================
  52. /**
  53. * Target platform for translation (excludes OAC since we translate TO/FROM OAC)
  54. */
  55. /**
  56. * The targets this engine can translate an {@link OpenAgent} to.
  57. *
  58. * `opencode` is excluded deliberately, and it is NOT an oversight to be fixed by widening this
  59. * later. This engine translates from `OpenAgent`, whose `permission` is an unordered `Record`;
  60. * OpenCode's permission semantics are ordered last-match-wins. Emitting OpenCode from an
  61. * unordered map would silently reorder a security-critical rule block — the exact corruption the
  62. * canonical refactor exists to remove. `OpenCodeAdapter.fromOAC()` therefore refuses outright
  63. * and directs callers to `fromCanonical(source)`, which the `oac build` path uses instead.
  64. *
  65. * Previously this read `Exclude<Platform, "oac">` and so tracked the matrix's platform list by
  66. * accident. Adding `opencode` to {@link Platform} on 2026-07-15 widened it silently and the
  67. * compiler caught it: this engine's mappers (`ToolMapper.ToolPlatform`) never knew OpenCode.
  68. * The coupling was the bug; this union is now stated outright.
  69. */
  70. export type TranslationTarget = Exclude<Platform, "oac" | "opencode">;
  71. /**
  72. * Configuration options for translation
  73. */
  74. export interface TranslationOptions {
  75. /** Strategy for handling 'ask' permissions (default: 'permissive') */
  76. permissionStrategy?: DegradationStrategy;
  77. /** Whether to include compatibility analysis (default: true) */
  78. analyzeCompatibility?: boolean;
  79. /** Whether to preserve unsupported features as comments (default: false) */
  80. preserveAsComments?: boolean;
  81. /** Custom model fallback if model not available on target */
  82. modelFallback?: string;
  83. }
  84. /**
  85. * Result of translating an agent
  86. */
  87. export interface TranslationResult {
  88. /** The translated agent frontmatter */
  89. frontmatter: Partial<AgentFrontmatter>;
  90. /** Translated tools (platform format) */
  91. tools?: Record<string, boolean>;
  92. /** Translated permissions (platform format) */
  93. permissions?: BinaryPermissions;
  94. /** Translated model ID */
  95. model?: string;
  96. /** Translated context paths */
  97. contextPaths?: string[];
  98. /** Translated skills (for Claude) */
  99. skills?: string[];
  100. /** All warnings generated during translation */
  101. warnings: string[];
  102. /** Compatibility analysis result */
  103. compatibility?: CompatibilityResult;
  104. /** Whether translation was successful */
  105. success: boolean;
  106. }
  107. /**
  108. * Result of translating from a platform back to OAC
  109. */
  110. export interface ReverseTranslationResult {
  111. /** Partial OAC agent that can be merged */
  112. agent: Partial<OpenAgent>;
  113. /** Warnings generated during translation */
  114. warnings: string[];
  115. /** Whether translation was successful */
  116. success: boolean;
  117. }
  118. // ============================================================================
  119. // Default Options
  120. // ============================================================================
  121. const DEFAULT_OPTIONS: Required<TranslationOptions> = {
  122. permissionStrategy: "permissive",
  123. analyzeCompatibility: true,
  124. preserveAsComments: false,
  125. modelFallback: "claude-sonnet-4",
  126. };
  127. // ============================================================================
  128. // TranslationEngine Class
  129. // ============================================================================
  130. /**
  131. * Engine that orchestrates all mappers for complete agent translation.
  132. */
  133. export class TranslationEngine {
  134. private options: Required<TranslationOptions>;
  135. constructor(options: TranslationOptions = {}) {
  136. this.options = { ...DEFAULT_OPTIONS, ...options };
  137. }
  138. // ==========================================================================
  139. // OAC → Platform Translation
  140. // ==========================================================================
  141. /**
  142. * Translate an OpenAgent to a target platform format.
  143. *
  144. * @param agent - The OpenAgent to translate
  145. * @param target - Target platform
  146. * @param options - Override default options
  147. * @returns Translation result
  148. */
  149. translate(
  150. agent: OpenAgent,
  151. target: TranslationTarget,
  152. options?: TranslationOptions
  153. ): TranslationResult {
  154. const opts = { ...this.options, ...options };
  155. const warnings: string[] = [];
  156. // Analyze compatibility first
  157. let compatibility: CompatibilityResult | undefined;
  158. if (opts.analyzeCompatibility) {
  159. compatibility = analyzeCompatibility(agent, target);
  160. warnings.push(...compatibility.warnings);
  161. }
  162. // Translate tools
  163. let tools: Record<string, boolean> | undefined;
  164. if (agent.frontmatter.tools) {
  165. const toolResult = mapToolAccessFromOAC(
  166. agent.frontmatter.tools,
  167. target
  168. );
  169. tools = toolResult.tools;
  170. warnings.push(...toolResult.warnings);
  171. }
  172. // Translate permissions
  173. let permissions: BinaryPermissions | undefined;
  174. if (agent.frontmatter.permission) {
  175. const permResult = mapPermissionsFromOAC(
  176. agent.frontmatter.permission,
  177. target,
  178. opts.permissionStrategy
  179. );
  180. permissions = permResult.permissions as BinaryPermissions;
  181. warnings.push(...permResult.warnings);
  182. }
  183. // Translate model
  184. let model: string | undefined;
  185. if (agent.frontmatter.model) {
  186. const modelResult = mapModelFromOAC(
  187. agent.frontmatter.model,
  188. target
  189. );
  190. model = modelResult.id;
  191. if (modelResult.warning) {
  192. warnings.push(modelResult.warning);
  193. }
  194. }
  195. // Translate contexts
  196. let contextPaths: string[] | undefined;
  197. if (agent.contexts && agent.contexts.length > 0) {
  198. const contextResult = mapContextReferencesFromOAC(
  199. agent.contexts,
  200. target
  201. );
  202. contextPaths = contextResult.paths;
  203. warnings.push(...contextResult.warnings);
  204. }
  205. // Translate skills (Claude-specific)
  206. let skills: string[] | undefined;
  207. if (target === "claude" && agent.frontmatter.skills) {
  208. const skillResult = mapSkillsToClaudeFormat(agent.frontmatter.skills);
  209. skills = skillResult.skills;
  210. warnings.push(...skillResult.warnings);
  211. }
  212. // Build translated frontmatter
  213. const frontmatter: Partial<AgentFrontmatter> = {
  214. name: agent.frontmatter.name,
  215. description: agent.frontmatter.description,
  216. mode: agent.frontmatter.mode,
  217. };
  218. // Include temperature if supported
  219. if (agent.frontmatter.temperature !== undefined) {
  220. const capabilities = getToolCapabilities(target);
  221. if (capabilities.supportsTemperature) {
  222. frontmatter.temperature = agent.frontmatter.temperature;
  223. }
  224. }
  225. return {
  226. frontmatter,
  227. tools,
  228. permissions,
  229. model,
  230. contextPaths,
  231. skills,
  232. warnings,
  233. compatibility,
  234. success: !compatibility || compatibility.compatible,
  235. };
  236. }
  237. // ==========================================================================
  238. // Platform → OAC Translation
  239. // ==========================================================================
  240. /**
  241. * Translate from a platform format back to OAC format.
  242. *
  243. * @param source - Platform-specific agent data
  244. * @param platform - Source platform
  245. * @returns Partial OpenAgent that can be merged
  246. */
  247. translateToOAC(
  248. source: {
  249. name?: string;
  250. description?: string;
  251. tools?: Record<string, boolean>;
  252. permissions?: BinaryPermissions;
  253. model?: string;
  254. contextPaths?: string[];
  255. skills?: string[];
  256. systemPrompt?: string;
  257. },
  258. platform: TranslationTarget
  259. ): ReverseTranslationResult {
  260. const warnings: string[] = [];
  261. // Translate tools
  262. let oacTools: ToolAccess | undefined;
  263. if (source.tools) {
  264. const toolResult = mapToolAccessToOAC(source.tools, platform);
  265. oacTools = toolResult.tools;
  266. warnings.push(...toolResult.warnings);
  267. }
  268. // Translate permissions
  269. let oacPermissions: PermissionMap | undefined;
  270. if (source.permissions) {
  271. const permResult = mapPermissionsToOAC(
  272. source.permissions,
  273. platform
  274. );
  275. oacPermissions = permResult.permissions as PermissionMap;
  276. warnings.push(...permResult.warnings);
  277. }
  278. // Translate model
  279. let oacModel: string | undefined;
  280. if (source.model) {
  281. const modelResult = mapModelToOAC(source.model, platform);
  282. oacModel = modelResult.id;
  283. if (modelResult.warning) {
  284. warnings.push(modelResult.warning);
  285. }
  286. }
  287. // Translate context paths
  288. let oacContexts: ContextReference[] | undefined;
  289. if (source.contextPaths && source.contextPaths.length > 0) {
  290. oacContexts = source.contextPaths.map((path) => {
  291. const result = mapContextPathToOAC(path, platform);
  292. if (result.warning) {
  293. warnings.push(result.warning);
  294. }
  295. return { path: result.path };
  296. });
  297. }
  298. // Translate skills (from Claude format)
  299. let oacSkills: SkillReference[] | undefined;
  300. if (platform === "claude" && source.skills) {
  301. oacSkills = mapSkillsFromClaudeFormat(source.skills);
  302. }
  303. // Build partial OpenAgent
  304. const agent: Partial<OpenAgent> = {
  305. frontmatter: {
  306. name: source.name || "Unnamed Agent",
  307. description: source.description || "Imported agent",
  308. mode: "primary",
  309. ...(oacTools && { tools: oacTools }),
  310. ...(oacPermissions && { permission: oacPermissions }),
  311. ...(oacModel && { model: oacModel }),
  312. ...(oacSkills && { skills: oacSkills }),
  313. },
  314. ...(source.systemPrompt && { systemPrompt: source.systemPrompt }),
  315. ...(oacContexts && { contexts: oacContexts }),
  316. };
  317. return {
  318. agent,
  319. warnings,
  320. success: true,
  321. };
  322. }
  323. // ==========================================================================
  324. // Batch Translation
  325. // ==========================================================================
  326. /**
  327. * Translate multiple agents to a target platform.
  328. *
  329. * @param agents - Array of OpenAgents to translate
  330. * @param target - Target platform
  331. * @returns Array of translation results
  332. */
  333. translateBatch(
  334. agents: OpenAgent[],
  335. target: TranslationTarget
  336. ): TranslationResult[] {
  337. return agents.map((agent) => this.translate(agent, target));
  338. }
  339. // ==========================================================================
  340. // Utility Methods
  341. // ==========================================================================
  342. /**
  343. * Get a preview of what will happen during translation without actually translating.
  344. *
  345. * @param agent - The agent to preview
  346. * @param target - Target platform
  347. * @returns Compatibility analysis
  348. */
  349. preview(agent: OpenAgent, target: TranslationTarget): CompatibilityResult {
  350. return analyzeCompatibility(agent, target);
  351. }
  352. /**
  353. * Check if an agent can be translated to a target with full fidelity.
  354. *
  355. * @param agent - The agent to check
  356. * @param target - Target platform
  357. * @returns True if no features will be lost
  358. */
  359. isFullyCompatible(agent: OpenAgent, target: TranslationTarget): boolean {
  360. const result = analyzeCompatibility(agent, target);
  361. return result.lost.length === 0 && result.degraded.length === 0;
  362. }
  363. /**
  364. * Get the current translation options.
  365. *
  366. * @returns Current options
  367. */
  368. getOptions(): Required<TranslationOptions> {
  369. return { ...this.options };
  370. }
  371. /**
  372. * Update translation options.
  373. *
  374. * @param options - Options to merge
  375. */
  376. setOptions(options: TranslationOptions): void {
  377. this.options = { ...this.options, ...options };
  378. }
  379. }
  380. // ============================================================================
  381. // Factory Functions
  382. // ============================================================================
  383. /**
  384. * Create a TranslationEngine with default options.
  385. *
  386. * @returns New TranslationEngine instance
  387. */
  388. export function createTranslationEngine(
  389. options?: TranslationOptions
  390. ): TranslationEngine {
  391. return new TranslationEngine(options);
  392. }
  393. /**
  394. * Quick translate function for one-off translations.
  395. *
  396. * @param agent - Agent to translate
  397. * @param target - Target platform
  398. * @param options - Translation options
  399. * @returns Translation result
  400. */
  401. export function translate(
  402. agent: OpenAgent,
  403. target: TranslationTarget,
  404. options?: TranslationOptions
  405. ): TranslationResult {
  406. const engine = new TranslationEngine(options);
  407. return engine.translate(agent, target);
  408. }
  409. /**
  410. * Quick preview function for one-off compatibility checks.
  411. *
  412. * @param agent - Agent to preview
  413. * @param target - Target platform
  414. * @returns Compatibility result
  415. */
  416. export function previewTranslation(
  417. agent: OpenAgent,
  418. target: TranslationTarget
  419. ): CompatibilityResult {
  420. return analyzeCompatibility(agent, target);
  421. }