@tenex-chat/backend 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. package/README.md +194 -0
  2. package/dist/backend-wrapper.cjs +3 -0
  3. package/dist/src/index.js +331928 -0
  4. package/package.json +103 -0
  5. package/src/agents/AgentRegistry.ts +418 -0
  6. package/src/agents/AgentStorage.ts +1133 -0
  7. package/src/agents/ConfigResolver.ts +229 -0
  8. package/src/agents/agent-installer.ts +236 -0
  9. package/src/agents/agent-loader.ts +241 -0
  10. package/src/agents/constants.ts +82 -0
  11. package/src/agents/errors.ts +48 -0
  12. package/src/agents/execution/AgentExecutor.ts +561 -0
  13. package/src/agents/execution/ExecutionContextFactory.ts +112 -0
  14. package/src/agents/execution/MessageCompiler.ts +597 -0
  15. package/src/agents/execution/MessageSyncer.ts +100 -0
  16. package/src/agents/execution/PostCompletionChecker.ts +278 -0
  17. package/src/agents/execution/ProgressMonitor.ts +50 -0
  18. package/src/agents/execution/RALResolver.ts +177 -0
  19. package/src/agents/execution/SessionManager.ts +181 -0
  20. package/src/agents/execution/StreamCallbacks.ts +312 -0
  21. package/src/agents/execution/StreamExecutionHandler.ts +579 -0
  22. package/src/agents/execution/StreamSetup.ts +313 -0
  23. package/src/agents/execution/ToolEventHandlers.ts +239 -0
  24. package/src/agents/execution/ToolExecutionTracker.ts +498 -0
  25. package/src/agents/execution/ToolResultUtils.ts +97 -0
  26. package/src/agents/execution/ToolSupervisionWrapper.ts +174 -0
  27. package/src/agents/execution/constants.ts +16 -0
  28. package/src/agents/execution/index.ts +3 -0
  29. package/src/agents/execution/types.ts +96 -0
  30. package/src/agents/execution/utils.ts +26 -0
  31. package/src/agents/index.ts +4 -0
  32. package/src/agents/script-installer.ts +266 -0
  33. package/src/agents/supervision/SupervisorLLMService.ts +253 -0
  34. package/src/agents/supervision/SupervisorOrchestrator.ts +471 -0
  35. package/src/agents/supervision/heuristics/ConsecutiveToolsWithoutTodoHeuristic.ts +73 -0
  36. package/src/agents/supervision/heuristics/DelegationClaimHeuristic.ts +80 -0
  37. package/src/agents/supervision/heuristics/HeuristicRegistry.ts +114 -0
  38. package/src/agents/supervision/heuristics/PendingTodosHeuristic.ts +93 -0
  39. package/src/agents/supervision/heuristics/SilentAgentHeuristic.ts +54 -0
  40. package/src/agents/supervision/heuristics/index.ts +5 -0
  41. package/src/agents/supervision/index.ts +28 -0
  42. package/src/agents/supervision/registerHeuristics.ts +110 -0
  43. package/src/agents/supervision/supervisionHealthCheck.ts +123 -0
  44. package/src/agents/supervision/types.ts +171 -0
  45. package/src/agents/tool-names.ts +46 -0
  46. package/src/agents/tool-normalization.ts +184 -0
  47. package/src/agents/types/index.ts +2 -0
  48. package/src/agents/types/runtime.ts +74 -0
  49. package/src/agents/types/storage.ts +145 -0
  50. package/src/commands/agent/import/index.ts +6 -0
  51. package/src/commands/agent/import/openclaw-distiller.ts +57 -0
  52. package/src/commands/agent/import/openclaw-reader.ts +141 -0
  53. package/src/commands/agent/import/openclaw.ts +154 -0
  54. package/src/commands/agent/index.ts +6 -0
  55. package/src/commands/agent.ts +215 -0
  56. package/src/commands/daemon.ts +198 -0
  57. package/src/commands/doctor.ts +134 -0
  58. package/src/commands/setup/embed.ts +228 -0
  59. package/src/commands/setup/global-system-prompt.ts +223 -0
  60. package/src/commands/setup/image.ts +179 -0
  61. package/src/commands/setup/index.ts +16 -0
  62. package/src/commands/setup/interactive.ts +95 -0
  63. package/src/commands/setup/llm.ts +38 -0
  64. package/src/commands/setup/onboarding.ts +294 -0
  65. package/src/commands/setup/providers.ts +27 -0
  66. package/src/constants.ts +34 -0
  67. package/src/conversations/ConversationDiskReader.ts +148 -0
  68. package/src/conversations/ConversationRegistry.ts +728 -0
  69. package/src/conversations/ConversationStore.ts +868 -0
  70. package/src/conversations/MessageBuilder.ts +866 -0
  71. package/src/conversations/executionTime.ts +62 -0
  72. package/src/conversations/formatters/DelegationXmlFormatter.ts +64 -0
  73. package/src/conversations/formatters/ThreadedConversationFormatter.ts +303 -0
  74. package/src/conversations/formatters/index.ts +9 -0
  75. package/src/conversations/formatters/utils/MessageFormatter.ts +46 -0
  76. package/src/conversations/formatters/utils/TimestampFormatter.ts +56 -0
  77. package/src/conversations/formatters/utils/TreeBuilder.ts +131 -0
  78. package/src/conversations/formatters/utils/TreeRenderer.ts +49 -0
  79. package/src/conversations/index.ts +2 -0
  80. package/src/conversations/persistence/ToolMessageStorage.ts +143 -0
  81. package/src/conversations/search/ConversationIndexManager.ts +393 -0
  82. package/src/conversations/search/QueryParser.ts +114 -0
  83. package/src/conversations/search/SearchEngine.ts +175 -0
  84. package/src/conversations/search/SnippetExtractor.ts +345 -0
  85. package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +484 -0
  86. package/src/conversations/search/embeddings/ConversationIndexingJob.ts +320 -0
  87. package/src/conversations/search/embeddings/IndexingStateManager.ts +338 -0
  88. package/src/conversations/search/embeddings/index.ts +18 -0
  89. package/src/conversations/search/index.ts +49 -0
  90. package/src/conversations/search/types.ts +124 -0
  91. package/src/conversations/services/CategoryManager.ts +160 -0
  92. package/src/conversations/services/ConversationResolver.ts +296 -0
  93. package/src/conversations/services/ConversationSummarizer.ts +234 -0
  94. package/src/conversations/services/MetadataDebounceManager.ts +188 -0
  95. package/src/conversations/services/index.ts +2 -0
  96. package/src/conversations/types.ts +148 -0
  97. package/src/conversations/utils/content-utils.ts +69 -0
  98. package/src/conversations/utils/image-placeholder.ts +281 -0
  99. package/src/conversations/utils/image-url-utils.ts +171 -0
  100. package/src/conversations/utils/multimodal-content.ts +90 -0
  101. package/src/conversations/utils/tool-result-truncator.ts +159 -0
  102. package/src/daemon/Daemon.ts +1883 -0
  103. package/src/daemon/ProjectRuntime.ts +657 -0
  104. package/src/daemon/RestartState.ts +152 -0
  105. package/src/daemon/RuntimeLifecycle.ts +268 -0
  106. package/src/daemon/SubscriptionManager.ts +305 -0
  107. package/src/daemon/UnixSocketTransport.ts +318 -0
  108. package/src/daemon/filters/SubscriptionFilterBuilder.ts +119 -0
  109. package/src/daemon/index.ts +9 -0
  110. package/src/daemon/routing/DaemonRouter.ts +491 -0
  111. package/src/daemon/types.ts +150 -0
  112. package/src/daemon/utils/routing-log.ts +76 -0
  113. package/src/daemon/utils/telemetry.ts +173 -0
  114. package/src/event-handler/agentDeletion.ts +383 -0
  115. package/src/event-handler/index.ts +749 -0
  116. package/src/event-handler/newConversation.ts +165 -0
  117. package/src/event-handler/project.ts +166 -0
  118. package/src/event-handler/reply.ts +18 -0
  119. package/src/events/NDKAgentDefinition.ts +292 -0
  120. package/src/events/NDKAgentLesson.ts +106 -0
  121. package/src/events/NDKEventMetadata.ts +34 -0
  122. package/src/events/NDKMCPTool.ts +60 -0
  123. package/src/events/NDKProjectStatus.ts +384 -0
  124. package/src/events/index.ts +4 -0
  125. package/src/index.ts +126 -0
  126. package/src/lib/agent-home.ts +334 -0
  127. package/src/lib/error-formatter.ts +200 -0
  128. package/src/lib/fs/filesystem.ts +128 -0
  129. package/src/lib/fs/index.ts +1 -0
  130. package/src/lib/json-parser.ts +30 -0
  131. package/src/lib/string.ts +15 -0
  132. package/src/lib/time.ts +74 -0
  133. package/src/llm/ChunkHandler.ts +277 -0
  134. package/src/llm/FinishHandler.ts +250 -0
  135. package/src/llm/LLMConfigEditor.ts +154 -0
  136. package/src/llm/LLMServiceFactory.ts +230 -0
  137. package/src/llm/MessageProcessor.ts +90 -0
  138. package/src/llm/RecordingState.ts +37 -0
  139. package/src/llm/StreamPublisher.ts +40 -0
  140. package/src/llm/TracingUtils.ts +77 -0
  141. package/src/llm/chunk-validators.ts +57 -0
  142. package/src/llm/constants.ts +6 -0
  143. package/src/llm/index.ts +12 -0
  144. package/src/llm/meta/MetaModelResolver.ts +352 -0
  145. package/src/llm/meta/index.ts +11 -0
  146. package/src/llm/middleware/flight-recorder.ts +188 -0
  147. package/src/llm/providers/MockProvider.ts +332 -0
  148. package/src/llm/providers/agent/ClaudeCodeProvider.ts +343 -0
  149. package/src/llm/providers/agent/ClaudeCodeToolsAdapter.ts +203 -0
  150. package/src/llm/providers/agent/CodexAppServerProvider.ts +214 -0
  151. package/src/llm/providers/agent/CodexAppServerToolsAdapter.ts +91 -0
  152. package/src/llm/providers/agent/index.ts +10 -0
  153. package/src/llm/providers/base/AgentProvider.ts +107 -0
  154. package/src/llm/providers/base/BaseProvider.ts +114 -0
  155. package/src/llm/providers/base/StandardProvider.ts +38 -0
  156. package/src/llm/providers/base/index.ts +9 -0
  157. package/src/llm/providers/index.ts +106 -0
  158. package/src/llm/providers/key-manager.ts +238 -0
  159. package/src/llm/providers/ollama-models.ts +105 -0
  160. package/src/llm/providers/openrouter-models.ts +102 -0
  161. package/src/llm/providers/provider-ids.ts +18 -0
  162. package/src/llm/providers/registry/ProviderRegistry.ts +414 -0
  163. package/src/llm/providers/registry/index.ts +7 -0
  164. package/src/llm/providers/standard/AnthropicProvider.ts +71 -0
  165. package/src/llm/providers/standard/OllamaProvider.ts +59 -0
  166. package/src/llm/providers/standard/OpenAIProvider.ts +44 -0
  167. package/src/llm/providers/standard/OpenRouterProvider.ts +103 -0
  168. package/src/llm/providers/standard/index.ts +10 -0
  169. package/src/llm/providers/types.ts +194 -0
  170. package/src/llm/providers/usage-metadata.ts +78 -0
  171. package/src/llm/service.ts +713 -0
  172. package/src/llm/types.ts +167 -0
  173. package/src/llm/utils/ConfigurationManager.ts +650 -0
  174. package/src/llm/utils/ConfigurationTester.ts +229 -0
  175. package/src/llm/utils/ModelSelector.ts +212 -0
  176. package/src/llm/utils/ProviderConfigUI.ts +177 -0
  177. package/src/llm/utils/claudeCodePromptCompiler.ts +141 -0
  178. package/src/llm/utils/codex-models.ts +53 -0
  179. package/src/llm/utils/context-window-cache.ts +30 -0
  180. package/src/llm/utils/models-dev-cache.ts +267 -0
  181. package/src/llm/utils/provider-setup.ts +50 -0
  182. package/src/llm/utils/tool-errors.ts +78 -0
  183. package/src/llm/utils/usage.ts +74 -0
  184. package/src/logging/EventRoutingLogger.ts +205 -0
  185. package/src/nostr/AgentEventDecoder.ts +357 -0
  186. package/src/nostr/AgentEventEncoder.ts +677 -0
  187. package/src/nostr/AgentProfilePublisher.ts +657 -0
  188. package/src/nostr/AgentPublisher.ts +437 -0
  189. package/src/nostr/BlossomService.ts +226 -0
  190. package/src/nostr/InterventionPublisher.ts +132 -0
  191. package/src/nostr/TagExtractor.ts +228 -0
  192. package/src/nostr/collectEvents.ts +83 -0
  193. package/src/nostr/constants.ts +38 -0
  194. package/src/nostr/encryption.ts +26 -0
  195. package/src/nostr/index.ts +31 -0
  196. package/src/nostr/keys.ts +17 -0
  197. package/src/nostr/kinds.ts +37 -0
  198. package/src/nostr/ndkClient.ts +72 -0
  199. package/src/nostr/relays.ts +43 -0
  200. package/src/nostr/trace-context.ts +39 -0
  201. package/src/nostr/types.ts +227 -0
  202. package/src/nostr/utils.ts +84 -0
  203. package/src/prompts/core/FragmentRegistry.ts +30 -0
  204. package/src/prompts/core/PromptBuilder.ts +98 -0
  205. package/src/prompts/core/index.ts +3 -0
  206. package/src/prompts/core/types.ts +13 -0
  207. package/src/prompts/fragments/00-global-system-prompt.ts +44 -0
  208. package/src/prompts/fragments/01-agent-identity.ts +69 -0
  209. package/src/prompts/fragments/02-agent-home-directory.ts +114 -0
  210. package/src/prompts/fragments/03-system-reminders-explanation.ts +14 -0
  211. package/src/prompts/fragments/04-relay-configuration.ts +38 -0
  212. package/src/prompts/fragments/05-delegation-chain.ts +45 -0
  213. package/src/prompts/fragments/06-agent-todos.ts +74 -0
  214. package/src/prompts/fragments/06-todo-usage-guidance.ts +34 -0
  215. package/src/prompts/fragments/07-meta-project-context.ts +234 -0
  216. package/src/prompts/fragments/08-active-conversations.ts +382 -0
  217. package/src/prompts/fragments/09-recent-conversations.ts +153 -0
  218. package/src/prompts/fragments/10-referenced-article.ts +21 -0
  219. package/src/prompts/fragments/11-nudges.ts +134 -0
  220. package/src/prompts/fragments/12-skills.ts +127 -0
  221. package/src/prompts/fragments/13-available-nudges.ts +122 -0
  222. package/src/prompts/fragments/15-available-agents.ts +53 -0
  223. package/src/prompts/fragments/16-stay-in-your-lane.ts +41 -0
  224. package/src/prompts/fragments/17-todo-before-delegation.ts +39 -0
  225. package/src/prompts/fragments/20-voice-mode.ts +62 -0
  226. package/src/prompts/fragments/22-scheduled-tasks.ts +175 -0
  227. package/src/prompts/fragments/24-retrieved-lessons.ts +26 -0
  228. package/src/prompts/fragments/25-rag-instructions.ts +333 -0
  229. package/src/prompts/fragments/26-mcp-resources.ts +237 -0
  230. package/src/prompts/fragments/27-memorized-reports.ts +77 -0
  231. package/src/prompts/fragments/28-agent-directed-monitoring.ts +32 -0
  232. package/src/prompts/fragments/29-rag-collections.ts +50 -0
  233. package/src/prompts/fragments/30-worktree-context.ts +98 -0
  234. package/src/prompts/fragments/31-agents-md-guidance.ts +96 -0
  235. package/src/prompts/fragments/32-process-metrics.ts +72 -0
  236. package/src/prompts/fragments/debug-mode.ts +48 -0
  237. package/src/prompts/fragments/delegation-completion.ts +44 -0
  238. package/src/prompts/fragments/index.ts +91 -0
  239. package/src/prompts/index.ts +21 -0
  240. package/src/prompts/utils/systemPromptBuilder.ts +777 -0
  241. package/src/scripts/migrate-prefix-index.ts +157 -0
  242. package/src/services/AgentDefinitionMonitor.ts +701 -0
  243. package/src/services/ConfigService.ts +723 -0
  244. package/src/services/CooldownRegistry.ts +199 -0
  245. package/src/services/LLMOperationsRegistry.ts +424 -0
  246. package/src/services/OwnerAgentListService.ts +354 -0
  247. package/src/services/PubkeyService.ts +308 -0
  248. package/src/services/agents/AgentMetadataStore.ts +72 -0
  249. package/src/services/agents/AgentResolution.ts +59 -0
  250. package/src/services/agents/EscalationService.ts +281 -0
  251. package/src/services/agents/NDKAgentDiscovery.ts +95 -0
  252. package/src/services/agents/index.ts +7 -0
  253. package/src/services/agents-md/AgentsMdService.ts +184 -0
  254. package/src/services/agents-md/SystemReminderInjector.ts +238 -0
  255. package/src/services/agents-md/index.ts +11 -0
  256. package/src/services/apns/APNsClient.ts +203 -0
  257. package/src/services/apns/APNsService.ts +358 -0
  258. package/src/services/apns/index.ts +11 -0
  259. package/src/services/apns/types.ts +80 -0
  260. package/src/services/compression/CompressionService.ts +445 -0
  261. package/src/services/compression/compression-schema.ts +28 -0
  262. package/src/services/compression/compression-types.ts +74 -0
  263. package/src/services/compression/compression-utils.ts +587 -0
  264. package/src/services/config/types.ts +394 -0
  265. package/src/services/dispatch/AgentDispatchService.ts +937 -0
  266. package/src/services/dispatch/AgentRouter.ts +181 -0
  267. package/src/services/dispatch/DelegationCompletionHandler.ts +232 -0
  268. package/src/services/embedding/EmbeddingProvider.ts +188 -0
  269. package/src/services/embedding/index.ts +5 -0
  270. package/src/services/event-context/EventContextService.ts +108 -0
  271. package/src/services/event-context/index.ts +2 -0
  272. package/src/services/heuristics/ContextBuilder.ts +106 -0
  273. package/src/services/heuristics/HeuristicEngine.ts +200 -0
  274. package/src/services/heuristics/formatters.ts +58 -0
  275. package/src/services/heuristics/index.ts +12 -0
  276. package/src/services/heuristics/rules/index.ts +25 -0
  277. package/src/services/heuristics/rules/todoBeforeDelegation.ts +69 -0
  278. package/src/services/heuristics/rules/todoReminderOnToolUse.ts +63 -0
  279. package/src/services/heuristics/types.ts +144 -0
  280. package/src/services/image/ImageGenerationService.ts +389 -0
  281. package/src/services/image/index.ts +12 -0
  282. package/src/services/intervention/InterventionService.ts +1352 -0
  283. package/src/services/intervention/index.ts +7 -0
  284. package/src/services/mcp/MCPManager.ts +683 -0
  285. package/src/services/mcp/McpNotificationDelivery.ts +139 -0
  286. package/src/services/mcp/McpSubscriptionService.ts +653 -0
  287. package/src/services/mcp/mcpInstaller.ts +130 -0
  288. package/src/services/nip46/Nip46SigningLog.ts +81 -0
  289. package/src/services/nip46/Nip46SigningService.ts +467 -0
  290. package/src/services/nip46/index.ts +4 -0
  291. package/src/services/nudge/NudgeService.ts +224 -0
  292. package/src/services/nudge/NudgeWhitelistService.ts +382 -0
  293. package/src/services/nudge/index.ts +5 -0
  294. package/src/services/nudge/types.ts +83 -0
  295. package/src/services/projects/ProjectContext.ts +672 -0
  296. package/src/services/projects/ProjectContextStore.ts +102 -0
  297. package/src/services/projects/index.ts +6 -0
  298. package/src/services/prompt-compiler/index.ts +15 -0
  299. package/src/services/prompt-compiler/prompt-compiler-service.ts +1143 -0
  300. package/src/services/pubkey-gate/PubkeyGateService.ts +93 -0
  301. package/src/services/pubkey-gate/index.ts +1 -0
  302. package/src/services/rag/EmbeddingProviderFactory.ts +292 -0
  303. package/src/services/rag/LanceDBMaintenanceService.ts +211 -0
  304. package/src/services/rag/RAGDatabaseService.ts +173 -0
  305. package/src/services/rag/RAGOperations.ts +682 -0
  306. package/src/services/rag/RAGService.ts +240 -0
  307. package/src/services/rag/RagSubscriptionService.ts +618 -0
  308. package/src/services/rag/rag-utils.ts +174 -0
  309. package/src/services/ral/PendingDelegationsRegistry.ts +168 -0
  310. package/src/services/ral/RALRegistry.ts +2782 -0
  311. package/src/services/ral/index.ts +4 -0
  312. package/src/services/ral/types.ts +292 -0
  313. package/src/services/reports/LocalReportStore.ts +380 -0
  314. package/src/services/reports/ReportEmbeddingService.ts +430 -0
  315. package/src/services/reports/ReportService.ts +440 -0
  316. package/src/services/reports/articleUtils.ts +52 -0
  317. package/src/services/reports/index.ts +7 -0
  318. package/src/services/scheduling/SchedulerService.ts +1057 -0
  319. package/src/services/scheduling/errors.ts +14 -0
  320. package/src/services/scheduling/index.ts +7 -0
  321. package/src/services/scheduling/utils.ts +77 -0
  322. package/src/services/search/SearchProviderRegistry.ts +78 -0
  323. package/src/services/search/UnifiedSearchService.ts +218 -0
  324. package/src/services/search/index.ts +47 -0
  325. package/src/services/search/projectFilter.ts +22 -0
  326. package/src/services/search/providers/ConversationSearchProvider.ts +48 -0
  327. package/src/services/search/providers/LessonSearchProvider.ts +75 -0
  328. package/src/services/search/providers/ReportSearchProvider.ts +49 -0
  329. package/src/services/search/types.ts +144 -0
  330. package/src/services/skill/SkillService.ts +482 -0
  331. package/src/services/skill/index.ts +2 -0
  332. package/src/services/skill/types.ts +70 -0
  333. package/src/services/status/OperationsStatusService.ts +276 -0
  334. package/src/services/status/ProjectStatusService.ts +522 -0
  335. package/src/services/status/index.ts +11 -0
  336. package/src/services/storage/PrefixKVStore.ts +242 -0
  337. package/src/services/storage/index.ts +1 -0
  338. package/src/services/system-reminder/SystemReminderUtils.ts +96 -0
  339. package/src/services/system-reminder/index.ts +7 -0
  340. package/src/services/trust-pubkeys/TrustPubkeyService.ts +325 -0
  341. package/src/services/trust-pubkeys/index.ts +2 -0
  342. package/src/telemetry/ConversationSpanManager.ts +111 -0
  343. package/src/telemetry/EventLoopMonitor.ts +206 -0
  344. package/src/telemetry/LLMSpanRegistry.ts +20 -0
  345. package/src/telemetry/NostrSpanProcessor.ts +89 -0
  346. package/src/telemetry/ToolCallSpanProcessor.ts +66 -0
  347. package/src/telemetry/diagnostics.ts +27 -0
  348. package/src/telemetry/setup.ts +120 -0
  349. package/src/tools/implementations/agents_discover.ts +121 -0
  350. package/src/tools/implementations/agents_hire.ts +127 -0
  351. package/src/tools/implementations/agents_list.ts +96 -0
  352. package/src/tools/implementations/agents_publish.ts +611 -0
  353. package/src/tools/implementations/agents_read.ts +173 -0
  354. package/src/tools/implementations/agents_write.ts +200 -0
  355. package/src/tools/implementations/ask.ts +411 -0
  356. package/src/tools/implementations/change_model.ts +141 -0
  357. package/src/tools/implementations/conversation_get.ts +661 -0
  358. package/src/tools/implementations/conversation_list.ts +377 -0
  359. package/src/tools/implementations/conversation_search.ts +370 -0
  360. package/src/tools/implementations/delegate.ts +327 -0
  361. package/src/tools/implementations/delegate_crossproject.ts +209 -0
  362. package/src/tools/implementations/delegate_followup.ts +300 -0
  363. package/src/tools/implementations/fs_edit.ts +162 -0
  364. package/src/tools/implementations/fs_glob.ts +182 -0
  365. package/src/tools/implementations/fs_grep.ts +513 -0
  366. package/src/tools/implementations/fs_read.ts +332 -0
  367. package/src/tools/implementations/fs_write.ts +113 -0
  368. package/src/tools/implementations/generate_image.ts +259 -0
  369. package/src/tools/implementations/home_fs.ts +515 -0
  370. package/src/tools/implementations/kill.ts +651 -0
  371. package/src/tools/implementations/learn.ts +166 -0
  372. package/src/tools/implementations/lesson-formatter.ts +38 -0
  373. package/src/tools/implementations/lesson_delete.ts +164 -0
  374. package/src/tools/implementations/lesson_get.ts +105 -0
  375. package/src/tools/implementations/lessons_list.ts +153 -0
  376. package/src/tools/implementations/mcp_resource_read.ts +161 -0
  377. package/src/tools/implementations/mcp_subscribe.ts +158 -0
  378. package/src/tools/implementations/mcp_subscription_stop.ts +85 -0
  379. package/src/tools/implementations/nostr_fetch.ts +149 -0
  380. package/src/tools/implementations/nostr_publish_as_user.ts +353 -0
  381. package/src/tools/implementations/project_list.ts +146 -0
  382. package/src/tools/implementations/rag_add_documents.ts +573 -0
  383. package/src/tools/implementations/rag_create_collection.ts +65 -0
  384. package/src/tools/implementations/rag_delete_collection.ts +68 -0
  385. package/src/tools/implementations/rag_list_collections.ts +77 -0
  386. package/src/tools/implementations/rag_query.ts +107 -0
  387. package/src/tools/implementations/rag_subscription_create.ts +105 -0
  388. package/src/tools/implementations/rag_subscription_delete.ts +80 -0
  389. package/src/tools/implementations/rag_subscription_get.ts +123 -0
  390. package/src/tools/implementations/rag_subscription_list.ts +128 -0
  391. package/src/tools/implementations/report_delete.ts +79 -0
  392. package/src/tools/implementations/report_read.ts +160 -0
  393. package/src/tools/implementations/report_write.ts +278 -0
  394. package/src/tools/implementations/reports_list.ts +77 -0
  395. package/src/tools/implementations/schedule_task.ts +104 -0
  396. package/src/tools/implementations/schedule_task_cancel.ts +62 -0
  397. package/src/tools/implementations/schedule_task_once.ts +128 -0
  398. package/src/tools/implementations/schedule_tasks_list.ts +79 -0
  399. package/src/tools/implementations/search.ts +160 -0
  400. package/src/tools/implementations/shell.ts +553 -0
  401. package/src/tools/implementations/todo.ts +260 -0
  402. package/src/tools/implementations/upload_blob.ts +381 -0
  403. package/src/tools/implementations/web_fetch.ts +153 -0
  404. package/src/tools/implementations/web_search.ts +250 -0
  405. package/src/tools/registry.ts +670 -0
  406. package/src/tools/types.ts +177 -0
  407. package/src/tools/utils.ts +256 -0
  408. package/src/types/event-ids.ts +320 -0
  409. package/src/types/index.ts +46 -0
  410. package/src/utils/agentFetcher.ts +107 -0
  411. package/src/utils/cli-error.ts +29 -0
  412. package/src/utils/conversation-id.ts +27 -0
  413. package/src/utils/conversation-utils.ts +1 -0
  414. package/src/utils/delegation-chain.ts +357 -0
  415. package/src/utils/error-handler.ts +42 -0
  416. package/src/utils/git/gitignore.ts +69 -0
  417. package/src/utils/git/index.ts +2 -0
  418. package/src/utils/git/initializeGitRepo.ts +204 -0
  419. package/src/utils/git/worktree.ts +260 -0
  420. package/src/utils/lessonFormatter.ts +70 -0
  421. package/src/utils/lessonTrust.ts +24 -0
  422. package/src/utils/lockfile.ts +123 -0
  423. package/src/utils/logger.ts +149 -0
  424. package/src/utils/nostr-entity-parser.ts +365 -0
  425. package/src/utils/process.ts +49 -0
  426. package/src/wrapper.ts +262 -0
  427. package/tsconfig.json +41 -0
