index.ts 74 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251
  1. import type { AgentConfig as SDKAgentConfig } from '@opencode-ai/sdk/v2';
  2. import { getSkillPermissionsForAgent } from '../cli/skills';
  3. import {
  4. AGENT_ALIASES,
  5. type AgentOverrideConfig,
  6. ALL_AGENT_NAMES,
  7. DEFAULT_DISABLED_AGENTS,
  8. DEFAULT_MODELS,
  9. loadAgentPrompt,
  10. loadPluginConfig,
  11. type PluginConfig,
  12. PROTECTED_AGENTS,
  13. SUBAGENT_NAMES,
  14. } from '../config';
  15. import { getAgentMcpList } from '../config/agent-mcps';
  16. import { type HostConfigSnapshot, RuntimeConfig } from '../config/runtime';
  17. import { applyOrchestratorModelConfig } from '../config/strip-orchestrator-model';
  18. import {
  19. type ActivatedMarketplaceAgent,
  20. composePackagePrompt,
  21. type MarketplaceActivationPlan,
  22. type MarketplaceDiagnostic,
  23. reservedRuntimeNames,
  24. resolveMarketplaceActivation,
  25. } from '../marketplace/activation';
  26. import { renderMarketplaceAutoDelegationBlock } from '../marketplace/routing';
  27. import { MARKETPLACE_TOOL_NAMES } from '../marketplace/schemas';
  28. import type {
  29. MarketplaceLivePackage,
  30. MarketplaceLiveSnapshot,
  31. } from '../marketplace/status';
  32. import type { MarketplaceStore } from '../marketplace/store';
  33. import {
  34. escapeRegExp,
  35. isSafeAgentAlias,
  36. normalizeAgentName,
  37. } from '../utils/agent-variant';
  38. import { COUNCIL_SYNTHESIS_REINFORCEMENT, createCouncilAgent } from './council';
  39. import { buildCouncillorAgents, getCouncillorSeatName } from './council-agents';
  40. import { createCouncillorAgent } from './councillor';
  41. import type { RoutingEntry } from './orchestrator';
  42. import {
  43. type AgentDefinition,
  44. createOrchestratorAgent,
  45. renderCouncilRoutingBlock,
  46. resolvePrompt,
  47. } from './orchestrator';
  48. import {
  49. ROLE_DEFINITIONS,
  50. renderRoleRoutingBlock,
  51. SUPPORTED_SPECIALIST_ROLES,
  52. } from './role-definitions';
  53. import {
  54. appendTaskRejectionInstruction,
  55. TASK_REJECTION_INSTRUCTION,
  56. } from './task-rejection';
  57. export type { AgentDefinition } from './orchestrator';
  58. export type { RoleDefinition, SpecialistRole } from './role-definitions';
  59. export {
  60. ROLE_DEFINITIONS,
  61. SUPPORTED_SPECIALIST_ROLES,
  62. } from './role-definitions';
  63. type AgentFactory = (
  64. model: string,
  65. customPrompt?: string,
  66. customAppendPrompt?: string,
  67. ) => AgentDefinition;
  68. interface CreateAgentsOptions {
  69. projectDirectory?: string;
  70. marketplace?: MarketplaceActivationPlan;
  71. marketplaceStore?: MarketplaceStore;
  72. availableMcpNames?: readonly string[];
  73. }
  74. const TASK_CONTROL_DEFAULTS = [
  75. 'task_cancel',
  76. 'task_message',
  77. 'task_revive',
  78. 'task_status',
  79. 'task_result',
  80. ] as const;
  81. export interface ResolvedAgentRegistry {
  82. readonly agents: readonly AgentDefinition[];
  83. readonly sdkConfigs: Readonly<Record<string, SDKAgentConfig>>;
  84. readonly modelArrays: Readonly<
  85. Record<string, readonly { id: string; variant?: string }[]>
  86. >;
  87. readonly modelChains: Readonly<Record<string, readonly string[]>>;
  88. readonly mcpLists: Readonly<Record<string, readonly string[]>>;
  89. readonly skillPermissions: Readonly<
  90. Record<string, Readonly<Record<string, 'allow' | 'ask' | 'deny'>>>
  91. >;
  92. readonly routing: readonly RoutingEntry[];
  93. readonly provenance: Readonly<Record<string, string>>;
  94. readonly runtimeNameByCanonicalId: Readonly<Record<string, string>>;
  95. readonly canonicalIdByRuntimeName: Readonly<Record<string, string>>;
  96. readonly packageIdByRuntimeName: Readonly<Record<string, string>>;
  97. readonly runtimeNameByPackageId: Readonly<Record<string, string>>;
  98. readonly marketplaceLive: readonly MarketplaceLivePackage[];
  99. readonly diagnostics: readonly MarketplaceDiagnostic[];
  100. }
  101. type PermissionAction = 'allow' | 'ask' | 'deny';
  102. type PermissionRecord = Record<string, unknown>;
  103. function deepFreeze<T>(value: T): T {
  104. if (value && typeof value === 'object') {
  105. for (const nested of Object.values(value as Record<string, unknown>)) {
  106. deepFreeze(nested);
  107. }
  108. if (!Object.isFrozen(value)) Object.freeze(value);
  109. }
  110. return value;
  111. }
  112. /** Clone JSON-shaped data before handing it to a host-owned configuration. */
  113. export function cloneOwned<T>(value: T): T {
  114. if (Array.isArray(value)) {
  115. return value.map((entry) => cloneOwned(entry)) as unknown as T;
  116. }
  117. if (value !== null && typeof value === 'object') {
  118. const clone: Record<string, unknown> = {};
  119. for (const [key, entry] of Object.entries(
  120. value as Record<string, unknown>,
  121. )) {
  122. clone[key] = cloneOwned(entry);
  123. }
  124. return clone as T;
  125. }
  126. return value;
  127. }
  128. export function normalizePermission(permission: unknown): PermissionRecord {
  129. if (typeof permission === 'string') {
  130. return { '*': permission as PermissionAction };
  131. }
  132. if (permission && typeof permission === 'object') {
  133. return cloneOwned(permission as PermissionRecord);
  134. }
  135. return {};
  136. }
  137. /** A wildcard deny must remain authoritative under v2's last-match-wins rules. */
  138. function hasWildcardDeny(permission: PermissionRecord): boolean {
  139. return permission['*'] === 'deny';
  140. }
  141. function mergePermissionRules(
  142. base: PermissionRecord,
  143. override: PermissionRecord,
  144. ): PermissionRecord {
  145. const result = cloneOwned(base);
  146. for (const [key, value] of Object.entries(override)) {
  147. const previous = result[key];
  148. if (
  149. previous !== null &&
  150. typeof previous === 'object' &&
  151. !Array.isArray(previous) &&
  152. value !== null &&
  153. typeof value === 'object' &&
  154. !Array.isArray(value)
  155. ) {
  156. result[key] = mergePermissionRules(
  157. previous as PermissionRecord,
  158. value as PermissionRecord,
  159. );
  160. } else {
  161. result[key] = cloneOwned(value);
  162. }
  163. }
  164. return result;
  165. }
  166. function applyTaskControlDefaults(
  167. agentName: string,
  168. permission: PermissionRecord,
  169. ): void {
  170. const isOrchestrator = agentName === 'orchestrator';
  171. for (const toolName of TASK_CONTROL_DEFAULTS) {
  172. permission[toolName] ??= isOrchestrator ? 'allow' : 'deny';
  173. }
  174. permission.wait_for_user ??= isOrchestrator ? 'allow' : 'deny';
  175. permission.marketplace ??= isOrchestrator ? 'allow' : 'deny';
  176. }
  177. function isDeniedPermissionValue(value: unknown): boolean {
  178. if (value === 'deny') return true;
  179. if (value === null || typeof value !== 'object' || Array.isArray(value)) {
  180. return false;
  181. }
  182. const map = value as PermissionRecord;
  183. if (map['*'] === 'deny') return true;
  184. const actions = Object.values(map);
  185. return actions.length > 0 && actions.every((entry) => entry === 'deny');
  186. }
  187. /** True when marketplace is denied by a string action or nested pattern map. */
  188. export function isMarketplacePermissionDenied(permission: unknown): boolean {
  189. const record = normalizePermission(permission);
  190. if (isDeniedPermissionValue(record.marketplace)) return true;
  191. return (
  192. record.marketplace === undefined && isDeniedPermissionValue(record['*'])
  193. );
  194. }
  195. /** True when the marketplace tool is registered and not denied for orchestrator. */
  196. export function isMarketplaceToolAvailable(runtime: RuntimeConfig): boolean {
  197. if (runtime.disabledTools.includes('marketplace')) return false;
  198. return !isMarketplacePermissionDenied(
  199. runtime.agent('orchestrator')?.permission,
  200. );
  201. }
  202. /** Resolve marketplace live identities from on-disk config without touching
  203. * the session RuntimeConfig singleton. */
  204. export function resolveDesiredMarketplaceLiveFromDisk(
  205. projectDirectory: string,
  206. store: MarketplaceStore,
  207. host?: HostConfigSnapshot,
  208. ): MarketplaceLiveSnapshot {
  209. const config = loadPluginConfig(projectDirectory, { silent: true });
  210. const directory = `${projectDirectory}\0marketplace-desired`;
  211. RuntimeConfig.reset(directory);
  212. const runtime = RuntimeConfig.init(directory, config);
  213. if (host) runtime.captureHostConfig(host);
  214. try {
  215. const registry = buildResolvedAgentRegistry(runtime, {
  216. marketplaceStore: store,
  217. projectDirectory,
  218. });
  219. return {
  220. packages: [...registry.marketplaceLive],
  221. diagnostics: [...registry.diagnostics],
  222. };
  223. } catch (error) {
  224. return {
  225. packages: [],
  226. diagnostics: [
  227. {
  228. packageId: '(store)',
  229. code: 'operational',
  230. message: error instanceof Error ? error.message : String(error),
  231. },
  232. ],
  233. };
  234. } finally {
  235. RuntimeConfig.reset(directory);
  236. }
  237. }
  238. function buildMarketplaceLive(
  239. marketplace: MarketplaceActivationPlan,
  240. runtimeNameByPackageId: Readonly<Record<string, string>>,
  241. ): MarketplaceLivePackage[] {
  242. const registered = new Map<string, { version: string; digest: string }>();
  243. for (const activated of marketplace.agents) {
  244. registered.set(activated.packageId, {
  245. version: activated.version,
  246. digest: activated.digest,
  247. });
  248. }
  249. const live: MarketplaceLivePackage[] = [];
  250. for (const [packageId, runtimeName] of Object.entries(
  251. runtimeNameByPackageId,
  252. )) {
  253. const meta = registered.get(packageId);
  254. if (!meta) continue;
  255. live.push({
  256. packageId,
  257. version: meta.version,
  258. digest: meta.digest,
  259. runtimeName,
  260. });
  261. }
  262. return live;
  263. }
  264. /** Project registry defaults into a host-owned config object without mutating
  265. * registry data. Task controls are defaults; wait_for_user and marketplace
  266. * are immutable plugin gates for non-orchestrator agents. */
  267. export function projectAgentPermission(
  268. agentName: string,
  269. hostEntry: Record<string, unknown>,
  270. registry: ResolvedAgentRegistry,
  271. ): void {
  272. const canonicalName =
  273. registry.canonicalIdByRuntimeName[agentName] ?? agentName;
  274. const registryEntry =
  275. registry.sdkConfigs[canonicalName] ?? registry.sdkConfigs[agentName];
  276. const permission = normalizePermission(registryEntry?.permission);
  277. const hostPermission = normalizePermission(hostEntry.permission);
  278. const projected = mergePermissionRules(permission, hostPermission);
  279. if (!hasWildcardDeny(projected)) {
  280. applyTaskControlDefaults(canonicalName, projected);
  281. }
  282. if (canonicalName !== 'orchestrator') {
  283. projected.wait_for_user = 'deny';
  284. projected.marketplace = 'deny';
  285. }
  286. hostEntry.permission = cloneOwned(projected);
  287. }
  288. function projectPermissionValues(
  289. canonicalName: string,
  290. registryPermission: unknown,
  291. hostPermission: unknown,
  292. ): PermissionRecord {
  293. const permission = normalizePermission(registryPermission);
  294. const projected = mergePermissionRules(
  295. permission,
  296. normalizePermission(hostPermission),
  297. );
  298. if (!hasWildcardDeny(projected)) {
  299. applyTaskControlDefaults(canonicalName, projected);
  300. }
  301. if (canonicalName !== 'orchestrator') {
  302. projected.wait_for_user = 'deny';
  303. projected.marketplace = 'deny';
  304. }
  305. return projected;
  306. }
  307. export function cloneAgentConfigs(
  308. configs: Readonly<Record<string, SDKAgentConfig>>,
  309. ): Record<string, SDKAgentConfig> {
  310. return cloneOwned(configs);
  311. }
  312. export function resolvePrimaryModelValue(value: unknown): string | undefined {
  313. if (typeof value === 'string') return value;
  314. if (!Array.isArray(value) || value.length === 0) return undefined;
  315. const first = value[0];
  316. return typeof first === 'string'
  317. ? first
  318. : first && typeof first === 'object' && 'id' in first
  319. ? typeof first.id === 'string'
  320. ? first.id
  321. : undefined
  322. : undefined;
  323. }
  324. function getPrimaryModelFromOverride(
  325. override: AgentOverrideConfig | undefined,
  326. ): string | undefined {
  327. return resolvePrimaryModelValue(override?.model);
  328. }
  329. function hasExplicitVariantOverride(
  330. override: AgentOverrideConfig | undefined,
  331. ): boolean {
  332. if (override?.variant !== undefined) return true;
  333. if (!Array.isArray(override?.model) || override.model.length === 0) {
  334. return false;
  335. }
  336. const primary = override.model[0];
  337. return typeof primary !== 'string' && primary.variant !== undefined;
  338. }
  339. function stringVariant(value: unknown): string | undefined {
  340. return typeof value === 'string' ? value : undefined;
  341. }
  342. const isInternalOnly = (name: string): boolean =>
  343. name === 'councillor' || name.startsWith('councillor-');
  344. /**
  345. * Alias-aware override lookup inside a merged (preset-aware) agents record.
  346. * Mirrors getAgentOverride semantics without the host layer, which the
  347. * config hook applies separately at merge time.
  348. */
  349. function getOverrideFromAgents(
  350. agents: Record<string, AgentOverrideConfig>,
  351. name: string,
  352. ): AgentOverrideConfig | undefined {
  353. return (
  354. agents[name] ??
  355. agents[
  356. Object.keys(AGENT_ALIASES).find((key) => AGENT_ALIASES[key] === name) ??
  357. ''
  358. ]
  359. );
  360. }
  361. function buildAcpAgentDefinition(
  362. name: string,
  363. config: NonNullable<PluginConfig['acpAgents']>[string],
  364. fallbackModel?: string,
  365. ): AgentDefinition {
  366. const description =
  367. config.description ?? `External ACP agent '${name}' via ${config.command}`;
  368. const prompt =
  369. config.prompt ??
  370. [
  371. `You are the ${name} ACP wrapper agent.`,
  372. '',
  373. 'Your only job is to send the user task to the configured external ACP agent using the acp_run tool, then return the ACP agent result.',
  374. `Always call acp_run with agent: ${JSON.stringify(
  375. name,
  376. )} and pass the full user task as prompt.`,
  377. 'Do not edit files yourself unless the ACP result explicitly asks you to report a local follow-up to the orchestrator.',
  378. ].join('\n');
  379. return {
  380. name,
  381. description,
  382. config: {
  383. model: config.wrapperModel ?? fallbackModel ?? DEFAULT_MODELS.oracle,
  384. prompt,
  385. permission: {
  386. read: 'deny',
  387. edit: 'deny',
  388. bash: 'deny',
  389. task: 'deny',
  390. glob: 'deny',
  391. grep: 'deny',
  392. list: 'deny',
  393. webfetch: 'deny',
  394. question: 'deny',
  395. skill: 'deny',
  396. acp_run: 'allow',
  397. },
  398. },
  399. } as AgentDefinition;
  400. }
  401. function isSafeDisplayName(displayName: string): boolean {
  402. return isSafeAgentAlias(displayName);
  403. }
  404. // Agent Configuration Helpers
  405. /**
  406. * Apply user-provided overrides to an agent's configuration.
  407. * Supports overriding model (string or priority array), variant, and temperature.
  408. * When model is an array, stores it as _modelArray for runtime fallback resolution
  409. * and selects its primary entry for ephemeral subagents. The orchestrator leaves
  410. * config.model unset so its live runtime selection is not overwritten.
  411. */
  412. function applyOverrides(
  413. agent: AgentDefinition,
  414. override: AgentOverrideConfig,
  415. ): void {
  416. if (override.model !== undefined) {
  417. if (Array.isArray(override.model)) {
  418. // A model override replaces a marketplace candidate chain, including
  419. // its package-level primary variant. Re-apply only the owner-provided
  420. // variant below.
  421. delete agent.config.variant;
  422. agent._modelArray = override.model.map((m) =>
  423. typeof m === 'string' ? { id: m } : m,
  424. );
  425. const primaryModel = agent._modelArray[0];
  426. // Subagents are ephemeral, freshly-created sessions with no prior
  427. // runtime state to preserve, so giving them a concrete config.model
  428. // at launch time (the array's primary entry) is safe — see #9100e59.
  429. // ForegroundFallbackManager handles runtime failover to the
  430. // remaining entries in _modelArray.
  431. //
  432. // The orchestrator is different: it's a long-lived, foreground
  433. // session where a user's runtime `/model` selection must survive
  434. // across plugin re-inits (triggered by client.config.update() ->
  435. // Instance.dispose(), e.g. on every subagent dispatch). Setting
  436. // config.model here unconditionally would stomp that live
  437. // selection every time this function re-runs, because it runs
  438. // BEFORE the config() hook's merge with the live
  439. // opencodeConfig.agent.orchestrator.model (see src/index.ts:524-528,
  440. // added by #639). Leaving it undefined for the orchestrator lets
  441. // that later, precedence-aware guard be the sole source of truth.
  442. agent.config.model =
  443. agent.name === 'orchestrator' ? undefined : primaryModel.id;
  444. // Subagents launch with the primary model, so carry its inline variant
  445. // into the OpenCode config too. An explicit agent-level variant below
  446. // intentionally takes precedence.
  447. if (
  448. agent.name !== 'orchestrator' &&
  449. override.variant === undefined &&
  450. primaryModel.variant !== undefined
  451. ) {
  452. agent.config.variant = primaryModel.variant;
  453. }
  454. } else {
  455. // Marketplace agents may have installed an explicit package fallback
  456. // chain before owner overrides are applied. A scalar owner model is a
  457. // replacement, not an additional candidate, so discard that chain and
  458. // its package-level primary variant.
  459. delete agent._modelArray;
  460. delete agent.config.variant;
  461. agent.config.model = override.model;
  462. }
  463. }
  464. if (override.variant !== undefined) agent.config.variant = override.variant;
  465. if (override.temperature !== undefined)
  466. agent.config.temperature = override.temperature;
  467. if (override.color) agent.config.color = override.color;
  468. if (override.options) {
  469. agent.config.options = {
  470. ...agent.config.options,
  471. ...override.options,
  472. };
  473. }
  474. if (override.displayName) {
  475. agent.displayName = override.displayName;
  476. }
  477. if (override.description) {
  478. agent.description = override.description;
  479. }
  480. if (override.permission) {
  481. agent.config.permission = override.permission;
  482. }
  483. }
  484. /**
  485. * Apply an explicit model inheritance policy after the agent factory has
  486. * supplied its built-in fallback model. OpenCode uses the parent session model
  487. * when an agent config does not specify `model`.
  488. */
  489. function applyModelInheritance(
  490. agent: AgentDefinition,
  491. override: AgentOverrideConfig | undefined,
  492. orchestratorModel: string | undefined,
  493. ): void {
  494. if (override?.model !== undefined) return;
  495. if (override?.inheritModelFrom === 'session') {
  496. delete agent._modelArray;
  497. delete agent.config.model;
  498. if (!hasExplicitVariantOverride(override)) {
  499. delete agent.config.variant;
  500. }
  501. return;
  502. }
  503. if (override?.inheritModelFrom === 'orchestrator') {
  504. delete agent._modelArray;
  505. if (orchestratorModel === undefined) {
  506. delete agent.config.model;
  507. } else {
  508. agent.config.model = orchestratorModel;
  509. }
  510. if (!hasExplicitVariantOverride(override)) {
  511. delete agent.config.variant;
  512. }
  513. }
  514. }
  515. /**
  516. * Resolve the model an agent's final config carries, mirroring the combined
  517. * effect of `createAgents` fallbacks, `applyOverrides`, and the inheritance
  518. * passes. Returns `undefined` exactly when the agent config ends up with NO
  519. * model key — i.e. `inheritModelFrom: 'session'` (or `'orchestrator'` with no
  520. * configured orchestrator model) — in which case OpenCode serves the agent
  521. * with the parent session's current model.
  522. *
  523. * This is the single resolution source for both agent definition building and
  524. * background-task admission, so provider/model concurrency accounting keys off
  525. * the model the spawned subagent actually uses. Explicit `model` wins; then
  526. * `inheritModelFrom`; then the historical fixer → librarian fallback; then
  527. * the preset primary model; then the per-agent default.
  528. */
  529. export function resolveAgentConfigModel(
  530. runtime: RuntimeConfig,
  531. name: string,
  532. ): string | undefined {
  533. const mergedAgents = runtime.agents();
  534. const override = getOverrideFromAgents(mergedAgents, name);
  535. if (override?.model !== undefined) {
  536. return getPrimaryModelFromOverride(override);
  537. }
  538. if (override?.inheritModelFrom === 'session') {
  539. return undefined;
  540. }
  541. if (override?.inheritModelFrom === 'orchestrator') {
  542. return getPrimaryModelFromOverride(
  543. getOverrideFromAgents(mergedAgents, 'orchestrator'),
  544. );
  545. }
  546. // Dynamic councillors are defined outside `agents()` under the selected
  547. // council preset. Their generated agent config carries the preset model.
  548. if (name.startsWith('councillor-')) {
  549. const seat = name.slice('councillor-'.length);
  550. const preset =
  551. runtime.council?.presets?.[runtime.council.default_preset ?? 'default'];
  552. return preset?.[seat]?.models?.[0]?.id;
  553. }
  554. // ACP agents are generated from `acpAgents`; admission is for the wrapper
  555. // session, so account for its configured wrapper model when present.
  556. if (runtime.acpAgents[name]?.wrapperModel) {
  557. return runtime.acpAgents[name].wrapperModel;
  558. }
  559. if (name === 'fixer') {
  560. const librarianModel = getPrimaryModelFromOverride(
  561. getOverrideFromAgents(mergedAgents, 'librarian'),
  562. );
  563. return (
  564. librarianModel ??
  565. runtime.primaryModel ??
  566. ROLE_DEFINITIONS.librarian.defaultModel
  567. );
  568. }
  569. const roleName = runtime.agent(name)?.baseRole ?? name;
  570. const role = ROLE_DEFINITIONS[roleName as keyof typeof ROLE_DEFINITIONS];
  571. return (
  572. runtime.primaryModel ??
  573. role?.defaultModel ??
  574. (DEFAULT_MODELS as Record<string, string | undefined>)[name]
  575. );
  576. }
  577. /**
  578. * Apply model inheritance to the final host agent config after the host layer
  579. * has been merged. This clears stale host models for `session` inheritance,
  580. * which cannot be handled by the agent definition alone.
  581. */
  582. export function applyModelInheritanceToConfig(
  583. configAgent: Record<string, unknown>,
  584. runtime: RuntimeConfig,
  585. ): void {
  586. const mergedAgents = runtime.agents();
  587. const orchestratorModel = getPrimaryModelFromOverride(
  588. runtime.agent('orchestrator'),
  589. );
  590. for (const agentName of Object.keys(configAgent)) {
  591. const override = getOverrideFromAgents(mergedAgents, agentName);
  592. if (!override) continue;
  593. if (
  594. override.model !== undefined ||
  595. override.inheritModelFrom === undefined
  596. ) {
  597. continue;
  598. }
  599. const resolvedName = AGENT_ALIASES[agentName] ?? agentName;
  600. const entry = configAgent[resolvedName];
  601. if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
  602. continue;
  603. }
  604. const agentConfig = entry as Record<string, unknown>;
  605. const hostAgent =
  606. runtime.hostAgent(agentName) ?? runtime.hostAgent(resolvedName);
  607. const hasHostVariant = hostAgent?.variant !== undefined;
  608. const preserveVariant =
  609. hasExplicitVariantOverride(override) || hasHostVariant;
  610. if (override.inheritModelFrom === 'session') {
  611. delete agentConfig.model;
  612. } else if (orchestratorModel === undefined) {
  613. delete agentConfig.model;
  614. } else {
  615. agentConfig.model = orchestratorModel;
  616. }
  617. if (!preserveVariant) {
  618. delete agentConfig.variant;
  619. }
  620. }
  621. }
  622. function isKnownAgentName(name: string): boolean {
  623. return (ALL_AGENT_NAMES as readonly string[]).includes(name);
  624. }
  625. function normalizeCustomAgentName(name: string): string {
  626. return name.trim();
  627. }
  628. function isSafeCustomAgentName(name: string): boolean {
  629. return isSafeAgentAlias(name) && !isKnownAgentName(name);
  630. }
  631. function hasCustomAgentModel(
  632. override: AgentOverrideConfig | undefined,
  633. ): override is AgentOverrideConfig & {
  634. model: NonNullable<AgentOverrideConfig['model']>;
  635. } {
  636. if (!override?.model) {
  637. return false;
  638. }
  639. return !Array.isArray(override.model) || override.model.length > 0;
  640. }
  641. function buildCustomAgentDefinition(
  642. name: string,
  643. override: AgentOverrideConfig,
  644. filePrompt?: string,
  645. fileAppendPrompt?: string,
  646. fallbackModel?: string,
  647. ): AgentDefinition {
  648. const role = override.baseRole
  649. ? ROLE_DEFINITIONS[override.baseRole]
  650. : undefined;
  651. const defaultPrompt = role?.basePrompt ?? `You are the ${name} specialist.`;
  652. const primaryModel = getPrimaryModelFromOverride(override);
  653. const description =
  654. override.description ?? role?.description ?? `Custom subagent '${name}'`;
  655. return {
  656. name,
  657. ...(role ? { baseRole: role.id } : {}),
  658. description,
  659. config: {
  660. model:
  661. primaryModel ??
  662. fallbackModel ??
  663. role?.defaultModel ??
  664. DEFAULT_MODELS.oracle,
  665. prompt: resolvePrompt(
  666. name,
  667. override.prompt,
  668. filePrompt,
  669. defaultPrompt,
  670. fileAppendPrompt,
  671. [TASK_REJECTION_INSTRUCTION],
  672. ),
  673. },
  674. } as AgentDefinition;
  675. }
  676. function rewriteRoutingPrompt(
  677. prompt: string,
  678. nameMap: ReadonlyMap<string, string>,
  679. ): string {
  680. let rewritten = prompt;
  681. for (const [internalName, displayName] of nameMap) {
  682. rewritten = rewritten.replace(
  683. new RegExp(`@${escapeRegExp(internalName)}\\b`, 'g'),
  684. `@${normalizeAgentName(displayName)}`,
  685. );
  686. }
  687. return rewritten;
  688. }
  689. /**
  690. * Apply default permissions to an agent.
  691. * Sets 'question' permission to 'allow' and includes skill permission presets.
  692. * If configuredSkills is provided, it honors that list instead of defaults.
  693. *
  694. * Note: If the agent already explicitly sets question to 'deny', that is
  695. * respected (e.g. councillor should not ask questions).
  696. */
  697. function applyDefaultPermissionPolicy(
  698. agent: AgentDefinition,
  699. configuredSkills?: readonly string[],
  700. disabledSkills?: readonly string[],
  701. ): void {
  702. const existing = normalizePermission(agent.config.permission);
  703. // A user/package deny-all intentionally disables every capability. Adding
  704. // role, question, or skill allows after it reopens those tools on v2.
  705. if (hasWildcardDeny(existing)) {
  706. agent.config.permission = existing as SDKAgentConfig['permission'];
  707. return;
  708. }
  709. const role = agent.baseRole ? ROLE_DEFINITIONS[agent.baseRole] : undefined;
  710. // Get skill-specific permissions for this agent
  711. const skillPermissions = getSkillPermissionsForAgent(
  712. configuredSkills ? agent.name : (agent.baseRole ?? agent.name),
  713. configuredSkills ?? role?.defaultSkills,
  714. disabledSkills,
  715. );
  716. const filePermissions = role
  717. ? {
  718. read: existing.read ?? 'allow',
  719. edit:
  720. existing.edit ??
  721. (role.permissionPolicy === 'read-write' ? 'allow' : 'deny'),
  722. write:
  723. existing.write ??
  724. (role.permissionPolicy === 'read-write' ? 'allow' : 'deny'),
  725. apply_patch:
  726. existing.apply_patch ??
  727. (role.permissionPolicy === 'read-write' ? 'allow' : 'deny'),
  728. ast_grep_replace:
  729. existing.ast_grep_replace ??
  730. (role.permissionPolicy === 'read-write' ? 'allow' : 'deny'),
  731. }
  732. : {};
  733. // Respect explicit deny on question (councillor)
  734. const questionPerm = existing.question === 'deny' ? 'deny' : 'allow';
  735. agent.config.permission = {
  736. ...existing,
  737. ...filePermissions,
  738. question: questionPerm,
  739. // Apply skill permissions as nested object under 'skill' key
  740. skill: {
  741. ...(typeof existing.skill === 'object' ? existing.skill : {}),
  742. ...skillPermissions,
  743. },
  744. } as unknown as SDKAgentConfig['permission'];
  745. }
  746. /** Task controls are editable defaults, not immutable gates. */
  747. function applyDefaultTaskControls(agent: AgentDefinition): void {
  748. const permission = normalizePermission(agent.config.permission);
  749. if (hasWildcardDeny(permission)) {
  750. agent.config.permission = permission as SDKAgentConfig['permission'];
  751. return;
  752. }
  753. const canonicalName = agent.baseRole ?? agent.name;
  754. applyTaskControlDefaults(canonicalName, permission);
  755. agent.config.permission = {
  756. ...permission,
  757. } as unknown as SDKAgentConfig['permission'];
  758. }
  759. /** Apply immutable plugin gates after host projection. */
  760. function applyFinalImmutableGates(agent: AgentDefinition): void {
  761. const permission = normalizePermission(agent.config.permission);
  762. const canonicalName = agent.baseRole ?? agent.name;
  763. const orchestratorOnly =
  764. canonicalName === 'orchestrator'
  765. ? {
  766. wait_for_user: permission.wait_for_user,
  767. marketplace: permission.marketplace,
  768. }
  769. : { wait_for_user: 'deny', marketplace: 'deny' };
  770. agent.config.permission = {
  771. ...permission,
  772. ...orchestratorOnly,
  773. } as unknown as SDKAgentConfig['permission'];
  774. }
  775. function applyFinalPermissions(
  776. agent: AgentDefinition,
  777. configuredSkills: readonly string[] | undefined,
  778. disabledSkills: readonly string[] | undefined,
  779. ): void {
  780. applyDefaultPermissionPolicy(agent, configuredSkills, disabledSkills);
  781. applyDefaultTaskControls(agent);
  782. applyFinalImmutableGates(agent);
  783. }
  784. function uniqueNames(values: readonly string[]): string[] {
  785. return [...new Set(values)];
  786. }
  787. function marketplaceSkillList(
  788. agent: AgentDefinition,
  789. override: AgentOverrideConfig | undefined,
  790. extraSkills: readonly string[] | undefined,
  791. ): readonly string[] | undefined {
  792. if (override?.skills) return override.skills;
  793. if (!extraSkills || extraSkills.length === 0) return undefined;
  794. const roleSkills = agent.baseRole
  795. ? ROLE_DEFINITIONS[agent.baseRole].defaultSkills
  796. : [];
  797. return uniqueNames([...roleSkills, ...extraSkills]);
  798. }
  799. const READONLY_MARKETPLACE_TOOLS = [
  800. 'read',
  801. 'glob',
  802. 'grep',
  803. 'ast_grep_search',
  804. 'webfetch',
  805. 'websearch',
  806. ] as const;
  807. const NON_MARKETPLACE_TOOL_NAMES = [
  808. 'task',
  809. 'acp_run',
  810. 'lsp',
  811. 'list',
  812. 'codesearch',
  813. ] as const;
  814. function restrictMarketplaceNames(
  815. ceiling: readonly string[],
  816. ownerValues: readonly string[] | undefined,
  817. ): string[] {
  818. if (ownerValues === undefined) return [...ceiling];
  819. const denied = new Set(
  820. ownerValues
  821. .filter((value) => value.startsWith('!'))
  822. .map((value) => value.slice(1)),
  823. );
  824. const explicit = new Set(
  825. ownerValues.filter((value) => value !== '*' && !value.startsWith('!')),
  826. );
  827. const allowsAll = ownerValues.includes('*');
  828. return ceiling.filter(
  829. (name) => !denied.has(name) && (allowsAll || explicit.has(name)),
  830. );
  831. }
  832. function marketplaceCapabilityCeilings(
  833. agent: AgentDefinition,
  834. activated: ActivatedMarketplaceAgent,
  835. override: AgentOverrideConfig | undefined,
  836. ): {
  837. tools: readonly string[];
  838. skills: readonly string[];
  839. mcps: readonly string[];
  840. } {
  841. const role = agent.baseRole ? ROLE_DEFINITIONS[agent.baseRole] : undefined;
  842. const baselineTools = role
  843. ? role.permissionPolicy === 'read-only'
  844. ? [...READONLY_MARKETPLACE_TOOLS]
  845. : [...MARKETPLACE_TOOL_NAMES]
  846. : [];
  847. const tools = role
  848. ? uniqueNames([...baselineTools, ...activated.manifest.tools])
  849. : [...activated.manifest.tools];
  850. const skills = restrictMarketplaceNames(
  851. role
  852. ? uniqueNames([...role.defaultSkills, ...activated.requiredSkills])
  853. : activated.requiredSkills,
  854. override?.skills,
  855. );
  856. const mcps = restrictMarketplaceNames(
  857. role
  858. ? uniqueNames([...role.defaultMcps, ...activated.requiredMcps])
  859. : activated.requiredMcps,
  860. override?.mcps,
  861. );
  862. return { tools, skills, mcps };
  863. }
  864. function applyRestrictiveMarketplacePermission(
  865. target: PermissionRecord,
  866. source: unknown,
  867. tools: ReadonlySet<string>,
  868. skills: ReadonlySet<string>,
  869. mcps: ReadonlySet<string>,
  870. applyWildcard = true,
  871. ): void {
  872. const record = normalizePermission(source);
  873. const wildcard = applyWildcard ? record['*'] : undefined;
  874. const applyRestriction = (key: string, value: unknown): void => {
  875. if (value !== 'deny' && value !== 'ask') return;
  876. if (target[key] !== 'deny' || value === 'deny') {
  877. target[key] = value;
  878. }
  879. };
  880. const skillIsGloballyDenied = (): boolean => {
  881. if (target.skill === 'deny') return true;
  882. if (
  883. target.skill &&
  884. typeof target.skill === 'object' &&
  885. !Array.isArray(target.skill)
  886. ) {
  887. return (target.skill as PermissionRecord)['*'] === 'deny';
  888. }
  889. return false;
  890. };
  891. const applySkillRestriction = (
  892. skillNames: ReadonlySet<string>,
  893. value: unknown,
  894. ): void => {
  895. if (value === 'deny') {
  896. target.skill = 'deny';
  897. return;
  898. }
  899. if (value !== 'ask' || skillIsGloballyDenied()) return;
  900. const targetSkills = normalizePermission(target.skill);
  901. for (const skill of skillNames) {
  902. if (targetSkills[skill] !== 'deny') targetSkills[skill] = 'ask';
  903. }
  904. target.skill = targetSkills;
  905. };
  906. // The package projection always has a default-deny wildcard. A restrictive
  907. // owner/host wildcard must therefore be projected onto every package
  908. // surface explicitly; changing only `*` would leave per-tool allows in
  909. // place and v2's last-match-wins evaluator would widen the package again.
  910. for (const tool of tools) applyRestriction(tool, wildcard);
  911. for (const mcp of mcps) applyRestriction(mcp, wildcard);
  912. applySkillRestriction(skills, wildcard);
  913. for (const tool of tools) {
  914. applyRestriction(tool, record[tool]);
  915. }
  916. const skillPermission = record.skill;
  917. applySkillRestriction(skills, skillPermission);
  918. if (
  919. skillPermission &&
  920. typeof skillPermission === 'object' &&
  921. !Array.isArray(skillPermission)
  922. ) {
  923. applySkillRestriction(skills, (skillPermission as PermissionRecord)['*']);
  924. if (!skillIsGloballyDenied()) {
  925. const effectiveTargetSkills = normalizePermission(target.skill);
  926. for (const skill of skills) {
  927. const value = (skillPermission as PermissionRecord)[skill];
  928. if (value === 'deny') effectiveTargetSkills[skill] = 'deny';
  929. else if (
  930. value === 'ask' &&
  931. !skillIsGloballyDenied() &&
  932. effectiveTargetSkills[skill] !== 'deny'
  933. )
  934. effectiveTargetSkills[skill] = 'ask';
  935. }
  936. target.skill = effectiveTargetSkills;
  937. }
  938. }
  939. for (const mcp of mcps) {
  940. applyRestriction(mcp, record[mcp]);
  941. }
  942. }
  943. function projectMarketplacePermission(
  944. packagePermission: unknown,
  945. hostPermission: unknown,
  946. tools: readonly string[],
  947. skills: readonly string[],
  948. mcps: readonly string[],
  949. availableMcpNames: readonly string[],
  950. hostTools: unknown,
  951. ): PermissionRecord {
  952. const result: PermissionRecord = { '*': 'deny' };
  953. const allowedTools = new Set(tools);
  954. for (const tool of MARKETPLACE_TOOL_NAMES) {
  955. result[tool] = allowedTools.has(tool) ? 'allow' : 'deny';
  956. }
  957. for (const tool of NON_MARKETPLACE_TOOL_NAMES) result[tool] = 'deny';
  958. result.skill = Object.fromEntries(skills.map((skill) => [skill, 'allow']));
  959. const allowedMcps = new Set(mcps);
  960. for (const mcp of availableMcpNames) {
  961. const key = `${mcp.replace(/[^a-zA-Z0-9_-]/g, '_')}_*`;
  962. result[key] = allowedMcps.has(mcp) ? 'allow' : 'deny';
  963. }
  964. const toolSet = new Set(tools);
  965. const skillSet = new Set(skills);
  966. const mcpSet = new Set(
  967. mcps.map((mcp) => `${mcp.replace(/[^a-zA-Z0-9_-]/g, '_')}_*`),
  968. );
  969. applyRestrictiveMarketplacePermission(
  970. result,
  971. packagePermission,
  972. toolSet,
  973. skillSet,
  974. mcpSet,
  975. false,
  976. );
  977. applyRestrictiveMarketplacePermission(
  978. result,
  979. hostPermission,
  980. toolSet,
  981. skillSet,
  982. mcpSet,
  983. );
  984. if (hostTools && typeof hostTools === 'object' && !Array.isArray(hostTools)) {
  985. for (const [tool, value] of Object.entries(
  986. hostTools as Record<string, unknown>,
  987. )) {
  988. if (toolSet.has(tool) && value === false) result[tool] = 'deny';
  989. }
  990. }
  991. return result;
  992. }
  993. function applyMarketplaceCapabilities(
  994. agent: AgentDefinition,
  995. ceilings: ReturnType<typeof marketplaceCapabilityCeilings>,
  996. ): void {
  997. const permission: PermissionRecord = { '*': 'deny' };
  998. const allowedTools = new Set(ceilings.tools);
  999. for (const tool of MARKETPLACE_TOOL_NAMES) {
  1000. permission[tool] = allowedTools.has(tool) ? 'allow' : 'deny';
  1001. }
  1002. for (const tool of NON_MARKETPLACE_TOOL_NAMES) permission[tool] = 'deny';
  1003. permission.skill = Object.fromEntries(
  1004. ceilings.skills.map((skill) => [skill, 'allow']),
  1005. );
  1006. for (const mcp of ceilings.mcps) {
  1007. permission[`${mcp.replace(/[^a-zA-Z0-9_-]/g, '_')}_*`] = 'allow';
  1008. }
  1009. applyRestrictiveMarketplacePermission(
  1010. permission,
  1011. agent.config.permission,
  1012. new Set(ceilings.tools),
  1013. new Set(ceilings.skills),
  1014. new Set(
  1015. ceilings.mcps.map((mcp) => `${mcp.replace(/[^a-zA-Z0-9_-]/g, '_')}_*`),
  1016. ),
  1017. );
  1018. agent.config.permission =
  1019. permission as unknown as SDKAgentConfig['permission'];
  1020. }
  1021. function marketplacePackage(
  1022. plan: MarketplaceActivationPlan | undefined,
  1023. agentName: string,
  1024. ): ActivatedMarketplaceAgent | undefined {
  1025. return plan?.agents.find((entry) => entry.manifest.agentName === agentName);
  1026. }
  1027. // Agent Classification
  1028. export type SubagentName = (typeof SUBAGENT_NAMES)[number];
  1029. export function isSubagent(name: string): name is SubagentName {
  1030. return (SUBAGENT_NAMES as readonly string[]).includes(name);
  1031. }
  1032. function buildRoutingEntriesFromAgents(
  1033. agents: readonly AgentDefinition[],
  1034. guidanceByAgent: ReadonlyMap<string, string>,
  1035. marketplace?: MarketplaceActivationPlan,
  1036. ): RoutingEntry[] {
  1037. const entries = agents.flatMap((agent): RoutingEntry[] => {
  1038. if (agent.name === 'council') {
  1039. const runtimeName = agent.displayName
  1040. ? normalizeAgentName(agent.displayName)
  1041. : agent.name;
  1042. return [
  1043. {
  1044. agentName: runtimeName,
  1045. routingBlock: renderCouncilRoutingBlock(runtimeName),
  1046. },
  1047. ];
  1048. }
  1049. if (agent.name === 'councillor' || agent.name.startsWith('councillor-')) {
  1050. return [];
  1051. }
  1052. const marketplaceManifest = marketplace?.agents.find(
  1053. (entry) => entry.manifest.agentName === agent.name,
  1054. )?.manifest;
  1055. const runtimeName = agent.displayName
  1056. ? normalizeAgentName(agent.displayName)
  1057. : agent.name;
  1058. if (agent.baseRole) {
  1059. const roleRoutingBlock = renderRoleRoutingBlock(
  1060. ROLE_DEFINITIONS[agent.baseRole],
  1061. runtimeName,
  1062. );
  1063. const routingBlock = marketplaceManifest
  1064. ? renderMarketplaceAutoDelegationBlock(marketplaceManifest, runtimeName)
  1065. : roleRoutingBlock;
  1066. return [
  1067. {
  1068. agentName: runtimeName,
  1069. routingBlock: guidanceByAgent.has(agent.name)
  1070. ? appendRoutingGuidance(
  1071. marketplaceManifest ? roleRoutingBlock : routingBlock,
  1072. guidanceByAgent.get(agent.name),
  1073. )
  1074. : routingBlock,
  1075. },
  1076. ];
  1077. }
  1078. const genericRoutingBlock = [
  1079. `@${runtimeName}`,
  1080. `- Lane: ${agent.description ?? `Configured agent ${agent.name}`}`,
  1081. ].join('\n');
  1082. const routingBlock = marketplaceManifest
  1083. ? renderMarketplaceAutoDelegationBlock(
  1084. marketplaceManifest,
  1085. runtimeName,
  1086. agent.description,
  1087. )
  1088. : genericRoutingBlock;
  1089. return [
  1090. {
  1091. agentName: runtimeName,
  1092. routingBlock: guidanceByAgent.has(agent.name)
  1093. ? appendRoutingGuidance(
  1094. marketplaceManifest ? genericRoutingBlock : routingBlock,
  1095. guidanceByAgent.get(agent.name),
  1096. )
  1097. : routingBlock,
  1098. },
  1099. ];
  1100. });
  1101. return entries.sort((left, right) =>
  1102. left.agentName < right.agentName
  1103. ? -1
  1104. : left.agentName > right.agentName
  1105. ? 1
  1106. : 0,
  1107. );
  1108. }
  1109. function appendRoutingGuidance(
  1110. routingBlock: string,
  1111. guidance: string | undefined,
  1112. ): string {
  1113. return guidance ? `${routingBlock}\n\n${guidance}` : routingBlock;
  1114. }
  1115. function buildRoutingGuidance(
  1116. runtime: RuntimeConfig,
  1117. agents: readonly AgentDefinition[],
  1118. ): ReadonlyMap<string, string> {
  1119. const displayNameMap = new Map<string, string>();
  1120. for (const agent of agents) {
  1121. if (agent.displayName) {
  1122. displayNameMap.set(agent.name, agent.displayName);
  1123. }
  1124. }
  1125. const guidance = new Map<string, string>();
  1126. const mergedAgents = runtime.agents();
  1127. for (const agent of agents) {
  1128. const acp = runtime.acpAgents[agent.name];
  1129. const customPrompt = getOverrideFromAgents(
  1130. mergedAgents,
  1131. agent.name,
  1132. )?.orchestratorPrompt;
  1133. const prompt = acp
  1134. ? (acp.orchestratorPrompt ??
  1135. [
  1136. `@${agent.displayName ? normalizeAgentName(agent.displayName) : agent.name}`,
  1137. `- Lane: External ACP-connected agent (${acp.command})`,
  1138. `- Role: ${agent.description ?? `External ACP agent ${agent.name}`}`,
  1139. '- **Delegate when:** The user explicitly asks for this ACP-backed agent, or the task matches its role and benefits from software/subscription-specific capabilities outside OpenCode.',
  1140. '- **Do not delegate when:** The built-in specialists can handle the task more directly or local file ownership would conflict with another writer lane.',
  1141. '- **Result handling:** Treat returned output as external-agent work. Reconcile any reported file changes before continuing.',
  1142. ].join('\n'))
  1143. : customPrompt;
  1144. if (prompt) {
  1145. guidance.set(agent.name, rewriteRoutingPrompt(prompt, displayNameMap));
  1146. }
  1147. }
  1148. return guidance;
  1149. }
  1150. function buildRoutingEntriesForResolvedAgents(
  1151. runtime: RuntimeConfig,
  1152. agents: readonly AgentDefinition[],
  1153. marketplace?: MarketplaceActivationPlan,
  1154. ): RoutingEntry[] {
  1155. return buildRoutingEntriesFromAgents(
  1156. agents,
  1157. buildRoutingGuidance(runtime, agents),
  1158. marketplace,
  1159. );
  1160. }
  1161. // Agent Factories
  1162. const SUBAGENT_FACTORIES: Record<SubagentName, AgentFactory> = {
  1163. ...Object.fromEntries(
  1164. SUPPORTED_SPECIALIST_ROLES.map((name) => [
  1165. name,
  1166. (model: string) => ROLE_DEFINITIONS[name].createBaseline(model),
  1167. ]),
  1168. ),
  1169. council: createCouncilAgent,
  1170. councillor: createCouncillorAgent,
  1171. } as Record<SubagentName, AgentFactory>;
  1172. // Public API
  1173. /**
  1174. * Create all agent definitions with optional configuration overrides.
  1175. * Instantiates the orchestrator and all subagents, applying user config and defaults.
  1176. *
  1177. * @param runtime - Runtime configuration interface (plugin layer, preset-aware)
  1178. * @returns Array of agent definitions (orchestrator first, then subagents)
  1179. */
  1180. export function createAgents(
  1181. runtime: RuntimeConfig,
  1182. options?: CreateAgentsOptions,
  1183. ): AgentDefinition[] {
  1184. const mergedAgents = runtime.agents();
  1185. const marketplace = options?.marketplace;
  1186. const disabled = new Set(runtime.disabledAgents);
  1187. if (!runtime.council) {
  1188. disabled.add('council');
  1189. // The bare councillor is only meaningful as part of configured Council Mode.
  1190. disabled.add('councillor');
  1191. }
  1192. const primaryModel = runtime.primaryModel;
  1193. const orchestratorOverride = getOverrideFromAgents(
  1194. mergedAgents,
  1195. 'orchestrator',
  1196. );
  1197. const configuredOrchestratorModel =
  1198. getPrimaryModelFromOverride(orchestratorOverride);
  1199. // Preserve the historical fixer → librarian fallback unless an explicit
  1200. // inheritance policy opts the fixer into a different source.
  1201. const getModelForAgent = (name: SubagentName): string => {
  1202. const role = ROLE_DEFINITIONS[name as keyof typeof ROLE_DEFINITIONS];
  1203. const override = getOverrideFromAgents(mergedAgents, name);
  1204. if (override?.model === undefined) {
  1205. if (override?.inheritModelFrom === 'orchestrator') {
  1206. return configuredOrchestratorModel ?? (role?.defaultModel as string);
  1207. }
  1208. if (override?.inheritModelFrom === 'session') {
  1209. return primaryModel ?? (role?.defaultModel as string);
  1210. }
  1211. }
  1212. if (name === 'fixer' && override?.model === undefined) {
  1213. const librarianOverride = getOverrideFromAgents(
  1214. mergedAgents,
  1215. 'librarian',
  1216. )?.model;
  1217. let librarianModel: string | undefined;
  1218. if (Array.isArray(librarianOverride)) {
  1219. const first = librarianOverride[0];
  1220. librarianModel = typeof first === 'string' ? first : first?.id;
  1221. } else {
  1222. librarianModel = librarianOverride;
  1223. }
  1224. return (
  1225. librarianModel ??
  1226. primaryModel ??
  1227. (ROLE_DEFINITIONS.librarian.defaultModel as string)
  1228. );
  1229. }
  1230. return (
  1231. primaryModel ?? role?.defaultModel ?? (DEFAULT_MODELS[name] as string)
  1232. );
  1233. };
  1234. // 1. Gather all sub-agent definitions with custom prompts
  1235. const protoSubAgents = (
  1236. Object.entries(SUBAGENT_FACTORIES) as [SubagentName, AgentFactory][]
  1237. )
  1238. .filter(([name]) => !disabled.has(name))
  1239. .map(([name, factory]) => {
  1240. // Get base agent definition using the subagent factory with undefined prompts
  1241. const agent = factory(getModelForAgent(name), undefined, undefined);
  1242. if (name in ROLE_DEFINITIONS) {
  1243. agent.description =
  1244. ROLE_DEFINITIONS[name as keyof typeof ROLE_DEFINITIONS].description;
  1245. }
  1246. const customPrompts = loadAgentPrompt(name, {
  1247. preset: runtime.preset,
  1248. projectDirectory: options?.projectDirectory,
  1249. });
  1250. const override = getOverrideFromAgents(mergedAgents, name);
  1251. const inlinePrompt = override?.prompt;
  1252. const defaultPrompt = agent.config.prompt ?? '';
  1253. agent.config.prompt = resolvePrompt(
  1254. name,
  1255. inlinePrompt,
  1256. customPrompts.prompt,
  1257. defaultPrompt,
  1258. customPrompts.appendPrompt,
  1259. [
  1260. TASK_REJECTION_INSTRUCTION,
  1261. ...(name === 'council' ? [COUNCIL_SYNTHESIS_REINFORCEMENT] : []),
  1262. ],
  1263. );
  1264. return agent;
  1265. });
  1266. const marketplaceAgentNames = new Set(
  1267. (marketplace?.agents ?? []).map((entry) => entry.manifest.agentName),
  1268. );
  1269. // 1b. Discover unknown keys in config.agents as custom subagents.
  1270. const customAgentNames = runtime.customAgentNames
  1271. .map(normalizeCustomAgentName)
  1272. .filter((name) => name.length > 0)
  1273. .filter((name) => {
  1274. if (marketplaceAgentNames.has(name)) {
  1275. return false;
  1276. }
  1277. if (!isSafeCustomAgentName(name)) {
  1278. throw new Error(`Unsafe custom agent name '${name}'`);
  1279. }
  1280. if (disabled.has(name)) {
  1281. return false;
  1282. }
  1283. return true;
  1284. });
  1285. const protoCustomAgents = customAgentNames.flatMap((name) => {
  1286. const override = getOverrideFromAgents(mergedAgents, name);
  1287. if (
  1288. !hasCustomAgentModel(override) &&
  1289. override?.inheritModelFrom === undefined
  1290. ) {
  1291. console.warn(
  1292. `[oh-my-opencode] Custom agent '${name}' skipped: 'model' is required`,
  1293. );
  1294. return [];
  1295. }
  1296. const customPrompts = loadAgentPrompt(name, {
  1297. preset: runtime.preset,
  1298. projectDirectory: options?.projectDirectory,
  1299. });
  1300. return [
  1301. buildCustomAgentDefinition(
  1302. name,
  1303. override,
  1304. customPrompts.prompt,
  1305. customPrompts.appendPrompt,
  1306. override.inheritModelFrom === 'orchestrator'
  1307. ? configuredOrchestratorModel
  1308. : primaryModel,
  1309. ),
  1310. ];
  1311. });
  1312. const protoMarketplaceAgents = (marketplace?.agents ?? []).flatMap(
  1313. (activated) => {
  1314. const name = activated.manifest.agentName;
  1315. if (disabled.has(name)) return [];
  1316. const role = activated.manifest.extends
  1317. ? ROLE_DEFINITIONS[activated.manifest.extends.builtin]
  1318. : undefined;
  1319. const override = getOverrideFromAgents(mergedAgents, name);
  1320. const customPrompts = loadAgentPrompt(name, {
  1321. preset: runtime.preset,
  1322. projectDirectory: options?.projectDirectory,
  1323. });
  1324. const policy = activated.manifest.model;
  1325. const model =
  1326. policy.source === 'explicit'
  1327. ? typeof policy.candidates[0] === 'string'
  1328. ? policy.candidates[0]
  1329. : policy.candidates[0]?.id
  1330. : policy.source === 'builtin'
  1331. ? role?.defaultModel
  1332. : policy.source === 'orchestrator'
  1333. ? (configuredOrchestratorModel ?? primaryModel)
  1334. : undefined;
  1335. const agent: AgentDefinition = role
  1336. ? role.createBaseline(
  1337. (model ?? role.defaultModel ?? DEFAULT_MODELS.oracle) as string,
  1338. )
  1339. : {
  1340. name,
  1341. description: activated.manifest.description,
  1342. config: { prompt: activated.manifest.prompt },
  1343. };
  1344. agent.name = name;
  1345. if (role) agent.baseRole = role.id;
  1346. agent.description = activated.manifest.description;
  1347. if (policy.source === 'session') delete agent.config.model;
  1348. if (policy.source === 'explicit') {
  1349. agent._modelArray = policy.candidates.map((candidate) => ({
  1350. ...(typeof candidate === 'string' ? { id: candidate } : candidate),
  1351. }));
  1352. agent.config.model = model;
  1353. if (
  1354. typeof policy.candidates[0] !== 'string' &&
  1355. policy.candidates[0]?.variant
  1356. ) {
  1357. agent.config.variant = policy.candidates[0].variant;
  1358. }
  1359. }
  1360. if (activated.manifest.temperature !== undefined) {
  1361. agent.config.temperature = activated.manifest.temperature;
  1362. }
  1363. if (activated.manifest.color !== undefined) {
  1364. agent.config.color = activated.manifest.color;
  1365. }
  1366. agent.config.prompt = resolvePrompt(
  1367. name,
  1368. override?.prompt,
  1369. customPrompts.prompt,
  1370. role
  1371. ? composePackagePrompt(
  1372. role.basePrompt,
  1373. activated.manifest.prompt,
  1374. activated.manifest.extends?.promptMode ?? 'append',
  1375. )
  1376. : activated.manifest.prompt,
  1377. customPrompts.appendPrompt,
  1378. [TASK_REJECTION_INSTRUCTION],
  1379. );
  1380. return [agent];
  1381. },
  1382. );
  1383. const acpAgentNames = Object.keys(runtime.acpAgents)
  1384. .map(normalizeCustomAgentName)
  1385. .filter((name) => name.length > 0)
  1386. .filter((name) => {
  1387. if (!isSafeAgentAlias(name)) {
  1388. throw new Error(
  1389. `ACP agent name '${name}' must match /^[a-z][a-z0-9_-]*$/i`,
  1390. );
  1391. }
  1392. if (isKnownAgentName(name) || AGENT_ALIASES[name] !== undefined) {
  1393. throw new Error(
  1394. `ACP agent '${name}' conflicts with a built-in agent name or alias`,
  1395. );
  1396. }
  1397. if (customAgentNames.includes(name)) {
  1398. throw new Error(
  1399. `ACP agent '${name}' conflicts with a custom agent of the same name`,
  1400. );
  1401. }
  1402. if (protoMarketplaceAgents.some((agent) => agent.name === name)) {
  1403. throw new Error(
  1404. `ACP agent '${name}' conflicts with a marketplace agent of the same name`,
  1405. );
  1406. }
  1407. return !disabled.has(name);
  1408. });
  1409. const protoAcpAgents = acpAgentNames.map((name) => {
  1410. const acp = runtime.acpAgents[name];
  1411. if (!acp) throw new Error(`ACP agent '${name}' is missing config`);
  1412. return buildAcpAgentDefinition(name, acp, primaryModel);
  1413. });
  1414. // 2. Apply overrides and default permissions to built-in subagents
  1415. const builtInSubAgents = protoSubAgents.map((agent) => {
  1416. const override = getOverrideFromAgents(mergedAgents, agent.name);
  1417. if (override) {
  1418. applyOverrides(agent, override);
  1419. }
  1420. applyModelInheritance(agent, override, configuredOrchestratorModel);
  1421. applyFinalPermissions(
  1422. agent,
  1423. marketplaceSkillList(agent, override, undefined),
  1424. runtime.disabledSkills,
  1425. );
  1426. return agent;
  1427. });
  1428. const customSubAgents = protoCustomAgents.map((agent) => {
  1429. const override = getOverrideFromAgents(mergedAgents, agent.name);
  1430. if (override) {
  1431. applyOverrides(agent, override);
  1432. }
  1433. applyModelInheritance(agent, override, configuredOrchestratorModel);
  1434. applyFinalPermissions(agent, override?.skills, runtime.disabledSkills);
  1435. return agent;
  1436. });
  1437. const marketplaceSubAgents = protoMarketplaceAgents.flatMap((agent) => {
  1438. const override = getOverrideFromAgents(mergedAgents, agent.name);
  1439. if (override) {
  1440. applyOverrides(agent, override);
  1441. }
  1442. if (agent.displayName) {
  1443. const displayName = normalizeAgentName(agent.displayName);
  1444. if (displayName === agent.name) {
  1445. agent.displayName = undefined;
  1446. } else if (!isSafeDisplayName(displayName)) {
  1447. marketplace?.diagnostics.push({
  1448. packageId:
  1449. marketplace.agents.find(
  1450. (entry) => entry.manifest.agentName === agent.name,
  1451. )?.packageId ?? agent.name,
  1452. code: 'invalid-alias',
  1453. message: `display alias '${agent.displayName}' is not a valid agent alias`,
  1454. });
  1455. return [];
  1456. } else if (reservedRuntimeNames(runtime, agent.name).has(displayName)) {
  1457. marketplace?.diagnostics.push({
  1458. packageId:
  1459. marketplace.agents.find(
  1460. (entry) => entry.manifest.agentName === agent.name,
  1461. )?.packageId ?? agent.name,
  1462. code: 'collision',
  1463. message: `display alias '${displayName}' collides with an existing agent name`,
  1464. });
  1465. return [];
  1466. }
  1467. }
  1468. applyModelInheritance(agent, override, configuredOrchestratorModel);
  1469. const marketplaceEntry = marketplacePackage(marketplace, agent.name);
  1470. const marketplaceCeilings = marketplaceEntry
  1471. ? marketplaceCapabilityCeilings(agent, marketplaceEntry, override)
  1472. : undefined;
  1473. if (marketplaceCeilings) {
  1474. applyMarketplaceCapabilities(agent, marketplaceCeilings);
  1475. }
  1476. applyFinalPermissions(
  1477. agent,
  1478. marketplaceCeilings?.skills,
  1479. runtime.disabledSkills,
  1480. );
  1481. return [agent];
  1482. });
  1483. const acpSubAgents = protoAcpAgents.map((agent) => {
  1484. applyFinalPermissions(agent, undefined, runtime.disabledSkills);
  1485. return agent;
  1486. });
  1487. // Build dynamic councillor agents from council config (flatten mode).
  1488. // Each councillor becomes a dispatchable subagent with its own model,
  1489. // so the orchestrator can task() them with native panes at depth 1.
  1490. // Only a *configured* color is inherited: councillor override first, then
  1491. // council override. No default fallback — unconfigured councillors stay
  1492. // colorless so the host TUI palette keeps assigning distinct colors.
  1493. const councillorColor =
  1494. getOverrideFromAgents(mergedAgents, 'councillor')?.color ??
  1495. getOverrideFromAgents(mergedAgents, 'council')?.color;
  1496. const councillorAgents = buildCouncillorAgents(runtime, disabled).map(
  1497. (agent) => {
  1498. if (councillorColor) agent.config.color ??= councillorColor;
  1499. applyFinalPermissions(agent, undefined, runtime.disabledSkills);
  1500. return agent;
  1501. },
  1502. );
  1503. const allSubAgents = [
  1504. ...builtInSubAgents,
  1505. ...customSubAgents,
  1506. ...marketplaceSubAgents,
  1507. ...acpSubAgents,
  1508. ...councillorAgents,
  1509. ];
  1510. for (const agent of [...acpSubAgents, ...councillorAgents]) {
  1511. agent.config.prompt = appendTaskRejectionInstruction(
  1512. agent.config.prompt ?? '',
  1513. );
  1514. }
  1515. const runtimeNameByCanonicalId = Object.fromEntries(
  1516. allSubAgents.map((agent) => [
  1517. agent.name,
  1518. agent.displayName ? normalizeAgentName(agent.displayName) : agent.name,
  1519. ]),
  1520. );
  1521. // 3. Create Orchestrator (with its own overrides and custom prompts)
  1522. // DEFAULT_MODELS.orchestrator is undefined; model is resolved via override or
  1523. // left unset so the runtime chat.message hook can pick it from _modelArray.
  1524. const orchestratorModel =
  1525. orchestratorOverride?.model ?? DEFAULT_MODELS.orchestrator;
  1526. const orchestratorPrompts = loadAgentPrompt('orchestrator', {
  1527. preset: runtime.preset,
  1528. projectDirectory: options?.projectDirectory,
  1529. });
  1530. const orchestrator = createOrchestratorAgent(
  1531. orchestratorModel,
  1532. undefined,
  1533. undefined,
  1534. disabled,
  1535. councillorAgents.length > 0 ? ['council'] : undefined,
  1536. !runtime.disabledTools.includes('wait_for_user'),
  1537. runtime.backgroundJobs.orchestratorWake.enabled,
  1538. buildRoutingEntriesForResolvedAgents(runtime, allSubAgents, marketplace),
  1539. runtimeNameByCanonicalId,
  1540. isMarketplaceToolAvailable(runtime),
  1541. );
  1542. const inlineOrchestratorPrompt = orchestratorOverride?.prompt;
  1543. const defaultOrchestratorPrompt = orchestrator.config.prompt ?? '';
  1544. orchestrator.config.prompt = resolvePrompt(
  1545. 'orchestrator',
  1546. inlineOrchestratorPrompt,
  1547. orchestratorPrompts.prompt,
  1548. defaultOrchestratorPrompt,
  1549. orchestratorPrompts.appendPrompt,
  1550. );
  1551. if (orchestratorOverride) {
  1552. applyOverrides(orchestrator, orchestratorOverride);
  1553. }
  1554. applyModelInheritance(
  1555. orchestrator,
  1556. orchestratorOverride,
  1557. configuredOrchestratorModel,
  1558. );
  1559. applyFinalPermissions(
  1560. orchestrator,
  1561. orchestratorOverride?.skills,
  1562. runtime.disabledSkills,
  1563. );
  1564. // Collect all display names from orchestrator and all subagents
  1565. const displayNameMap = new Map<string, string>();
  1566. if (orchestrator.displayName) {
  1567. displayNameMap.set('orchestrator', orchestrator.displayName);
  1568. }
  1569. for (const agent of allSubAgents) {
  1570. if (agent.displayName) {
  1571. displayNameMap.set(agent.name, agent.displayName);
  1572. }
  1573. }
  1574. // Validate display names
  1575. const usedDisplayNames = new Set<string>();
  1576. for (const [, displayName] of displayNameMap) {
  1577. const normalizedDisplayName = normalizeAgentName(displayName);
  1578. if (!isSafeDisplayName(normalizedDisplayName)) {
  1579. throw new Error(
  1580. `displayName '${normalizedDisplayName}' must match /^[a-z][a-z0-9_-]*$/i`,
  1581. );
  1582. }
  1583. if (usedDisplayNames.has(normalizedDisplayName)) {
  1584. throw new Error(
  1585. `Duplicate displayName '${normalizedDisplayName}' assigned to multiple agents`,
  1586. );
  1587. }
  1588. usedDisplayNames.add(normalizedDisplayName);
  1589. }
  1590. for (const displayName of usedDisplayNames) {
  1591. if (
  1592. (ALL_AGENT_NAMES as readonly string[]).includes(displayName) ||
  1593. customAgentNames.includes(displayName) ||
  1594. marketplaceSubAgents.some((agent) => agent.name === displayName) ||
  1595. acpAgentNames.includes(displayName)
  1596. ) {
  1597. throw new Error(
  1598. `displayName '${displayName}' conflicts with an agent name`,
  1599. );
  1600. }
  1601. }
  1602. let updatedPrompt = orchestrator.config.prompt ?? '';
  1603. // Inject council-dispatch block if dynamic councillors exist (flatten mode)
  1604. if (councillorAgents.length > 0) {
  1605. const dispatchList = councillorAgents
  1606. .map(
  1607. (a: AgentDefinition) =>
  1608. ` - task(subagent_type='${a.name}', description='Councillor ${getCouncillorSeatName(a.name)} on <brief topic>', prompt=<user's question>)`,
  1609. )
  1610. .join('\n');
  1611. updatedPrompt = `${updatedPrompt}\n\n## Council Mode\n\nWhen you need to run a council or the user asks for consensus/multiple opinions, use this procedure INSTEAD of delegating to @council:\n\n1. If the question references an external resource (PR, URL, issue, doc), fetch its content FIRST using your own tools (webfetch/bash/gh), then embed a concise summary in the prompt you send to each councillor — councillors have read-only codebase access only and cannot fetch external content themselves.\n2. Dispatch the user's question (with any fetched context) to each councillor in PARALLEL via task():\n${dispatchList}\n3. Collect ALL councillor responses. If any councillor returns empty or does not respond within 3 minutes, proceed without it — do not wait indefinitely. If a councillor's response is empty, retry that councillor once before continuing.\n4. Call task(subagent_type='council', description='Synthesize council report') with a prompt that includes the original user question AND all councillor responses. For each councillor, label its response with its seat name AND its model (e.g. "alpha (gpt-5.6-luna)"). Format each councillor's seat name and response clearly separated. If a councillor failed or timed out, include that status explicitly (e.g. "beta (gemini-3-pro): FAILED/TIMED OUT") instead of omitting it. Skip only councillors that returned empty after one retry.\n5. Present the council's synthesized report.\n\nThis ensures each councillor runs with its own model and the council agent synthesizes the full multi-model consensus.`;
  1612. }
  1613. orchestrator.config.prompt = updatedPrompt;
  1614. return [orchestrator, ...allSubAgents];
  1615. }
  1616. /**
  1617. * Get agent configurations formatted for the OpenCode SDK.
  1618. * Converts agent definitions to SDK config format and applies classification metadata.
  1619. *
  1620. * @param runtime - Runtime configuration interface (plugin layer, preset-aware)
  1621. * @param options - Optional options including projectDirectory
  1622. * @returns Record mapping agent names to their SDK configurations
  1623. */
  1624. function buildCanonicalAgentConfigs(
  1625. runtime: RuntimeConfig,
  1626. options?: CreateAgentsOptions,
  1627. agents = createAgents(runtime, options),
  1628. ): Record<string, SDKAgentConfig> {
  1629. const applyClassification = (
  1630. name: string,
  1631. sdkConfig: SDKAgentConfig & {
  1632. mcps?: string[];
  1633. displayName?: string;
  1634. hidden?: boolean;
  1635. },
  1636. ): void => {
  1637. if (name === 'council') {
  1638. // Council is callable both as a primary agent (user-facing)
  1639. // and as a subagent (orchestrator can delegate to it)
  1640. sdkConfig.mode = 'all';
  1641. } else if (name === 'councillor' || name.startsWith('councillor-')) {
  1642. // Internal agent - subagent mode, hidden from @ autocomplete.
  1643. // Dynamic councillors are named councillor-<seat> (see council-agents.ts).
  1644. sdkConfig.mode = 'subagent';
  1645. sdkConfig.hidden = true;
  1646. } else if (isSubagent(name)) {
  1647. sdkConfig.mode = 'subagent';
  1648. } else if (name === 'orchestrator') {
  1649. sdkConfig.mode = 'primary';
  1650. } else {
  1651. sdkConfig.mode = 'subagent';
  1652. }
  1653. };
  1654. const resolveAgentMcps = (agent: AgentDefinition): string[] => {
  1655. const marketplaceEntry = marketplacePackage(
  1656. options?.marketplace,
  1657. agent.name,
  1658. );
  1659. if (marketplaceEntry) {
  1660. return [
  1661. ...marketplaceCapabilityCeilings(
  1662. agent,
  1663. marketplaceEntry,
  1664. getOverrideFromAgents(runtime.agents(), agent.name),
  1665. ).mcps,
  1666. ];
  1667. }
  1668. const configured = runtime.agent(agent.name)?.mcps;
  1669. if (configured !== undefined) {
  1670. return getAgentMcpList(agent.name, runtime);
  1671. }
  1672. if (agent.baseRole)
  1673. return [...ROLE_DEFINITIONS[agent.baseRole].defaultMcps];
  1674. return getAgentMcpList(agent.name, runtime);
  1675. };
  1676. const entries: Array<[string, SDKAgentConfig]> = [];
  1677. for (const a of agents) {
  1678. const sdkConfig: SDKAgentConfig & {
  1679. mcps?: string[];
  1680. displayName?: string;
  1681. hidden?: boolean;
  1682. } = {
  1683. ...a.config,
  1684. description: a.description,
  1685. mcps: resolveAgentMcps(a),
  1686. };
  1687. if (a.displayName) {
  1688. sdkConfig.displayName = a.displayName;
  1689. }
  1690. applyClassification(a.name, sdkConfig);
  1691. entries.push([a.name, sdkConfig]);
  1692. }
  1693. return Object.fromEntries(entries);
  1694. }
  1695. function applyMcpPermissionRules(
  1696. permission: unknown,
  1697. agentMcps: readonly string[],
  1698. availableMcpNames: readonly string[],
  1699. ): PermissionRecord {
  1700. const result = normalizePermission(permission);
  1701. if (hasWildcardDeny(result)) return result;
  1702. const denied = new Set(
  1703. agentMcps
  1704. .filter((name) => name.startsWith('!'))
  1705. .map((name) => name.slice(1)),
  1706. );
  1707. const allowsAll = agentMcps.includes('*');
  1708. const allowed = new Set(
  1709. agentMcps.filter((name) => !name.startsWith('!') && name !== '*'),
  1710. );
  1711. for (const mcpName of availableMcpNames) {
  1712. const sanitized = mcpName.replace(/[^a-zA-Z0-9_-]/g, '_');
  1713. const key = `${sanitized}_*`;
  1714. if (!(key in result)) {
  1715. result[key] =
  1716. !denied.has(mcpName) && (allowsAll || allowed.has(mcpName))
  1717. ? 'allow'
  1718. : 'deny';
  1719. }
  1720. }
  1721. return result;
  1722. }
  1723. function hostAgentFor(
  1724. runtime: RuntimeConfig,
  1725. name: string,
  1726. canonicalName: string,
  1727. ): Record<string, unknown> | undefined {
  1728. const entry = runtime.hostAgent(name) ?? runtime.hostAgent(canonicalName);
  1729. return entry ? (entry as Record<string, unknown>) : undefined;
  1730. }
  1731. const SUPPORTED_HOST_AGENT_FIELDS = [
  1732. 'model',
  1733. 'variant',
  1734. 'temperature',
  1735. 'topP',
  1736. 'options',
  1737. 'tools',
  1738. 'steps',
  1739. 'color',
  1740. 'description',
  1741. 'permission',
  1742. ] as const;
  1743. function mergeSupportedHostAgentFields(
  1744. target: Record<string, unknown>,
  1745. host: Record<string, unknown> | undefined,
  1746. ): void {
  1747. if (!host) return;
  1748. for (const field of SUPPORTED_HOST_AGENT_FIELDS) {
  1749. if (field in host) {
  1750. target[field] = cloneOwned(host[field]);
  1751. }
  1752. }
  1753. }
  1754. /** Build every runtime agent surface once. Consumers must use this immutable
  1755. * snapshot rather than reconstructing policy from raw config. Host capture is
  1756. * optional only for the initial catalogue; the plugin replaces that catalogue
  1757. * with the host-finalized snapshot before runtime consumers execute. */
  1758. export function buildResolvedAgentRegistry(
  1759. runtime: RuntimeConfig,
  1760. options?: {
  1761. projectDirectory?: string;
  1762. availableMcpNames?: readonly string[];
  1763. marketplaceStore?: MarketplaceStore;
  1764. preflightSkillNames?: readonly string[];
  1765. preflightMcpNames?: readonly string[];
  1766. extraSkillDirectories?: readonly string[];
  1767. },
  1768. ): ResolvedAgentRegistry {
  1769. const marketplace = resolveMarketplaceActivation({
  1770. runtime,
  1771. store: options?.marketplaceStore,
  1772. projectDirectory: options?.projectDirectory,
  1773. availableSkillNames: options?.preflightSkillNames,
  1774. availableMcpNames: options?.preflightMcpNames,
  1775. extraSkillDirectories: options?.extraSkillDirectories,
  1776. });
  1777. const agents = createAgents(runtime, { ...options, marketplace });
  1778. const routing = buildRoutingEntriesForResolvedAgents(
  1779. runtime,
  1780. agents.slice(1),
  1781. marketplace,
  1782. );
  1783. const rawSdkConfigs = buildCanonicalAgentConfigs(
  1784. runtime,
  1785. { ...options, marketplace },
  1786. agents,
  1787. );
  1788. const sdkConfigs: Record<string, SDKAgentConfig> = {};
  1789. const modelArrays = cloneOwned(runtime.modelArrays);
  1790. for (const activated of marketplace.agents) {
  1791. const name = activated.manifest.agentName;
  1792. const agent = agents.find((entry) => entry.name === name);
  1793. if (agent?._modelArray && agent._modelArray.length > 0) {
  1794. modelArrays[name] = cloneOwned(agent._modelArray);
  1795. continue;
  1796. }
  1797. if (modelArrays[name]) continue;
  1798. const model =
  1799. typeof agent?.config.model === 'string' ? agent.config.model : undefined;
  1800. if (model) {
  1801. modelArrays[name] = [
  1802. {
  1803. id: model,
  1804. ...(typeof agent?.config.variant === 'string'
  1805. ? { variant: agent.config.variant }
  1806. : {}),
  1807. },
  1808. ];
  1809. }
  1810. }
  1811. for (const agent of agents) {
  1812. const name = agent.name;
  1813. const rawConfig = rawSdkConfigs[name];
  1814. if (!rawConfig) continue;
  1815. const runtimeName = agent.displayName
  1816. ? normalizeAgentName(agent.displayName)
  1817. : name;
  1818. const hostConfig = hostAgentFor(runtime, runtimeName, name);
  1819. const hostModel = resolvePrimaryModelValue(hostConfig?.model);
  1820. const configuredModels = modelArrays[name];
  1821. const hostVariant = stringVariant(hostConfig?.variant);
  1822. const hostReplacesModel = Boolean(
  1823. hostModel &&
  1824. configuredModels?.[0]?.id &&
  1825. configuredModels[0].id !== hostModel,
  1826. );
  1827. if (hostReplacesModel && hostModel) {
  1828. const override = getOverrideFromAgents(runtime.agents(), name);
  1829. const ownerVariant = hasExplicitVariantOverride(override)
  1830. ? stringVariant((rawConfig as Record<string, unknown>).variant)
  1831. : undefined;
  1832. modelArrays[name] = [
  1833. {
  1834. id: hostModel,
  1835. ...(hostVariant !== undefined
  1836. ? { variant: hostVariant }
  1837. : ownerVariant !== undefined
  1838. ? { variant: ownerVariant }
  1839. : {}),
  1840. },
  1841. ];
  1842. } else if (
  1843. hostModel &&
  1844. hostVariant !== undefined &&
  1845. configuredModels?.[0]?.id === hostModel
  1846. ) {
  1847. modelArrays[name] = configuredModels.map((entry, index) =>
  1848. index === 0 ? { ...entry, variant: hostVariant } : entry,
  1849. );
  1850. }
  1851. const ownedConfig = cloneOwned(rawConfig) as SDKAgentConfig &
  1852. Record<string, unknown>;
  1853. mergeSupportedHostAgentFields(ownedConfig, hostConfig);
  1854. if (hostReplacesModel && hostVariant === undefined) {
  1855. const override = getOverrideFromAgents(runtime.agents(), name);
  1856. if (!hasExplicitVariantOverride(override)) {
  1857. delete ownedConfig.variant;
  1858. }
  1859. }
  1860. const marketplaceEntry = marketplacePackage(marketplace, name);
  1861. if (marketplaceEntry) {
  1862. const ceilings = marketplaceCapabilityCeilings(
  1863. agent,
  1864. marketplaceEntry,
  1865. getOverrideFromAgents(runtime.agents(), name),
  1866. );
  1867. ownedConfig.permission = projectMarketplacePermission(
  1868. rawConfig.permission,
  1869. hostConfig?.permission,
  1870. ceilings.tools,
  1871. ceilings.skills,
  1872. ceilings.mcps,
  1873. options?.availableMcpNames ?? [],
  1874. hostConfig?.tools,
  1875. ) as SDKAgentConfig['permission'];
  1876. if (
  1877. hostConfig?.tools &&
  1878. typeof hostConfig.tools === 'object' &&
  1879. !Array.isArray(hostConfig.tools)
  1880. ) {
  1881. ownedConfig.tools = Object.fromEntries(
  1882. Object.entries(hostConfig.tools as Record<string, unknown>).filter(
  1883. ([tool, value]) =>
  1884. ceilings.tools.includes(tool) && typeof value === 'boolean',
  1885. ),
  1886. ) as Record<string, boolean>;
  1887. }
  1888. } else {
  1889. ownedConfig.permission = projectPermissionValues(
  1890. name,
  1891. rawConfig.permission,
  1892. hostConfig?.permission,
  1893. ) as SDKAgentConfig['permission'];
  1894. }
  1895. sdkConfigs[name] = ownedConfig;
  1896. }
  1897. // Apply inheritance only while constructing the immutable registry. The
  1898. // config hook must not repeat this resolution against its host projection.
  1899. applyModelInheritanceToConfig(sdkConfigs as Record<string, unknown>, runtime);
  1900. // Inheritance replaces any lower-layer candidate chain. Rebuild the
  1901. // orchestrator-inherited single candidate from the finalized SDK config so
  1902. // the fallback surface cannot retain a package model or variant.
  1903. for (const agent of agents) {
  1904. const override = getOverrideFromAgents(runtime.agents(), agent.name);
  1905. if (override?.inheritModelFrom === 'session') {
  1906. delete modelArrays[agent.name];
  1907. continue;
  1908. }
  1909. if (override?.inheritModelFrom !== 'orchestrator') continue;
  1910. const config = sdkConfigs[agent.name];
  1911. const model = resolvePrimaryModelValue(config?.model);
  1912. if (model === undefined) {
  1913. delete modelArrays[agent.name];
  1914. continue;
  1915. }
  1916. const variant = stringVariant(config?.variant);
  1917. modelArrays[agent.name] = [
  1918. {
  1919. id: model,
  1920. ...(variant !== undefined ? { variant } : {}),
  1921. },
  1922. ];
  1923. }
  1924. const mcpLists: Record<string, readonly string[]> = {};
  1925. const skillPermissions: Record<
  1926. string,
  1927. Readonly<Record<string, 'allow' | 'ask' | 'deny'>>
  1928. > = {};
  1929. const provenance: Record<string, string> = {};
  1930. const runtimeNameByCanonicalId: Record<string, string> = {};
  1931. const canonicalIdByRuntimeName: Record<string, string> = {};
  1932. const packageIdByRuntimeName: Record<string, string> = {};
  1933. const runtimeNameByPackageId: Record<string, string> = {};
  1934. const modelChains = Object.fromEntries(
  1935. Object.entries(modelArrays).map(([name, models]) => [
  1936. name,
  1937. models.map((model) => model.id),
  1938. ]),
  1939. );
  1940. for (const [name, models] of Object.entries(modelArrays)) {
  1941. const entry = sdkConfigs[name] as Record<string, unknown> | undefined;
  1942. if (entry && entry.model === undefined && models.length > 0) {
  1943. entry.model = models[0]?.id;
  1944. if (models[0]?.variant !== undefined) {
  1945. entry.variant = models[0].variant;
  1946. }
  1947. }
  1948. }
  1949. applyOrchestratorModelConfig({
  1950. agents: sdkConfigs as Record<string, unknown>,
  1951. enabled: runtime.stripOrchestratorModel,
  1952. presets: runtime.plugin?.presets,
  1953. configPreset: runtime.preset,
  1954. runtimePreset: runtime.getRuntimePreset(),
  1955. });
  1956. const availableMcpNames = options?.availableMcpNames ?? [];
  1957. for (const agent of agents) {
  1958. const sdkConfig = sdkConfigs[agent.name] as SDKAgentConfig & {
  1959. mcps?: string[];
  1960. };
  1961. mcpLists[agent.name] = sdkConfig.mcps ?? [];
  1962. sdkConfig.permission = applyMcpPermissionRules(
  1963. sdkConfig.permission,
  1964. sdkConfig.mcps ?? [],
  1965. availableMcpNames,
  1966. ) as SDKAgentConfig['permission'];
  1967. const permission = sdkConfig.permission;
  1968. const marketplaceAgent = marketplace.agents.find(
  1969. (entry) => entry.manifest.agentName === agent.name,
  1970. );
  1971. skillPermissions[agent.name] =
  1972. typeof permission === 'object' &&
  1973. permission !== null &&
  1974. typeof permission.skill === 'object' &&
  1975. permission.skill !== null
  1976. ? cloneOwned(
  1977. permission.skill as Record<string, 'allow' | 'ask' | 'deny'>,
  1978. )
  1979. : marketplaceAgent
  1980. ? {}
  1981. : getSkillPermissionsForAgent(
  1982. agent.name,
  1983. runtime.agent(agent.name)?.skills,
  1984. runtime.disabledSkills,
  1985. );
  1986. provenance[agent.name] = marketplaceAgent
  1987. ? `marketplace-agent:${marketplaceAgent.packageId}@${marketplaceAgent.version}`
  1988. : agent.name === 'orchestrator'
  1989. ? 'orchestrator-special'
  1990. : isSubagent(agent.name)
  1991. ? `builtin:${agent.name}`
  1992. : 'configured-agent';
  1993. runtimeNameByCanonicalId[agent.name] = agent.name;
  1994. canonicalIdByRuntimeName[agent.name] = agent.name;
  1995. if (marketplaceAgent) {
  1996. packageIdByRuntimeName[agent.name] = marketplaceAgent.packageId;
  1997. runtimeNameByPackageId[marketplaceAgent.packageId] = agent.name;
  1998. }
  1999. if (agent.displayName) {
  2000. const displayName = normalizeAgentName(agent.displayName);
  2001. if (modelArrays[agent.name]) {
  2002. modelArrays[displayName] = modelArrays[agent.name];
  2003. modelChains[displayName] = modelChains[agent.name];
  2004. }
  2005. mcpLists[displayName] = mcpLists[agent.name];
  2006. skillPermissions[displayName] = skillPermissions[agent.name];
  2007. provenance[displayName] = `alias:${agent.name}`;
  2008. runtimeNameByCanonicalId[agent.name] = displayName;
  2009. canonicalIdByRuntimeName[displayName] = agent.name;
  2010. const packageId = packageIdByRuntimeName[agent.name];
  2011. if (packageId) {
  2012. packageIdByRuntimeName[displayName] = packageId;
  2013. runtimeNameByPackageId[packageId] = displayName;
  2014. }
  2015. }
  2016. }
  2017. for (const [alias, canonical] of Object.entries(AGENT_ALIASES)) {
  2018. if (canonicalIdByRuntimeName[canonical]) {
  2019. canonicalIdByRuntimeName[alias] = canonical;
  2020. }
  2021. }
  2022. for (const agent of agents) {
  2023. if (!agent.displayName || isInternalOnly(agent.name)) continue;
  2024. const displayName = normalizeAgentName(agent.displayName);
  2025. const canonical = sdkConfigs[agent.name];
  2026. if (!canonical) continue;
  2027. const visible = cloneOwned(canonical) as SDKAgentConfig &
  2028. Record<string, unknown>;
  2029. delete visible.hidden;
  2030. sdkConfigs[displayName] = visible;
  2031. sdkConfigs[agent.name] = {
  2032. ...cloneOwned(canonical),
  2033. hidden: true,
  2034. };
  2035. }
  2036. return deepFreeze({
  2037. agents: Object.freeze(agents),
  2038. sdkConfigs: Object.freeze(sdkConfigs),
  2039. modelArrays: Object.freeze(
  2040. Object.fromEntries(
  2041. Object.entries(modelArrays).map(([name, models]) => [
  2042. name,
  2043. Object.freeze(models.map((model) => Object.freeze({ ...model }))),
  2044. ]),
  2045. ),
  2046. ),
  2047. modelChains: Object.freeze(
  2048. Object.fromEntries(
  2049. Object.entries(modelChains).map(([name, chain]) => [
  2050. name,
  2051. Object.freeze(chain),
  2052. ]),
  2053. ),
  2054. ),
  2055. mcpLists: Object.freeze(
  2056. Object.fromEntries(
  2057. Object.entries(mcpLists).map(([name, list]) => [
  2058. name,
  2059. Object.freeze([...list]),
  2060. ]),
  2061. ),
  2062. ),
  2063. skillPermissions: Object.freeze(
  2064. Object.fromEntries(
  2065. Object.entries(skillPermissions).map(([name, permissions]) => [
  2066. name,
  2067. Object.freeze({ ...permissions }),
  2068. ]),
  2069. ),
  2070. ),
  2071. marketplaceLive: Object.freeze(
  2072. buildMarketplaceLive(marketplace, runtimeNameByPackageId).map((entry) =>
  2073. Object.freeze({ ...entry }),
  2074. ),
  2075. ),
  2076. routing: Object.freeze(routing),
  2077. provenance: Object.freeze(provenance),
  2078. runtimeNameByCanonicalId: Object.freeze(runtimeNameByCanonicalId),
  2079. canonicalIdByRuntimeName: Object.freeze(canonicalIdByRuntimeName),
  2080. packageIdByRuntimeName: Object.freeze(packageIdByRuntimeName),
  2081. runtimeNameByPackageId: Object.freeze(runtimeNameByPackageId),
  2082. diagnostics: Object.freeze(
  2083. marketplace.diagnostics.map((entry) => Object.freeze({ ...entry })),
  2084. ),
  2085. });
  2086. }
  2087. export function getAgentConfigs(
  2088. runtime: RuntimeConfig,
  2089. options?: { projectDirectory?: string },
  2090. ): Record<string, SDKAgentConfig> {
  2091. return buildResolvedAgentRegistry(runtime, options).sdkConfigs as Record<
  2092. string,
  2093. SDKAgentConfig
  2094. >;
  2095. }
  2096. /**
  2097. * Get the set of disabled agent names from config, applying protection rules.
  2098. */
  2099. export function getDisabledAgents(config?: PluginConfig): Set<string> {
  2100. const userDisabled = config?.disabled_agents;
  2101. const disabledSource = Array.isArray(userDisabled)
  2102. ? userDisabled
  2103. : DEFAULT_DISABLED_AGENTS;
  2104. const disabled = new Set<string>();
  2105. for (const name of disabledSource) {
  2106. if (!PROTECTED_AGENTS.has(name)) {
  2107. disabled.add(name);
  2108. }
  2109. }
  2110. return disabled;
  2111. }