plugin.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. import type { Ability, LoadedAbility, ExecutorContext, AbilityExecution, Step, AgentStep, SkillStep, ApprovalStep, WorkflowStep } from './types/index.js'
  2. import { loadAbilities, listAbilities } from './loader/index.js'
  3. import { validateAbility, validateInputs } from './validator/index.js'
  4. import { executeAbility, formatExecutionResult } from './executor/index.js'
  5. import { ExecutionManager } from './executor/execution-manager.js'
  6. interface OpencodeClient {
  7. session: {
  8. get: (options: { path: { id: string } }) => Promise<unknown>
  9. list: () => Promise<Array<{ id: string }>>
  10. command: (options: { path: { id: string }; body: { command: string; arguments: string; agent?: string; model?: string } }) => Promise<unknown>
  11. prompt: (options: unknown) => Promise<unknown>
  12. todo: (options: unknown) => Promise<unknown>
  13. }
  14. events: {
  15. publish: (options: { body: { type: string; properties: Record<string, unknown> } }) => Promise<void>
  16. }
  17. }
  18. interface PluginContext {
  19. directory: string
  20. worktree: string
  21. client: OpencodeClient
  22. $: (strings: TemplateStringsArray, ...values: unknown[]) => { text: () => Promise<string> }
  23. }
  24. interface PluginConfig {
  25. abilities?: {
  26. enabled?: boolean
  27. auto_trigger?: boolean
  28. enforcement?: 'strict' | 'normal' | 'loose'
  29. directories?: string[]
  30. }
  31. disabled_abilities?: string[]
  32. }
  33. interface EventInput {
  34. event: {
  35. type: string
  36. properties?: Record<string, unknown>
  37. }
  38. }
  39. interface ChatMessageOutput {
  40. parts: Array<{ type: string; text: string; synthetic?: boolean }>
  41. }
  42. interface ToolExecuteInput {
  43. tool: string
  44. sessionID?: string
  45. callID?: string
  46. }
  47. interface ToolExecuteOutput {
  48. args: Record<string, unknown>
  49. }
  50. interface SessionIdleOutput {
  51. inject?: string
  52. }
  53. // Tools allowed during different step types
  54. const ALLOWED_TOOLS_BY_STEP_TYPE: Record<string, string[]> = {
  55. // Script steps block ALL tools (script runs deterministically)
  56. script: [],
  57. // Agent steps allow agent-invocation tools
  58. agent: ['task', 'background_task', 'call_omo_agent'],
  59. // Skill steps allow skill-loading tools
  60. skill: ['skill', 'slashcommand'],
  61. // Approval steps only allow approval-related actions
  62. approval: ['ability.status', 'ability.cancel'],
  63. // Workflow steps allow nested ability execution
  64. workflow: ['ability.run', 'ability.status'],
  65. }
  66. // Tools always allowed regardless of step (read-only/status tools)
  67. const ALWAYS_ALLOWED_TOOLS = [
  68. 'ability.list',
  69. 'ability.status',
  70. 'ability.cancel',
  71. 'todoread',
  72. 'read',
  73. 'glob',
  74. 'grep',
  75. 'lsp_hover',
  76. 'lsp_diagnostics',
  77. 'lsp_document_symbols',
  78. ]
  79. class AbilitiesPlugin {
  80. private abilities: Map<string, LoadedAbility> = new Map()
  81. private executionManager: ExecutionManager
  82. private config: PluginConfig = {}
  83. private ctx: PluginContext | null = null
  84. private mainSessionID: string | undefined
  85. private initialized = false
  86. private currentAgentId: string | undefined
  87. private agentAbilityBindings: Map<string, string[]> = new Map()
  88. constructor() {
  89. this.executionManager = new ExecutionManager()
  90. }
  91. async initialize(ctx: PluginContext, config: PluginConfig = {}): Promise<void> {
  92. this.ctx = ctx
  93. this.config = config
  94. const directories = config.abilities?.directories || [
  95. `${ctx.directory}/.opencode/abilities`,
  96. ]
  97. for (const dir of directories) {
  98. const loaded = await loadAbilities({ projectDir: dir, includeGlobal: true })
  99. for (const [name, ability] of loaded) {
  100. if (!config.disabled_abilities?.includes(name)) {
  101. this.abilities.set(name, ability)
  102. }
  103. }
  104. }
  105. this.initialized = true
  106. console.log(`[abilities] Loaded ${this.abilities.size} abilities`)
  107. }
  108. private matchesTrigger(text: string, ability: Ability): boolean {
  109. if (!ability.triggers) return false
  110. if (this.config.abilities?.auto_trigger === false) return false
  111. const lowerText = text.toLowerCase()
  112. if (ability.triggers.keywords) {
  113. for (const keyword of ability.triggers.keywords) {
  114. if (lowerText.includes(keyword.toLowerCase())) {
  115. return true
  116. }
  117. }
  118. }
  119. if (ability.triggers.patterns) {
  120. for (const pattern of ability.triggers.patterns) {
  121. try {
  122. if (new RegExp(pattern, 'i').test(text)) {
  123. return true
  124. }
  125. } catch {
  126. continue
  127. }
  128. }
  129. }
  130. return false
  131. }
  132. private detectAbility(text: string): Ability | null {
  133. for (const [, loaded] of this.abilities) {
  134. if (this.matchesTrigger(text, loaded.ability)) {
  135. return loaded.ability
  136. }
  137. }
  138. return null
  139. }
  140. private formatPlan(ability: Ability, inputs: Record<string, unknown>): string {
  141. const lines: string[] = []
  142. lines.push(`## Ability: ${ability.name}`)
  143. lines.push(ability.description)
  144. lines.push('')
  145. if (Object.keys(inputs).length > 0) {
  146. lines.push('### Inputs')
  147. for (const [key, value] of Object.entries(inputs)) {
  148. lines.push(`- ${key}: ${JSON.stringify(value)}`)
  149. }
  150. lines.push('')
  151. }
  152. lines.push('### Steps')
  153. for (let i = 0; i < ability.steps.length; i++) {
  154. const step = ability.steps[i]
  155. const deps = step.needs?.length ? ` (after: ${step.needs.join(', ')})` : ''
  156. lines.push(`${i + 1}. **${step.id}** [${step.type}]${deps}`)
  157. if (step.description) {
  158. lines.push(` ${step.description}`)
  159. }
  160. }
  161. return lines.join('\n')
  162. }
  163. private formatAbilityList(): string {
  164. if (this.abilities.size === 0) {
  165. return 'No abilities loaded.'
  166. }
  167. const list = listAbilities(this.abilities)
  168. const lines: string[] = ['Available abilities:', '']
  169. for (const item of list) {
  170. lines.push(`- **${item.name}** (${item.source})`)
  171. lines.push(` ${item.description}`)
  172. }
  173. return lines.join('\n')
  174. }
  175. private async showToast(title: string, message: string, variant: 'success' | 'error' | 'info' = 'info'): Promise<void> {
  176. if (!this.ctx?.client?.events?.publish) return
  177. try {
  178. await this.ctx.client.events.publish({
  179. body: {
  180. type: 'tui.toast.show',
  181. properties: {
  182. title,
  183. message,
  184. variant,
  185. duration: 5000
  186. }
  187. }
  188. })
  189. } catch {
  190. console.log(`[abilities] Toast: ${title} - ${message}`)
  191. }
  192. }
  193. private createExecutorContext(): ExecutorContext {
  194. const ctx = this.ctx
  195. const self = this
  196. return {
  197. cwd: ctx?.directory || process.cwd(),
  198. env: {},
  199. agents: ctx?.client ? {
  200. async call(options: { agent: string; prompt: string }): Promise<string> {
  201. console.log(`[abilities] Agent step: ${options.agent}`)
  202. console.log(`[abilities] Prompt: ${options.prompt.slice(0, 200)}...`)
  203. return `[Agent "${options.agent}" invocation pending - use Task tool with subagent_type="${options.agent}" and the provided prompt]`
  204. },
  205. async background(options: { agent: string; prompt: string }): Promise<string> {
  206. return this.call(options)
  207. }
  208. } : undefined,
  209. skills: ctx?.client ? {
  210. async load(name: string): Promise<string> {
  211. console.log(`[abilities] Skill step: ${name}`)
  212. return `[Skill "${name}" loaded - follow skill instructions]`
  213. }
  214. } : undefined,
  215. approval: ctx?.client ? {
  216. async request(options: { prompt: string; options?: string[] }): Promise<boolean> {
  217. console.log(`[abilities] Approval required: ${options.prompt}`)
  218. await self.showToast('Approval Required', options.prompt, 'info')
  219. return true
  220. }
  221. } : undefined,
  222. abilities: {
  223. get: (name: string) => {
  224. const loaded = self.abilities.get(name)
  225. return loaded?.ability
  226. },
  227. execute: async (ability, inputs) => {
  228. console.log(`[abilities] Nested workflow: ${ability.name}`)
  229. return executeAbility(ability, inputs, self.createExecutorContext())
  230. }
  231. },
  232. onStepStart: (step) => {
  233. console.log(`[abilities] Step started: ${step.id} (${step.type})`)
  234. },
  235. onStepComplete: (step, result) => {
  236. console.log(`[abilities] Step completed: ${step.id} - ${result.status}`)
  237. },
  238. onStepFail: (step, error) => {
  239. console.log(`[abilities] Step failed: ${step.id} - ${error.message}`)
  240. }
  241. }
  242. }
  243. async handleEvent(input: EventInput): Promise<void> {
  244. const { event } = input
  245. const props = event.properties
  246. if (event.type === 'session.created') {
  247. const sessionInfo = props?.info as { id?: string; parentID?: string } | undefined
  248. if (!sessionInfo?.parentID) {
  249. this.mainSessionID = sessionInfo?.id
  250. }
  251. }
  252. if (event.type === 'session.deleted') {
  253. const sessionInfo = props?.info as { id?: string } | undefined
  254. if (sessionInfo?.id) {
  255. this.executionManager.onSessionDeleted(sessionInfo.id)
  256. if (sessionInfo.id === this.mainSessionID) {
  257. this.mainSessionID = undefined
  258. }
  259. }
  260. }
  261. if (event.type === 'agent.changed') {
  262. const agentInfo = props?.agent as { id?: string; abilities?: string[] } | undefined
  263. if (agentInfo?.id) {
  264. this.setCurrentAgent(agentInfo.id)
  265. if (agentInfo.abilities && agentInfo.abilities.length > 0) {
  266. this.registerAgentAbilities(agentInfo.id, agentInfo.abilities)
  267. }
  268. }
  269. }
  270. }
  271. async handleSessionIdle(): Promise<SessionIdleOutput> {
  272. const execution = this.executionManager.getActive()
  273. if (!execution || execution.status !== 'running') {
  274. return {}
  275. }
  276. const currentStep = execution.currentStep
  277. const enforcement = this.config.abilities?.enforcement || 'strict'
  278. const lines: string[] = [
  279. `## Ability In Progress: ${execution.ability.name}`,
  280. '',
  281. `Progress: ${execution.completedSteps.length}/${execution.ability.steps.length} steps`,
  282. ]
  283. if (currentStep) {
  284. lines.push(`Current step: **${currentStep.id}** [${currentStep.type}]`)
  285. lines.push('')
  286. lines.push(this.getStepInstructions(currentStep))
  287. }
  288. if (enforcement === 'strict') {
  289. lines.push('')
  290. lines.push('**[STRICT]** You cannot exit or work on other tasks until this ability completes.')
  291. lines.push('Use `ability.cancel` if you need to abort.')
  292. } else {
  293. lines.push('')
  294. lines.push('Continue with the current step or use `ability.cancel` to abort.')
  295. }
  296. console.log(`[abilities] Session idle - injecting continuation for: ${execution.ability.name}`)
  297. return {
  298. inject: lines.join('\n')
  299. }
  300. }
  301. async handleChatMessage(
  302. _input: Record<string, unknown>,
  303. output: ChatMessageOutput
  304. ): Promise<void> {
  305. const activeExecution = this.executionManager.getActive()
  306. if (activeExecution && activeExecution.status === 'running') {
  307. const contextText = this.buildAbilityContextInjection(activeExecution)
  308. output.parts.unshift({
  309. type: 'text',
  310. synthetic: true,
  311. text: contextText,
  312. })
  313. return
  314. }
  315. const userText = output.parts
  316. .filter((p) => p.type === 'text' && !p.synthetic)
  317. .map((p) => p.text)
  318. .join(' ')
  319. const detected = this.detectAbility(userText)
  320. if (detected) {
  321. output.parts.unshift({
  322. type: 'text',
  323. synthetic: true,
  324. text: `## Ability Detected: ${detected.name}\n\n${detected.description}\n\nUse \`ability.run\` tool with name="${detected.name}" to execute.`,
  325. })
  326. }
  327. }
  328. private buildAbilityContextInjection(execution: AbilityExecution): string {
  329. const ability = execution.ability
  330. const currentStep = execution.currentStep
  331. const completed = execution.completedSteps.length
  332. const total = ability.steps.length
  333. const progress = `${completed}/${total}`
  334. const lines: string[] = [
  335. `## Active Ability: ${ability.name}`,
  336. '',
  337. `**Progress:** ${progress} steps completed`,
  338. '',
  339. ]
  340. if (currentStep) {
  341. lines.push(`### Current Step: ${currentStep.id} [${currentStep.type}]`)
  342. if (currentStep.description) {
  343. lines.push(currentStep.description)
  344. }
  345. lines.push('')
  346. lines.push(this.getStepInstructions(currentStep))
  347. }
  348. const enforcement = this.config.abilities?.enforcement || 'strict'
  349. if (enforcement === 'strict') {
  350. lines.push('')
  351. lines.push('**[STRICT MODE]** You MUST complete this step before proceeding. Other tools are blocked.')
  352. }
  353. return lines.join('\n')
  354. }
  355. private getStepInstructions(step: Step): string {
  356. switch (step.type) {
  357. case 'script':
  358. return `**Action:** Script is executing. Wait for completion.`
  359. case 'agent': {
  360. const agentStep = step as AgentStep
  361. return `**Action:** Invoke agent "${agentStep.agent}" with the prompt:\n\`\`\`\n${agentStep.prompt}\n\`\`\``
  362. }
  363. case 'skill': {
  364. const skillStep = step as SkillStep
  365. return `**Action:** Load and follow skill "${skillStep.skill}".`
  366. }
  367. case 'approval': {
  368. const approvalStep = step as ApprovalStep
  369. return `**Action:** Request user approval:\n"${approvalStep.prompt}"`
  370. }
  371. case 'workflow': {
  372. const workflowStep = step as WorkflowStep
  373. return `**Action:** Execute nested ability "${workflowStep.workflow}".`
  374. }
  375. default:
  376. return '**Action:** Complete the current step.'
  377. }
  378. }
  379. async handleToolExecuteBefore(
  380. input: ToolExecuteInput,
  381. _output: ToolExecuteOutput
  382. ): Promise<void> {
  383. const execution = this.executionManager.getActive()
  384. if (!execution) return
  385. const currentStep = execution.currentStep
  386. if (!currentStep) return
  387. const enforcement = this.config.abilities?.enforcement || 'strict'
  388. if (enforcement === 'loose') return
  389. const tool = input.tool
  390. if (ALWAYS_ALLOWED_TOOLS.includes(tool)) {
  391. return
  392. }
  393. const allowedTools = ALLOWED_TOOLS_BY_STEP_TYPE[currentStep.type] || []
  394. if (enforcement === 'strict') {
  395. if (!allowedTools.includes(tool)) {
  396. const stepTypeMsg = this.getStepTypeBlockMessage(currentStep)
  397. throw new Error(
  398. `[abilities] Tool '${tool}' blocked during ${currentStep.type} step '${currentStep.id}'. ${stepTypeMsg}`
  399. )
  400. }
  401. } else if (enforcement === 'normal') {
  402. const destructiveTools = ['write', 'edit', 'bash', 'task']
  403. if (destructiveTools.includes(tool) && !allowedTools.includes(tool)) {
  404. throw new Error(
  405. `[abilities] Destructive tool '${tool}' blocked during ${currentStep.type} step '${currentStep.id}'.`
  406. )
  407. }
  408. }
  409. console.log(`[abilities] Tool '${tool}' allowed during ${currentStep.type} step '${currentStep.id}'`)
  410. }
  411. private getStepTypeBlockMessage(step: Step): string {
  412. switch (step.type) {
  413. case 'script':
  414. return 'Script steps run deterministically - wait for completion.'
  415. case 'agent':
  416. return 'Only agent invocation tools (task, background_task) allowed.'
  417. case 'approval':
  418. return 'Waiting for user approval - only status/cancel tools allowed.'
  419. case 'skill':
  420. return 'Only skill-loading tools allowed.'
  421. case 'workflow':
  422. return 'Only ability execution tools allowed.'
  423. default:
  424. return 'Wait for step completion.'
  425. }
  426. }
  427. async handleToolExecuteAfter(
  428. input: ToolExecuteInput,
  429. output: ToolExecuteOutput
  430. ): Promise<void> {
  431. const execution = this.executionManager.getActive()
  432. if (!execution) return
  433. if (input.tool === 'ability.run') {
  434. const result = output.args as { status?: string; ability?: string }
  435. if (result.status === 'completed') {
  436. this.showToast(
  437. 'Ability Complete',
  438. `${result.ability} finished successfully`,
  439. 'success'
  440. )
  441. } else if (result.status === 'failed') {
  442. this.showToast(
  443. 'Ability Failed',
  444. `${result.ability} encountered an error`,
  445. 'error'
  446. )
  447. }
  448. }
  449. }
  450. getTools() {
  451. return {
  452. 'ability.list': {
  453. description: 'List all available abilities',
  454. parameters: {},
  455. execute: async () => {
  456. return this.formatAbilityList()
  457. },
  458. },
  459. 'ability.validate': {
  460. description: 'Validate an ability definition',
  461. parameters: {
  462. name: { type: 'string', description: 'Ability name to validate' },
  463. },
  464. execute: async (params: { name: string }) => {
  465. const loaded = this.abilities.get(params.name)
  466. if (!loaded) {
  467. return `Ability '${params.name}' not found`
  468. }
  469. const result = validateAbility(loaded.ability)
  470. if (result.valid) {
  471. return `Ability '${params.name}' is valid`
  472. }
  473. const errors = result.errors.map((e) => `- ${e.path}: ${e.message}`).join('\n')
  474. return `Ability '${params.name}' has errors:\n${errors}`
  475. },
  476. },
  477. 'ability.run': {
  478. description: `Execute an ability workflow.
  479. Available abilities:
  480. ${Array.from(this.abilities.values())
  481. .map((l) => `- ${l.ability.name}: ${l.ability.description}`)
  482. .join('\n')}
  483. Use: ability.run({ name: "ability-name", inputs: { ... } })`,
  484. parameters: {
  485. name: { type: 'string', description: 'Ability name to run' },
  486. inputs: { type: 'object', description: 'Input values for the ability' },
  487. },
  488. execute: async (
  489. params: { name: string; inputs?: Record<string, unknown> }
  490. ) => {
  491. const loaded = this.abilities.get(params.name)
  492. if (!loaded) {
  493. return { error: `Ability '${params.name}' not found` }
  494. }
  495. const ability = loaded.ability
  496. if (this.currentAgentId && !this.isAbilityAllowedForAgent(params.name, this.currentAgentId)) {
  497. return { error: `Ability '${params.name}' is not allowed for agent '${this.currentAgentId}'` }
  498. }
  499. const inputs = params.inputs || {}
  500. const inputErrors = validateInputs(ability, inputs)
  501. if (inputErrors.length > 0) {
  502. return {
  503. error: 'Input validation failed',
  504. details: inputErrors.map((e) => e.message),
  505. }
  506. }
  507. const plan = this.formatPlan(ability, inputs)
  508. console.log(`[abilities] Executing:\n${plan}`)
  509. const executorCtx = this.createExecutorContext()
  510. try {
  511. const execution = await this.executionManager.execute(
  512. ability,
  513. inputs,
  514. executorCtx
  515. )
  516. return {
  517. status: execution.status,
  518. ability: ability.name,
  519. result: formatExecutionResult(execution),
  520. steps: execution.completedSteps.map((s) => ({
  521. id: s.stepId,
  522. status: s.status,
  523. duration: s.duration,
  524. })),
  525. }
  526. } catch (error) {
  527. return {
  528. status: 'error',
  529. error: error instanceof Error ? error.message : String(error),
  530. }
  531. }
  532. },
  533. },
  534. 'ability.status': {
  535. description: 'Get status of active ability execution',
  536. parameters: {},
  537. execute: async () => {
  538. const execution = this.executionManager.getActive()
  539. if (!execution) {
  540. return { status: 'none', message: 'No active ability execution' }
  541. }
  542. return {
  543. status: execution.status,
  544. ability: execution.ability.name,
  545. currentStep: execution.currentStep?.id,
  546. progress: `${execution.completedSteps.length}/${execution.ability.steps.length}`,
  547. result: formatExecutionResult(execution),
  548. }
  549. },
  550. },
  551. 'ability.cancel': {
  552. description: 'Cancel the active ability execution',
  553. parameters: {},
  554. execute: async () => {
  555. const cancelled = this.executionManager.cancelActive()
  556. if (cancelled) {
  557. return { status: 'cancelled', message: 'Ability execution cancelled' }
  558. }
  559. return { status: 'none', message: 'No active ability to cancel' }
  560. },
  561. },
  562. 'ability.agent': {
  563. description: 'List abilities available to the current agent',
  564. parameters: {
  565. agent: { type: 'string', description: 'Agent ID (optional, defaults to current agent)' },
  566. },
  567. execute: async (params: { agent?: string }) => {
  568. const agentId = params.agent || this.currentAgentId
  569. if (!agentId) {
  570. return {
  571. message: 'No agent specified and no current agent set.',
  572. hint: 'Provide an agent ID or use this tool after an agent is active.'
  573. }
  574. }
  575. const abilities = this.getAgentAbilities(agentId)
  576. if (abilities.length === 0) {
  577. return {
  578. agent: agentId,
  579. abilities: [],
  580. message: `No abilities registered for agent '${agentId}'`
  581. }
  582. }
  583. return {
  584. agent: agentId,
  585. abilities: abilities.map(a => ({
  586. name: a.ability.name,
  587. description: a.ability.description,
  588. triggers: a.ability.triggers?.keywords || [],
  589. exclusive: a.ability.exclusive_agent === agentId,
  590. })),
  591. }
  592. },
  593. },
  594. }
  595. }
  596. cleanup(): void {
  597. this.executionManager.cleanup()
  598. this.abilities.clear()
  599. this.agentAbilityBindings.clear()
  600. this.currentAgentId = undefined
  601. this.initialized = false
  602. }
  603. registerAgentAbilities(agentId: string, abilityNames: string[]): void {
  604. this.agentAbilityBindings.set(agentId, abilityNames)
  605. console.log(`[abilities] Registered ${abilityNames.length} abilities for agent: ${agentId}`)
  606. }
  607. setCurrentAgent(agentId: string | undefined): void {
  608. this.currentAgentId = agentId
  609. if (agentId) {
  610. console.log(`[abilities] Current agent set to: ${agentId}`)
  611. }
  612. }
  613. getAgentAbilities(agentId: string): LoadedAbility[] {
  614. const boundNames = this.agentAbilityBindings.get(agentId) || []
  615. const result: LoadedAbility[] = []
  616. for (const name of boundNames) {
  617. const loaded = this.abilities.get(name)
  618. if (loaded) {
  619. result.push(loaded)
  620. }
  621. }
  622. for (const [, loaded] of this.abilities) {
  623. const ability = loaded.ability
  624. if (ability.compatible_agents?.includes(agentId)) {
  625. if (!result.find(r => r.ability.name === ability.name)) {
  626. result.push(loaded)
  627. }
  628. }
  629. if (ability.exclusive_agent === agentId) {
  630. if (!result.find(r => r.ability.name === ability.name)) {
  631. result.push(loaded)
  632. }
  633. }
  634. }
  635. return result
  636. }
  637. isAbilityAllowedForAgent(abilityName: string, agentId: string): boolean {
  638. const loaded = this.abilities.get(abilityName)
  639. if (!loaded) return false
  640. const ability = loaded.ability
  641. if (ability.exclusive_agent && ability.exclusive_agent !== agentId) {
  642. return false
  643. }
  644. if (ability.compatible_agents && ability.compatible_agents.length > 0) {
  645. return ability.compatible_agents.includes(agentId)
  646. }
  647. const boundNames = this.agentAbilityBindings.get(agentId)
  648. if (boundNames) {
  649. return boundNames.includes(abilityName)
  650. }
  651. return true
  652. }
  653. getCurrentAgent(): string | undefined {
  654. return this.currentAgentId
  655. }
  656. }
  657. const pluginInstance = new AbilitiesPlugin()
  658. export async function createAbilitiesPlugin(ctx: PluginContext, config: PluginConfig = {}) {
  659. await pluginInstance.initialize(ctx, config)
  660. return {
  661. tool: pluginInstance.getTools(),
  662. event: async (input: EventInput) => {
  663. await pluginInstance.handleEvent(input)
  664. },
  665. 'chat.message': async (input: Record<string, unknown>, output: ChatMessageOutput) => {
  666. await pluginInstance.handleChatMessage(input, output)
  667. },
  668. 'tool.execute.before': async (input: ToolExecuteInput, output: ToolExecuteOutput) => {
  669. await pluginInstance.handleToolExecuteBefore(input, output)
  670. },
  671. 'tool.execute.after': async (input: ToolExecuteInput, output: ToolExecuteOutput) => {
  672. await pluginInstance.handleToolExecuteAfter(input, output)
  673. },
  674. 'session.idle': async (): Promise<SessionIdleOutput> => {
  675. return pluginInstance.handleSessionIdle()
  676. },
  677. }
  678. }
  679. export { AbilitiesPlugin }
  680. export default createAbilitiesPlugin