@@ -0,0 +1,1133 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import type { StoredAgentData, ProjectScopedConfig, AgentDefaultConfig, AgentProjectConfig } from "@/agents/types";
4
+ import type { MCPServerConfig } from "@/llm/providers/types";
5
+ import {
6
+ resolveEffectiveConfig,
7
+ deduplicateProjectConfig,
8
+ type ResolvedAgentConfig,
9
+ } from "@/agents/ConfigResolver";
10
+ import { ensureDirectory, fileExists } from "@/lib/fs";
11
+ import { config } from "@/services/ConfigService";
12
+ import { logger } from "@/utils/logger";
13
+ import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
14
+ import { trace } from "@opentelemetry/api";
15
+
16
+ /**
17
+ * Options for `updateDefaultConfig()`.
18
+ */
19
+ export interface UpdateDefaultConfigOptions {
20
+ /** If true, clears all projectOverrides (default: false) */
21
+ clearProjectOverrides?: boolean;
22
+ }
23
+
24
+ /**
25
+ * Agent data stored in ~/.tenex/agents/<pubkey>.json
26
+ */
27
+ export interface StoredAgent extends StoredAgentData {
28
+ eventId?: string;
29
+ nsec: string;
30
+ slug: string;
31
+ /**
32
+ * Agent lifecycle status.
33
+ * - 'active': Agent is assigned to at least one project (default behavior)
34
+ * - 'inactive': Agent has been removed from all projects but identity preserved
35
+ *
36
+ * ## Identity Preservation Policy
37
+ * Agent files are NEVER deleted when removed from projects. Instead, they become
38
+ * 'inactive' and retain their pubkey/nsec for potential reactivation.
39
+ */
40
+ status?: "active" | "inactive";
41
+ /**
42
+ * @deprecated Use pmOverrides instead. Kept for backward compatibility.
43
+ * Will be migrated to pmOverrides on first save.
44
+ */
45
+ isPMOverride?: boolean;
46
+ /**
47
+ * Project-scoped PM override flags.
48
+ * Key is project dTag, value is true if this agent is PM for that project.
49
+ * Only one agent per project should have this set to true.
50
+ */
51
+ pmOverrides?: Record<string, boolean>;
52
+ /**
53
+ * Global PM designation flag.
54
+ * When true, this agent is designated as PM for ALL projects where it exists.
55
+ * Set via kind 24020 TenexAgentConfigUpdate event with ["pm"] tag (without a-tag).
56
+ * Takes precedence over pmOverrides and project tag designations.
57
+ */
58
+ isPM?: boolean;
59
+ /**
60
+ * Project-scoped configuration overrides.
61
+ * Key is project dTag, value contains project-specific settings.
62
+ * Set via kind 24020 TenexAgentConfigUpdate events WITH an a-tag specifying the project.
63
+ *
64
+ * ## Priority (highest to lowest)
65
+ * 1. projectConfigs[projectDTag].* (project-scoped from kind 24020 with a-tag)
66
+ * 2. Global fields (llmConfig, tools, isPM) (global from kind 24020 without a-tag)
67
+ * 3. pmOverrides[projectDTag] (legacy, for backward compatibility)
68
+ * 4. Project tag designations (from kind 31933)
69
+ */
70
+ projectConfigs?: Record<string, ProjectScopedConfig>;
71
+ }
72
+
73
+ /**
74
+ * Factory function to create a StoredAgent object.
75
+ *
76
+ * Ensures consistent structure and defaults across the codebase.
77
+ * Used by both agent-installer (Nostr agents) and agents_write (local agents).
78
+ *
79
+ * ## Why this exists
80
+ * Before: StoredAgent objects were manually constructed in 2 places with slight differences
81
+ * After: Single factory ensures consistency and makes schema changes easier
82
+ *
83
+ * @param config - Agent configuration
84
+ * @returns StoredAgent ready for saving to disk
85
+ *
86
+ * @example
87
+ * const agent = createStoredAgent({
88
+ * nsec: signer.nsec,
89
+ * slug: 'my-agent',
90
+ * name: 'My Agent',
91
+ * role: 'assistant',
92
+ * tools: ['fs_read', 'shell'],
93
+ * eventId: 'nostr_event_id',
94
+ * });
95
+ * await agentStorage.saveAgent(agent);
96
+ * await agentStorage.addAgentToProject(pubkey, 'project-dtag');
97
+ */
98
+ export function createStoredAgent(config: {
99
+ nsec: string;
100
+ slug: string;
101
+ name: string;
102
+ role: string;
103
+ description?: string | null;
104
+ instructions?: string | null;
105
+ useCriteria?: string | null;
106
+ eventId?: string;
107
+ mcpServers?: Record<string, MCPServerConfig>;
108
+ pmOverrides?: Record<string, boolean>;
109
+ defaultConfig?: AgentDefaultConfig;
110
+ projectOverrides?: Record<string, AgentProjectConfig>;
111
+ definitionDTag?: string;
112
+ definitionAuthor?: string;
113
+ definitionCreatedAt?: number;
114
+ }): StoredAgent {
115
+ return {
116
+ eventId: config.eventId,
117
+ nsec: config.nsec,
118
+ slug: config.slug,
119
+ name: config.name,
120
+ role: config.role,
121
+ description: config.description ?? undefined,
122
+ instructions: config.instructions ?? undefined,
123
+ useCriteria: config.useCriteria ?? undefined,
124
+ status: "active",
125
+ mcpServers: config.mcpServers,
126
+ pmOverrides: config.pmOverrides,
127
+ default: config.defaultConfig,
128
+ projectOverrides: config.projectOverrides,
129
+ definitionDTag: config.definitionDTag,
130
+ definitionAuthor: config.definitionAuthor,
131
+ definitionCreatedAt: config.definitionCreatedAt,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Check if an agent is active.
137
+ *
138
+ * An agent is considered active if:
139
+ * - It has `status: 'active'` explicitly set, OR
140
+ * - It has no status field (treated as active by default)
141
+ *
142
+ * This helper centralizes the logic for determining agent activity status.
143
+ */
144
+ export function isAgentActive(agent: StoredAgent): boolean {
145
+ if (agent.status === "inactive") {
146
+ return false;
147
+ }
148
+ return true;
149
+ }
150
+
151
+ /**
152
+ * Slug index entry tracking which projects use this slug
153
+ */
154
+ interface SlugEntry {
155
+ pubkey: string;
156
+ projectIds: string[];
157
+ }
158
+
159
+ /**
160
+ * Index structure for fast lookups
161
+ */
162
+ interface AgentIndex {
163
+ bySlug: Record<string, SlugEntry>; // slug -> { pubkey, projectIds[] }
164
+ byEventId: Record<string, string>; // eventId -> pubkey
165
+ byProject: Record<string, string[]>; // projectDTag -> pubkey[]
166
+ }
167
+
168
+ /**
169
+ * AgentStorage - Persistent storage layer for agent data
170
+ *
171
+ * ## Responsibility
172
+ * Manages agent data persistence in ~/.tenex/agents/
173
+ * - One JSON file per agent: <pubkey>.json (contains all data including private key)
174
+ * - Fast lookups via index.json (slug → pubkey, eventId → pubkey, project → pubkeys)
175
+ * - Project associations (which agents belong to which projects)
176
+ *
177
+ * ## Architecture
178
+ * - **AgentStorage** (this): Handles ALL persistence operations
179
+ * - **AgentRegistry**: Handles in-memory runtime instances (separate)
180
+ * - **agent-loader**: Orchestrates loading from storage → registry (separate)
181
+ *
182
+ * ## Storage Structure
183
+ * ```
184
+ * ~/.tenex/agents/
185
+ * ├── index.json # Fast lookup index
186
+ * ├── <pubkey1>.json # Agent data + private key
187
+ * └── <pubkey2>.json # Agent data + private key
188
+ * ```
189
+ *
190
+ * ## Usage Pattern
191
+ * 1. **Read operations**: Use load/get methods
192
+ * 2. **Write operations**: Use save/update methods
193
+ * 3. **After updates**: Call AgentRegistry.reloadAgent() to refresh in-memory instances
194
+ *
195
+ * ## Separation of Concerns
196
+ * - Storage (this class): Disk persistence only
197
+ * - Registry (AgentRegistry): Runtime instances only
198
+ * - Updates: storage.update() → registry.reload()
199
+ *
200
+ * @example
201
+ * // Load agent from disk
202
+ * const agent = await agentStorage.loadAgent(pubkey);
203
+ *
204
+ * // Update default configuration
205
+ * await agentStorage.updateDefaultConfig(pubkey, { model: 'anthropic:claude-opus-4' });
206
+ *
207
+ * // Refresh in-memory instance
208
+ * await agentRegistry.reloadAgent(pubkey);
209
+ *
210
+ * @see AgentRegistry for in-memory runtime management
211
+ * @see agent-loader for loading orchestration
212
+ */
213
+ export class AgentStorage {
214
+ private agentsDir: string;
215
+ private indexPath: string;
216
+ private index: AgentIndex | null = null;
217
+
218
+ constructor() {
219
+ this.agentsDir = config.getConfigPath("agents");
220
+ this.indexPath = path.join(this.agentsDir, "index.json");
221
+ }
222
+
223
+ /**
224
+ * Ensure storage directory exists and load index
225
+ */
226
+ async initialize(): Promise<void> {
227
+ await ensureDirectory(this.agentsDir);
228
+ await this.loadIndex();
229
+ }
230
+
231
+ /**
232
+ * Load the index file or create empty index if it doesn't exist
233
+ */
234
+ private async loadIndex(): Promise<void> {
235
+ if (await fileExists(this.indexPath)) {
236
+ try {
237
+ const content = await fs.readFile(this.indexPath, "utf-8");
238
+ const rawIndex = JSON.parse(content);
239
+
240
+ // Detect old format: bySlug is Record<string, string> instead of Record<string, SlugEntry>
241
+ const needsMigration = rawIndex.bySlug &&
242
+ Object.values(rawIndex.bySlug).some((val: any) => typeof val === "string");
243
+
244
+ if (needsMigration) {
245
+ logger.info("Migrating agent index from old format to multi-project slug structure");
246
+ this.index = this.migrateIndexFormat(rawIndex);
247
+
248
+ // Verify byProject is populated after migration
249
+ const hasValidByProject = this.index.byProject &&
250
+ Object.keys(this.index.byProject).length > 0;
251
+
252
+ if (!hasValidByProject) {
253
+ logger.warn("Migration produced empty byProject index, rebuilding from agent files");
254
+ await this.rebuildIndex();
255
+ } else {
256
+ await this.saveIndex();
257
+ }
258
+
259
+ logger.info("Agent index migration complete", {
260
+ slugCount: Object.keys(this.index.bySlug).length,
261
+ projectCount: Object.keys(this.index.byProject).length,
262
+ });
263
+ } else {
264
+ this.index = rawIndex;
265
+ }
266
+ } catch (error) {
267
+ logger.error("Failed to load agent index, creating new one", { error });
268
+ this.index = { bySlug: {}, byEventId: {}, byProject: {} };
269
+ }
270
+ } else {
271
+ this.index = { bySlug: {}, byEventId: {}, byProject: {} };
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Save the index file
277
+ */
278
+ private async saveIndex(): Promise<void> {
279
+ if (!this.index) return;
280
+ await fs.writeFile(this.indexPath, JSON.stringify(this.index, null, 2));
281
+ }
282
+
283
+ /**
284
+ * Migrate index from old flat format to new SlugEntry structure.
285
+ * Old format: bySlug[slug] = pubkey
286
+ * New format: bySlug[slug] = { pubkey, projectIds: [] }
287
+ *
288
+ * This function returns a new AgentIndex object and does NOT mutate the input.
289
+ */
290
+ private migrateIndexFormat(oldIndex: any): AgentIndex {
291
+ const newIndex: AgentIndex = {
292
+ bySlug: {},
293
+ byEventId: oldIndex.byEventId || {},
294
+ byProject: oldIndex.byProject || {},
295
+ };
296
+
297
+ // Build reverse lookup: pubkey -> projectIds[]
298
+ const pubkeyToProjects: Record<string, string[]> = {};
299
+ for (const [projectDTag, pubkeys] of Object.entries(oldIndex.byProject || {})) {
300
+ for (const pubkey of (pubkeys as string[])) {
301
+ if (!pubkeyToProjects[pubkey]) {
302
+ pubkeyToProjects[pubkey] = [];
303
+ }
304
+ pubkeyToProjects[pubkey].push(projectDTag);
305
+ }
306
+ }
307
+
308
+ // Convert slug index
309
+ for (const [slug, pubkey] of Object.entries(oldIndex.bySlug || {})) {
310
+ if (typeof pubkey === "string") {
311
+ newIndex.bySlug[slug] = {
312
+ pubkey,
313
+ projectIds: pubkeyToProjects[pubkey] || [],
314
+ };
315
+ } else {
316
+ // Already in new format
317
+ newIndex.bySlug[slug] = pubkey as SlugEntry;
318
+ }
319
+ }
320
+
321
+ return newIndex;
322
+ }
323
+
324
+ /**
325
+ * Rebuild index by scanning all agent files.
326
+ *
327
+ * Rebuilds bySlug and byEventId from agent files.
328
+ * byProject cannot be rebuilt from agent files (project associations live only in the index),
329
+ * so it is left empty.
330
+ *
331
+ * ## Slug Index Priority
332
+ * Active agents take precedence over inactive agents for slug ownership.
333
+ * If multiple agents share a slug, the active one becomes canonical.
334
+ * If all agents with a slug are inactive, one is chosen arbitrarily.
335
+ */
336
+ async rebuildIndex(): Promise<void> {
337
+ const index: AgentIndex = { bySlug: {}, byEventId: {}, byProject: {} };
338
+ // Track which slugs are owned by active agents
339
+ const activeSlugOwners = new Set<string>();
340
+
341
+ const files = await fs.readdir(this.agentsDir);
342
+ for (const file of files) {
343
+ if (!file.endsWith(".json") || file === "index.json") continue;
344
+
345
+ const pubkey = file.slice(0, -5); // Remove .json
346
+ try {
347
+ const agent = await this.loadAgent(pubkey);
348
+ if (!agent) continue;
349
+
350
+ const active = isAgentActive(agent);
351
+
352
+ // Update slug index - active agents take precedence
353
+ const existingEntry = index.bySlug[agent.slug];
354
+ if (existingEntry) {
355
+ if (existingEntry.pubkey !== pubkey) {
356
+ // Different agent with same slug - active takes precedence
357
+ if (active && !activeSlugOwners.has(agent.slug)) {
358
+ index.bySlug[agent.slug] = { pubkey, projectIds: [] };
359
+ activeSlugOwners.add(agent.slug);
360
+ }
361
+ }
362
+ // Same agent already indexed - no merge needed (no projects in agent files)
363
+ } else {
364
+ index.bySlug[agent.slug] = { pubkey, projectIds: [] };
365
+ if (active) {
366
+ activeSlugOwners.add(agent.slug);
367
+ }
368
+ }
369
+
370
+ // Update eventId index
371
+ if (agent.eventId) {
372
+ index.byEventId[agent.eventId] = pubkey;
373
+ }
374
+ } catch (error) {
375
+ logger.warn(`Failed to index agent file ${file}`, { error });
376
+ }
377
+ }
378
+
379
+ this.index = index;
380
+ await this.saveIndex();
381
+ logger.info("Rebuilt agent index", {
382
+ agents: Object.keys(index.bySlug).length,
383
+ });
384
+ }
385
+
386
+ /**
387
+ * Find an alternative active agent that can own a slug.
388
+ * Used when the current slug owner becomes inactive.
389
+ *
390
+ * @param slug - The slug to find an alternative owner for
391
+ * @param excludePubkey - Pubkey to exclude from consideration (the current/transitioning owner)
392
+ * @returns The pubkey of an active agent with this slug, or null if none exists
393
+ */
394
+ private async findAlternativeSlugOwner(slug: string, excludePubkey: string): Promise<string | null> {
395
+ const files = await fs.readdir(this.agentsDir);
396
+
397
+ for (const file of files) {
398
+ if (!file.endsWith(".json") || file === "index.json") continue;
399
+
400
+ const pubkey = file.slice(0, -5); // Remove .json
401
+ if (pubkey === excludePubkey) continue;
402
+
403
+ try {
404
+ const agent = await this.loadAgent(pubkey);
405
+ if (!agent) continue;
406
+
407
+ if (agent.slug === slug && isAgentActive(agent)) {
408
+ return pubkey;
409
+ }
410
+ } catch {
411
+ // Skip agents that fail to load
412
+ }
413
+ }
414
+
415
+ return null;
416
+ }
417
+
418
+ /**
419
+ * Load an agent by pubkey
420
+ */
421
+ async loadAgent(pubkey: string): Promise<StoredAgent | null> {
422
+ const filePath = path.join(this.agentsDir, `${pubkey}.json`);
423
+
424
+ if (!(await fileExists(filePath))) {
425
+ return null;
426
+ }
427
+
428
+ try {
429
+ const content = await fs.readFile(filePath, "utf-8");
430
+ const agent: StoredAgent = JSON.parse(content);
431
+ return agent;
432
+ } catch (error) {
433
+ logger.error(`Failed to load agent ${pubkey}`, { error });
434
+ return null;
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Clean up old agents with the same slug in overlapping projects.
440
+ * When a new agent is saved with a slug that already exists,
441
+ * remove the old agent from projects that overlap with the new agent.
442
+ */
443
+ private async cleanupDuplicateSlugs(
444
+ slug: string,
445
+ newPubkey: string,
446
+ newProjects: string[]
447
+ ): Promise<void> {
448
+ if (!this.index) await this.loadIndex();
449
+ if (!this.index) return;
450
+
451
+ const existingEntry = this.index.bySlug[slug];
452
+ if (!existingEntry || existingEntry.pubkey === newPubkey) return;
453
+
454
+ // Find overlapping projects using the index (source of truth for project associations)
455
+ const existingProjects = this.getIndexProjectsForAgent(existingEntry.pubkey);
456
+ const overlappingProjects = existingProjects.filter((p) => newProjects.includes(p));
457
+ if (overlappingProjects.length === 0) return;
458
+
459
+ logger.info(`Cleaning up duplicate slug '${slug}'`, {
460
+ oldPubkey: existingEntry.pubkey.substring(0, 8),
461
+ newPubkey: newPubkey.substring(0, 8),
462
+ overlappingProjects,
463
+ });
464
+
465
+ // Emit telemetry for agent eviction
466
+ trace.getActiveSpan()?.addEvent("agent.slug_conflict_eviction", {
467
+ "conflict.slug": slug,
468
+ "conflict.evicted_pubkey": existingEntry.pubkey,
469
+ "conflict.incoming_pubkey": newPubkey,
470
+ "conflict.overlapping_projects": overlappingProjects.join(", "),
471
+ "conflict.overlapping_count": overlappingProjects.length,
472
+ });
473
+
474
+ // Remove old agent from overlapping projects
475
+ for (const projectDTag of overlappingProjects) {
476
+ await this.removeAgentFromProject(existingEntry.pubkey, projectDTag);
477
+
478
+ // Update slug entry's project list
479
+ existingEntry.projectIds = (existingEntry.projectIds ?? []).filter(p => p !== projectDTag);
480
+
481
+ // Emit per-project eviction event for granular tracking
482
+ trace.getActiveSpan()?.addEvent("agent.evicted_from_project", {
483
+ "eviction.slug": slug,
484
+ "eviction.pubkey": existingEntry.pubkey,
485
+ "eviction.project": projectDTag,
486
+ "eviction.reason": "slug_conflict",
487
+ });
488
+ }
489
+
490
+ // If old agent has no projects left, remove the slug entry entirely
491
+ if ((existingEntry.projectIds ?? []).length === 0) {
492
+ delete this.index.bySlug[slug];
493
+ logger.info(`Removed slug entry for '${slug}' - no projects remaining`);
494
+
495
+ trace.getActiveSpan()?.addEvent("agent.slug_entry_deleted", {
496
+ "slug.deleted": slug,
497
+ "slug.pubkey": existingEntry.pubkey,
498
+ "slug.reason": "no_projects_remaining",
499
+ });
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Save an agent and update index
505
+ */
506
+ async saveAgent(agent: StoredAgent): Promise<void> {
507
+ // Get pubkey from nsec
508
+ const signer = new NDKPrivateKeySigner(agent.nsec);
509
+ const pubkey = signer.pubkey;
510
+
511
+ const filePath = path.join(this.agentsDir, `${pubkey}.json`);
512
+
513
+ // Load existing agent to check for changes
514
+ const existing = await this.loadAgent(pubkey);
515
+
516
+ // Get the agent's current projects from the index for duplicate slug cleanup
517
+ const currentProjects = this.getIndexProjectsForAgent(pubkey);
518
+
519
+ // Clean up old agents with same slug in overlapping projects
520
+ await this.cleanupDuplicateSlugs(agent.slug, pubkey, currentProjects);
521
+
522
+ // Save agent file
523
+ await fs.writeFile(filePath, JSON.stringify(agent, null, 2));
524
+
525
+ // Update index
526
+ if (!this.index) await this.loadIndex();
527
+ if (!this.index) return;
528
+
529
+ // Remove old slug entry if slug changed
530
+ if (existing && existing.slug !== agent.slug) {
531
+ const oldSlugEntry = this.index.bySlug[existing.slug];
532
+ if (oldSlugEntry && oldSlugEntry.pubkey === pubkey) {
533
+ delete this.index.bySlug[existing.slug];
534
+ trace.getActiveSpan()?.addEvent("agent.slug_renamed_cleanup", {
535
+ "slug.old": existing.slug,
536
+ "slug.new": agent.slug,
537
+ "agent.pubkey": pubkey,
538
+ });
539
+ }
540
+ }
541
+
542
+ // Remove old eventId entry if eventId changed
543
+ if (
544
+ existing?.eventId &&
545
+ existing.eventId !== agent.eventId &&
546
+ this.index.byEventId[existing.eventId] === pubkey
547
+ ) {
548
+ delete this.index.byEventId[existing.eventId];
549
+ }
550
+
551
+ // Update bySlug index
552
+ if (isAgentActive(agent)) {
553
+ const agentProjects = this.getIndexProjectsForAgent(pubkey);
554
+ const currentSlugEntry = this.index.bySlug[agent.slug];
555
+ if (!currentSlugEntry || currentSlugEntry.pubkey === pubkey) {
556
+ // Only update if we already own the slug or there's no current owner.
557
+ // Slug takeover for new agents happens in addAgentToProject after cleanup.
558
+ this.index.bySlug[agent.slug] = { pubkey, projectIds: agentProjects };
559
+ }
560
+ } else {
561
+ // For inactive agents, handle slug ownership transition
562
+ const currentOwner = this.index.bySlug[agent.slug];
563
+ if (currentOwner?.pubkey === pubkey) {
564
+ // This agent was the canonical owner but is now inactive
565
+ // Find another active agent with the same slug to take ownership
566
+ const alternativeOwner = await this.findAlternativeSlugOwner(agent.slug, pubkey);
567
+ if (alternativeOwner) {
568
+ const altProjects = this.getIndexProjectsForAgent(alternativeOwner);
569
+ this.index.bySlug[agent.slug] = {
570
+ pubkey: alternativeOwner,
571
+ projectIds: altProjects,
572
+ };
573
+ logger.debug(`Reassigned slug '${agent.slug}' from inactive ${pubkey.substring(0, 8)} to active ${alternativeOwner.substring(0, 8)}`);
574
+ } else {
575
+ // No alternative found - keep entry pointing to this agent with empty projects
576
+ currentOwner.projectIds = this.getIndexProjectsForAgent(pubkey);
577
+ }
578
+ } else if (!currentOwner) {
579
+ // No owner yet - claim it for reactivation lookup purposes
580
+ this.index.bySlug[agent.slug] = { pubkey, projectIds: [] };
581
+ }
582
+ // If another agent owns the slug, don't overwrite
583
+ }
584
+
585
+ if (agent.eventId) {
586
+ this.index.byEventId[agent.eventId] = pubkey;
587
+ }
588
+
589
+ await this.saveIndex();
590
+ logger.debug(`Saved agent ${agent.slug} (${pubkey})`);
591
+ }
592
+
593
+ /**
594
+ * Delete an agent and update index.
595
+ *
596
+ * @deprecated This method permanently deletes agent identity (pubkey/nsec).
597
+ * Prefer using removeAgentFromProject() which sets agents to 'inactive' status
598
+ * while preserving their identity for potential reactivation.
599
+ *
600
+ * This method is kept for:
601
+ * - Administrative cleanup of truly orphaned agents
602
+ * - Test teardown
603
+ * - Explicit user-requested deletion
604
+ *
605
+ * @param pubkey - Agent's public key to delete
606
+ */
607
+ async deleteAgent(pubkey: string): Promise<void> {
608
+ const agent = await this.loadAgent(pubkey);
609
+ if (!agent) return;
610
+
611
+ logger.warn(
612
+ `deleteAgent called for ${agent.slug} (${pubkey.substring(0, 8)}) - ` +
613
+ `this permanently destroys agent identity. Consider using removeAgentFromProject instead.`
614
+ );
615
+
616
+ // Delete file
617
+ const filePath = path.join(this.agentsDir, `${pubkey}.json`);
618
+ await fs.unlink(filePath);
619
+
620
+ // Update index
621
+ if (!this.index) await this.loadIndex();
622
+ if (!this.index) return;
623
+
624
+ // Remove from slug index
625
+ const slugEntry = this.index.bySlug[agent.slug];
626
+ if (slugEntry?.pubkey === pubkey) {
627
+ delete this.index.bySlug[agent.slug];
628
+ }
629
+
630
+ // Remove from eventId index
631
+ if (agent.eventId && this.index.byEventId[agent.eventId] === pubkey) {
632
+ delete this.index.byEventId[agent.eventId];
633
+ }
634
+
635
+ // Remove from project index by scanning byProject
636
+ for (const projectDTag of Object.keys(this.index.byProject)) {
637
+ const projectAgents = this.index.byProject[projectDTag];
638
+ if (projectAgents.includes(pubkey)) {
639
+ this.index.byProject[projectDTag] = projectAgents.filter((p) => p !== pubkey);
640
+ if (this.index.byProject[projectDTag].length === 0) {
641
+ delete this.index.byProject[projectDTag];
642
+ }
643
+ }
644
+ }
645
+
646
+ await this.saveIndex();
647
+ logger.info(`Deleted agent ${agent.slug} (${pubkey})`);
648
+ }
649
+
650
+ /**
651
+ * Check if any agent (in any project) uses the given slug.
652
+ * Used for global uniqueness checks (e.g., import commands).
653
+ */
654
+ async slugExists(slug: string): Promise<boolean> {
655
+ if (!this.index) await this.loadIndex();
656
+ return !!this.index?.bySlug[slug];
657
+ }
658
+
659
+ /**
660
+ * Get agent by slug (uses index for O(1) lookup).
661
+ *
662
+ * **DEPRECATED**: Use getAgentBySlugForProject() instead for project-scoped lookups.
663
+ * This method returns the LAST agent saved with this slug, which may not be the
664
+ * correct agent when multiple agents use the same slug across different projects.
665
+ *
666
+ * @deprecated Use getAgentBySlugForProject(slug, projectDTag) instead
667
+ */
668
+ async getAgentBySlug(slug: string): Promise<StoredAgent | null> {
669
+ if (!this.index) await this.loadIndex();
670
+ if (!this.index) return null;
671
+
672
+ const slugEntry = this.index.bySlug[slug];
673
+ if (!slugEntry) return null;
674
+
675
+ logger.warn("Using deprecated getAgentBySlug() - consider using getAgentBySlugForProject()", {
676
+ slug,
677
+ pubkey: slugEntry.pubkey.substring(0, 8),
678
+ });
679
+
680
+ return this.loadAgent(slugEntry.pubkey);
681
+ }
682
+
683
+ /**
684
+ * Get agent by slug within a specific project context.
685
+ * This is the correct method to use when slug may not be globally unique.
686
+ *
687
+ * @param slug - The agent slug to search for
688
+ * @param projectDTag - The project context to search within
689
+ * @returns The agent if found in this project, null otherwise
690
+ */
691
+ async getAgentBySlugForProject(slug: string, projectDTag: string): Promise<StoredAgent | null> {
692
+ if (!this.index) await this.loadIndex();
693
+ if (!this.index) return null;
694
+
695
+ const slugEntry = this.index.bySlug[slug];
696
+ if (!slugEntry) return null;
697
+
698
+ // Check if this slug is used in the specified project
699
+ if (!(slugEntry.projectIds ?? []).includes(projectDTag)) {
700
+ return null;
701
+ }
702
+
703
+ return this.loadAgent(slugEntry.pubkey);
704
+ }
705
+
706
+ /**
707
+ * Get agent by eventId (uses index for O(1) lookup)
708
+ */
709
+ async getAgentByEventId(eventId: string): Promise<StoredAgent | null> {
710
+ if (!this.index) await this.loadIndex();
711
+ if (!this.index) return null;
712
+
713
+ const pubkey = this.index.byEventId[eventId];
714
+ if (!pubkey) return null;
715
+
716
+ return this.loadAgent(pubkey);
717
+ }
718
+
719
+ /**
720
+ * Get all agents for a project (uses index for O(1) lookup).
721
+ * Deduplicates by slug, keeping only the agent currently in bySlug index.
722
+ *
723
+ * Only returns active agents - inactive agents (removed from all projects
724
+ * but identity preserved) are filtered out.
725
+ */
726
+ async getProjectAgents(projectDTag: string): Promise<StoredAgent[]> {
727
+ if (!this.index) await this.loadIndex();
728
+ if (!this.index) return [];
729
+
730
+ const pubkeys = this.index.byProject[projectDTag] || [];
731
+ const agents: StoredAgent[] = [];
732
+ const seenSlugs = new Set<string>();
733
+
734
+ for (const pubkey of pubkeys) {
735
+ const agent = await this.loadAgent(pubkey);
736
+ if (!agent) continue;
737
+
738
+ // Skip inactive agents - they shouldn't appear in project listings
739
+ if (!isAgentActive(agent)) continue;
740
+
741
+ // Skip if we've already seen this slug - keep only the canonical one
742
+ if (seenSlugs.has(agent.slug)) continue;
743
+
744
+ // Only include if this pubkey is the canonical one for this slug
745
+ const slugEntry = this.index.bySlug[agent.slug];
746
+ if (slugEntry?.pubkey === pubkey) {
747
+ agents.push(agent);
748
+ seenSlugs.add(agent.slug);
749
+ }
750
+ }
751
+
752
+ return agents;
753
+ }
754
+
755
+ /**
756
+ * Get all projects for an agent (reverse lookup by pubkey via index)
757
+ */
758
+ async getAgentProjects(pubkey: string): Promise<string[]> {
759
+ if (!this.index) await this.loadIndex();
760
+ return this.getIndexProjectsForAgent(pubkey);
761
+ }
762
+
763
+ /**
764
+ * Scan byProject index and return all dTags where pubkey appears.
765
+ */
766
+ private getIndexProjectsForAgent(pubkey: string): string[] {
767
+ if (!this.index) return [];
768
+ return Object.entries(this.index.byProject)
769
+ .filter(([, pubkeys]) => pubkeys.includes(pubkey))
770
+ .map(([dTag]) => dTag);
771
+ }
772
+
773
+ /**
774
+ * Add an agent to a project.
775
+ *
776
+ * If the agent was previously inactive (removed from all projects), this
777
+ * reactivates the agent, preserving its original identity (pubkey/nsec).
778
+ */
779
+ async addAgentToProject(pubkey: string, projectDTag: string): Promise<void> {
780
+ const agent = await this.loadAgent(pubkey);
781
+ if (!agent) {
782
+ throw new Error(`Agent ${pubkey} not found`);
783
+ }
784
+
785
+ if (!this.index) await this.loadIndex();
786
+ if (!this.index) return;
787
+
788
+ const wasInactive = !isAgentActive(agent);
789
+
790
+ // Update byProject index
791
+ // Clean up any agent with the same slug already in this project
792
+ await this.cleanupDuplicateSlugs(agent.slug, pubkey, [projectDTag]);
793
+
794
+ if (!this.index.byProject[projectDTag]) {
795
+ this.index.byProject[projectDTag] = [];
796
+ }
797
+ if (!this.index.byProject[projectDTag].includes(pubkey)) {
798
+ this.index.byProject[projectDTag].push(pubkey);
799
+ }
800
+
801
+ // Update bySlug index - the last agent added to a project claims slug ownership
802
+ const slugEntry = this.index.bySlug[agent.slug];
803
+ if (slugEntry?.pubkey === pubkey) {
804
+ slugEntry.projectIds ??= [];
805
+ if (!slugEntry.projectIds.includes(projectDTag)) {
806
+ slugEntry.projectIds.push(projectDTag);
807
+ }
808
+ } else {
809
+ // Take over slug ownership (cleanup already evicted any conflicting agents above)
810
+ this.index.bySlug[agent.slug] = { pubkey, projectIds: [projectDTag] };
811
+ }
812
+
813
+ // Reactivate if agent was inactive
814
+ agent.status = "active";
815
+ await this.saveAgent(agent);
816
+
817
+ if (wasInactive) {
818
+ logger.info(`Reactivated agent ${agent.slug} (${pubkey.substring(0, 8)}) for project ${projectDTag}`);
819
+ }
820
+ }
821
+
822
+ /**
823
+ * Remove an agent from a project.
824
+ *
825
+ * ## Identity Preservation Policy
826
+ * When an agent is removed from all projects, it becomes 'inactive' rather than
827
+ * being deleted. This preserves the agent's identity (pubkey/nsec) so that if
828
+ * the same agent is later assigned to a project, it retains its original keys.
829
+ *
830
+ * Inactive agents:
831
+ * - Are NOT returned by getProjectAgents()
832
+ * - Retain their pubkey, nsec, slug, and all configuration
833
+ * - Can be reactivated by addAgentToProject()
834
+ */
835
+ async removeAgentFromProject(pubkey: string, projectDTag: string): Promise<void> {
836
+ const agent = await this.loadAgent(pubkey);
837
+ if (!agent) return;
838
+
839
+ if (!this.index) await this.loadIndex();
840
+ if (!this.index) return;
841
+
842
+ // Update byProject index
843
+ const projectAgents = this.index.byProject[projectDTag];
844
+ if (projectAgents) {
845
+ this.index.byProject[projectDTag] = projectAgents.filter((p) => p !== pubkey);
846
+ if (this.index.byProject[projectDTag].length === 0) {
847
+ delete this.index.byProject[projectDTag];
848
+ }
849
+ }
850
+
851
+ // Update bySlug index projectIds
852
+ const slugEntry = this.index.bySlug[agent.slug];
853
+ if (slugEntry?.pubkey === pubkey) {
854
+ slugEntry.projectIds = (slugEntry.projectIds ?? []).filter((p) => p !== projectDTag);
855
+ }
856
+
857
+ // Set status based on remaining projects - NEVER delete agent files
858
+ const remainingProjects = this.getIndexProjectsForAgent(pubkey);
859
+ agent.status = remainingProjects.length === 0 ? "inactive" : "active";
860
+ await this.saveAgent(agent);
861
+
862
+ if (agent.status === "inactive") {
863
+ logger.info(`Agent ${agent.slug} (${pubkey.substring(0, 8)}) marked inactive - identity preserved`);
864
+ }
865
+ }
866
+
867
+ /**
868
+ * Update an agent's global PM designation flag.
869
+ *
870
+ * When isPM is true, this agent becomes the PM for ALL projects where it exists.
871
+ * This takes precedence over pmOverrides and project tag designations.
872
+ *
873
+ * Updates ONLY the stored data on disk. To refresh the in-memory instance,
874
+ * call AgentRegistry.reloadAgent() after this method.
875
+ *
876
+ * @param pubkey - Agent's public key (hex string)
877
+ * @param isPM - Whether this agent is designated as PM (true/false/undefined to clear)
878
+ * @returns true if updated successfully, false if agent not found
879
+ */
880
+ async updateAgentIsPM(pubkey: string, isPM: boolean | undefined): Promise<boolean> {
881
+ const agent = await this.loadAgent(pubkey);
882
+ if (!agent) {
883
+ logger.warn(`Agent with pubkey ${pubkey} not found`);
884
+ return false;
885
+ }
886
+
887
+ if (isPM === undefined || isPM === false) {
888
+ // Clear the flag if it exists
889
+ delete agent.isPM;
890
+ } else {
891
+ agent.isPM = true;
892
+ }
893
+
894
+ await this.saveAgent(agent);
895
+ logger.info(`Updated isPM flag for agent ${agent.name}`, { isPM: agent.isPM });
896
+ return true;
897
+ }
898
+
899
+ /**
900
+ * Get the effective (resolved) config for an agent, optionally scoped to a project.
901
+ *
902
+ * @param agent - The stored agent
903
+ * @param projectDTag - Optional project dTag for project-scoped resolution
904
+ * @returns ResolvedAgentConfig with effective model and tools
905
+ */
906
+ getEffectiveConfig(agent: StoredAgent, projectDTag?: string): ResolvedAgentConfig {
907
+ const defaultConfig: AgentDefaultConfig = {
908
+ model: agent.default?.model,
909
+ tools: agent.default?.tools,
910
+ };
911
+
912
+ const projectConfig = projectDTag
913
+ ? agent.projectOverrides?.[projectDTag]
914
+ : undefined;
915
+
916
+ return resolveEffectiveConfig(defaultConfig, projectConfig);
917
+ }
918
+
919
+ /**
920
+ * Update an agent's default configuration block.
921
+ *
922
+ * A 24020 event with NO a-tag should call this method.
923
+ * Writes to the `default` block in the agent file.
924
+ *
925
+ * @param pubkey - Agent's public key
926
+ * @param updates - Fields to update. Only defined fields are applied.
927
+ * @param options - Optional behavior flags
928
+ * @param options.clearProjectOverrides - If true, clears all projectOverrides (default: false)
929
+ * @returns true if updated successfully, false if agent not found
930
+ */
931
+ async updateDefaultConfig(
932
+ pubkey: string,
933
+ updates: AgentDefaultConfig,
934
+ options?: UpdateDefaultConfigOptions
935
+ ): Promise<boolean> {
936
+ const agent = await this.loadAgent(pubkey);
937
+ if (!agent) {
938
+ logger.warn(`Agent with pubkey ${pubkey} not found`);
939
+ return false;
940
+ }
941
+
942
+ if (!agent.default) {
943
+ agent.default = {};
944
+ }
945
+
946
+ if (updates.model !== undefined) {
947
+ agent.default.model = updates.model;
948
+ }
949
+
950
+ if (updates.tools !== undefined) {
951
+ if (updates.tools.length > 0) {
952
+ agent.default.tools = updates.tools;
953
+ } else {
954
+ delete agent.default.tools;
955
+ }
956
+ }
957
+
958
+ // Clean up empty default block
959
+ if (agent.default && Object.keys(agent.default).length === 0) {
960
+ delete agent.default;
961
+ }
962
+
963
+ // Clear all project overrides when a global config update is received
964
+ if (options?.clearProjectOverrides && agent.projectOverrides) {
965
+ delete agent.projectOverrides;
966
+ logger.info(`Cleared projectOverrides for agent ${agent.name} (global config update)`);
967
+ }
968
+
969
+ await this.saveAgent(agent);
970
+ logger.info(`Updated default config for agent ${agent.name}`, { updates });
971
+ return true;
972
+ }
973
+
974
+ /**
975
+ * Update an agent's per-project override configuration.
976
+ *
977
+ * A 24020 event WITH an a-tag should call this method.
978
+ * Writes to `projectOverrides[projectDTag]`.
979
+ *
980
+ * If any provided values equal the defaults after resolution, they are cleared
981
+ * from the override (dedup logic) to keep overrides minimal.
982
+ *
983
+ * If `reset` is true, clears the entire project override.
984
+ *
985
+ * @param pubkey - Agent's public key
986
+ * @param projectDTag - Project dTag to scope the config to
987
+ * @param override - The new project override (full replacement, not merge)
988
+ * @param reset - If true, clear the entire project override instead of setting it
989
+ * @returns true if updated successfully, false if agent not found
990
+ */
991
+ async updateProjectOverride(
992
+ pubkey: string,
993
+ projectDTag: string,
994
+ override: AgentProjectConfig,
995
+ reset = false
996
+ ): Promise<boolean> {
997
+ const agent = await this.loadAgent(pubkey);
998
+ if (!agent) {
999
+ logger.warn(`Agent with pubkey ${pubkey} not found`);
1000
+ return false;
1001
+ }
1002
+
1003
+ if (reset) {
1004
+ if (agent.projectOverrides) {
1005
+ delete agent.projectOverrides[projectDTag];
1006
+ if (Object.keys(agent.projectOverrides).length === 0) {
1007
+ delete agent.projectOverrides;
1008
+ }
1009
+ }
1010
+ logger.info(`Cleared project override for agent ${agent.name}`, { projectDTag });
1011
+ } else {
1012
+ const defaultConfig: AgentDefaultConfig = {
1013
+ model: agent.default?.model,
1014
+ tools: agent.default?.tools,
1015
+ };
1016
+
1017
+ const deduplicated = deduplicateProjectConfig(defaultConfig, override);
1018
+
1019
+ if (!agent.projectOverrides) {
1020
+ agent.projectOverrides = {};
1021
+ }
1022
+
1023
+ if (Object.keys(deduplicated).length === 0) {
1024
+ delete agent.projectOverrides[projectDTag];
1025
+ if (Object.keys(agent.projectOverrides).length === 0) {
1026
+ delete agent.projectOverrides;
1027
+ }
1028
+ logger.info(`Project override for ${projectDTag} cleared (all fields match defaults)`, {
1029
+ agentSlug: agent.slug,
1030
+ });
1031
+ } else {
1032
+ agent.projectOverrides[projectDTag] = deduplicated;
1033
+ logger.info(`Updated project override for agent ${agent.name}`, {
1034
+ projectDTag,
1035
+ override: deduplicated,
1036
+ });
1037
+ }
1038
+ }
1039
+
1040
+ await this.saveAgent(agent);
1041
+ return true;
1042
+ }
1043
+
1044
+ /**
1045
+ * Resolve the effective PM status for an agent in a specific project.
1046
+ * Priority:
1047
+ * 1. agent.isPM (global PM designation via kind 24020 without a-tag)
1048
+ * 2. projectOverrides[projectDTag].isPM (project-scoped PM via kind 24020 with a-tag)
1049
+ * 3. pmOverrides[projectDTag] (legacy, for backward compatibility)
1050
+ */
1051
+ resolveEffectiveIsPM(agent: StoredAgent, projectDTag: string): boolean {
1052
+ if (agent.isPM === true) {
1053
+ return true;
1054
+ }
1055
+ if (agent.projectOverrides?.[projectDTag]?.isPM === true) {
1056
+ return true;
1057
+ }
1058
+ return agent.pmOverrides?.[projectDTag] === true;
1059
+ }
1060
+
1061
+ /**
1062
+ * Update an agent's project-scoped PM designation.
1063
+ * Writes to projectOverrides[projectDTag].isPM.
1064
+ *
1065
+ * @param pubkey - Agent's public key
1066
+ * @param projectDTag - Project dTag to scope the config to
1067
+ * @param isPM - PM designation (true/false/undefined to clear)
1068
+ * @returns true if updated successfully, false if agent not found
1069
+ */
1070
+ async updateProjectScopedIsPM(
1071
+ pubkey: string,
1072
+ projectDTag: string,
1073
+ isPM: boolean | undefined
1074
+ ): Promise<boolean> {
1075
+ const agent = await this.loadAgent(pubkey);
1076
+ if (!agent) {
1077
+ logger.warn(`Agent with pubkey ${pubkey} not found`);
1078
+ return false;
1079
+ }
1080
+
1081
+ if (isPM === true) {
1082
+ if (!agent.projectOverrides) {
1083
+ agent.projectOverrides = {};
1084
+ }
1085
+ if (!agent.projectOverrides[projectDTag]) {
1086
+ agent.projectOverrides[projectDTag] = {};
1087
+ }
1088
+ agent.projectOverrides[projectDTag].isPM = true;
1089
+ } else {
1090
+ const override = agent.projectOverrides?.[projectDTag];
1091
+ if (override) {
1092
+ delete override.isPM;
1093
+ if (Object.keys(override).length === 0) {
1094
+ delete agent.projectOverrides![projectDTag];
1095
+ if (Object.keys(agent.projectOverrides!).length === 0) {
1096
+ delete agent.projectOverrides;
1097
+ }
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ await this.saveAgent(agent);
1103
+ logger.info(`Updated project-scoped PM flag for agent ${agent.name}`, {
1104
+ projectDTag,
1105
+ isPM,
1106
+ });
1107
+ return true;
1108
+ }
1109
+
1110
+ /**
1111
+ * Get all agents (for debugging/admin purposes)
1112
+ */
1113
+ async getAllAgents(): Promise<StoredAgent[]> {
1114
+ await ensureDirectory(this.agentsDir);
1115
+ const files = await fs.readdir(this.agentsDir);
1116
+ const agents: StoredAgent[] = [];
1117
+
1118
+ for (const file of files) {
1119
+ if (!file.endsWith(".json") || file === "index.json") continue;
1120
+
1121
+ const pubkey = file.slice(0, -5);
1122
+ const agent = await this.loadAgent(pubkey);
1123
+ if (agent) {
1124
+ agents.push(agent);
1125
+ }
1126
+ }
1127
+
1128
+ return agents;
1129
+ }
1130
+ }
1131
+
1132
+ // Export singleton instance
1133
+ export const agentStorage = new AgentStorage();