@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,866 @@
1
+ /**
2
+ * Message builder for converting ConversationStore entries to LLM messages.
3
+ *
4
+ * This module handles the complex logic of building ModelMessages from
5
+ * ConversationEntry records, including:
6
+ * - Tool call/result ordering for AI SDK validation
7
+ * - Orphaned tool call reconciliation
8
+ * - Message deference during pending tool execution
9
+ * - Delegation completion pruning
10
+ * - Message attribution for unexpected senders
11
+ * - AGENTS.md system reminder injection for file-read operations
12
+ */
13
+ import type { ModelMessage, ToolCallPart, ToolResultPart } from "ai";
14
+ import { trace } from "@opentelemetry/api";
15
+ import type { ConversationEntry, DelegationMarker } from "./types";
16
+ import { getPubkeyService } from "@/services/PubkeyService";
17
+ import { convertToMultimodalContent, hasImageUrls } from "./utils/multimodal-content";
18
+ import { processToolResult, shouldTruncateToolResult, type TruncationContext } from "./utils/tool-result-truncator";
19
+ import {
20
+ createImageTracker,
21
+ processToolResultWithImageTracking,
22
+ type ImageTracker,
23
+ } from "./utils/image-placeholder";
24
+ import { extractImageUrls } from "./utils/image-url-utils";
25
+ import {
26
+ createAgentsMdVisibilityTracker,
27
+ getSystemRemindersForPath,
28
+ shouldInjectForTool,
29
+ extractPathFromToolInput,
30
+ appendSystemReminderToOutput,
31
+ type AgentsMdVisibilityTracker,
32
+ } from "@/services/agents-md";
33
+
34
+ /**
35
+ * Extract tool input from a ToolCallPart.
36
+ * AI SDK v6 uses 'input' property, but some storage formats use 'args'.
37
+ * This utility handles both cases in a type-safe manner.
38
+ */
39
+ function getToolInput(part: ToolCallPart): unknown {
40
+ // ToolCallPart type may not include 'args', but storage format might
41
+ const partWithArgs = part as ToolCallPart & { args?: unknown };
42
+ return partWithArgs.input ?? partWithArgs.args;
43
+ }
44
+
45
+ export interface MessageBuilderContext {
46
+ /**
47
+ * The pubkey of the agent viewing/building messages
48
+ */
49
+ viewingAgentPubkey: string;
50
+ /**
51
+ * The current RAL number for the execution
52
+ */
53
+ ralNumber: number;
54
+ /**
55
+ * Set of currently active RAL numbers for the agent
56
+ */
57
+ activeRals: Set<number>;
58
+ /**
59
+ * Index offset when processing a slice of messages (default 0)
60
+ */
61
+ indexOffset?: number;
62
+ /**
63
+ * Total message count for truncation context
64
+ */
65
+ totalMessages: number;
66
+ /**
67
+ * Project root directory for AGENTS.md discovery.
68
+ * If provided, enables system reminder injection after file-read tool results.
69
+ */
70
+ projectRoot?: string;
71
+ /**
72
+ * Set of pubkeys that belong to agents (non-whitelisted).
73
+ * Used by computeAttributionPrefix to distinguish agent messages from user messages.
74
+ * If not provided, all non-self pubkeys are treated as users (no agent attribution).
75
+ */
76
+ agentPubkeys?: Set<string>;
77
+ /**
78
+ * The conversation ID being built.
79
+ * Required for delegation marker expansion - markers are only expanded
80
+ * if their parentConversationId matches this conversationId (direct children only).
81
+ */
82
+ conversationId?: string;
83
+ /**
84
+ * Callback to get messages from a delegation conversation.
85
+ * Used for lazy expansion of delegation markers.
86
+ * Returns the messages array or undefined if conversation not found.
87
+ */
88
+ getDelegationMessages?: (delegationConversationId: string) => ConversationEntry[] | undefined;
89
+ }
90
+
91
+ /**
92
+ * Context for AGENTS.md system reminder injection
93
+ */
94
+ interface AgentsMdContext {
95
+ /** Project root for AGENTS.md discovery */
96
+ projectRoot: string;
97
+ /** Visibility tracker for deduplication */
98
+ tracker: AgentsMdVisibilityTracker;
99
+ /** Map of toolCallId -> path for file-read tools */
100
+ toolCallPaths: Map<string, string>;
101
+ }
102
+
103
+ /**
104
+ * Derive the appropriate role for a message based on viewer perspective.
105
+ *
106
+ * Rules:
107
+ * - Explicit role override: If entry.role is set (for synthetic entries like compressed summaries), use it
108
+ * - assistant: Only for the viewing agent's own messages
109
+ * - user: All other messages (regardless of targeting)
110
+ * - tool: Tool results (fixed)
111
+ * - system: Synthetic system messages (compressed summaries, etc.)
112
+ *
113
+ * Note: Attribution context is not added to LLM input. Role simply distinguishes
114
+ * between the agent's own messages and messages from others.
115
+ */
116
+ function deriveRole(
117
+ entry: ConversationEntry,
118
+ viewingAgentPubkey: string
119
+ ): "user" | "assistant" | "tool" | "system" {
120
+ // Explicit role override for synthetic entries (e.g., compressed summaries)
121
+ // CRITICAL: Without this, compressed summaries with pubkey="system" would become "user" role,
122
+ // turning compressed history into user instructions and causing catastrophic LLM behavior.
123
+ if (entry.role) {
124
+ return entry.role;
125
+ }
126
+
127
+ // Tool messages have fixed roles
128
+ if (entry.messageType === "tool-call") return "assistant";
129
+ if (entry.messageType === "tool-result") return "tool";
130
+
131
+ // Text messages - assistant for own, user for everything else
132
+ if (entry.pubkey === viewingAgentPubkey) {
133
+ return "assistant"; // Own messages
134
+ }
135
+
136
+ return "user"; // All non-self messages
137
+ }
138
+
139
+ /**
140
+ * Compute an attribution prefix for a conversation entry.
141
+ *
142
+ * Returns a string prefix (or empty string) to prepend to the message content,
143
+ * enabling the LLM to distinguish who said what in multi-agent shared conversations.
144
+ *
145
+ * Priority rules (ordered, first match wins):
146
+ * 1. Self message → "" (no prefix)
147
+ * 2. Non-text entry (tool-call, tool-result, delegation-marker, role-override) → "" (no prefix)
148
+ * 3. Has targetedPubkeys NOT including viewing agent → "[@sender -> @recipient] " (routing)
149
+ * 4. Sender is agent (in agentPubkeys set) → "[@sender] " (attribution)
150
+ * 5. Otherwise (user message targeted to me, or no targeting) → "" (no prefix)
151
+ *
152
+ * **Purity note:** This function is pure when a custom `resolveDisplayName` is provided
153
+ * (as done in unit tests). The default resolver calls `PubkeyService.getNameSync()`, which
154
+ * reads from a global service singleton—this is intentional for production use but means
155
+ * the default invocation is not referentially transparent.
156
+ *
157
+ * @param entry - The conversation entry to compute prefix for
158
+ * @param viewingAgentPubkey - The pubkey of the agent viewing/building messages
159
+ * @param agentPubkeys - Set of pubkeys that belong to agents (used to distinguish agents from users)
160
+ * @param resolveDisplayName - Optional injectable name resolver (defaults to PubkeyService.getNameSync)
161
+ * @returns The prefix string to prepend, or empty string for no prefix
162
+ */
163
+ export function computeAttributionPrefix(
164
+ entry: ConversationEntry,
165
+ viewingAgentPubkey: string,
166
+ agentPubkeys: Set<string>,
167
+ resolveDisplayName?: (pubkey: string) => string
168
+ ): string {
169
+ const resolve = resolveDisplayName ?? ((pk: string) => {
170
+ try {
171
+ const name = getPubkeyService().getNameSync(pk);
172
+ return name || pk.substring(0, 8);
173
+ } catch {
174
+ return pk.substring(0, 8);
175
+ }
176
+ });
177
+
178
+ // Determine the actual sender (injected messages track original sender via senderPubkey)
179
+ const senderPubkey = entry.senderPubkey ?? entry.pubkey;
180
+
181
+ // Rule 1: Self message → no prefix
182
+ if (senderPubkey === viewingAgentPubkey) return "";
183
+
184
+ // Rule 2: Non-text entry → no prefix
185
+ if (entry.messageType !== "text") return "";
186
+ if (entry.role) return ""; // Explicit role override (synthetic entries like compressed summaries)
187
+
188
+ // Rule 3: Has targetedPubkeys NOT including viewing agent → routing prefix
189
+ const targetedPubkeys = entry.targetedPubkeys ?? [];
190
+ if (targetedPubkeys.length > 0 && !targetedPubkeys.includes(viewingAgentPubkey)) {
191
+ const senderName = resolve(senderPubkey);
192
+ const recipientName = resolve(targetedPubkeys[0]);
193
+ return `[@${senderName} -> @${recipientName}] `;
194
+ }
195
+
196
+ // Rule 4: Sender is agent → attribution prefix
197
+ if (agentPubkeys.has(senderPubkey)) {
198
+ const senderName = resolve(senderPubkey);
199
+ return `[@${senderName}] `;
200
+ }
201
+
202
+ // Rule 5: Otherwise (user message targeted to me, or no targeting) → no prefix
203
+ return "";
204
+ }
205
+
206
+ /**
207
+ * Convert a ConversationEntry to a ModelMessage for the viewing agent.
208
+ *
209
+ * Attribution prefixes are added for multi-agent shared conversations using
210
+ * computeAttributionPrefix() to help the LLM distinguish who said what.
211
+ *
212
+ * For text messages containing image URLs, the content is converted to
213
+ * multimodal format (TextPart + ImagePart array) for AI SDK compatibility.
214
+ *
215
+ * Image Placeholder Strategy:
216
+ * - First appearance of an image: shown in full
217
+ * - Subsequent appearances: replaced with placeholder referencing eventId
218
+ * - The imageTracker tracks which images have been seen
219
+ *
220
+ * AGENTS.md System Reminders:
221
+ * - When a file-read tool result is processed, check for AGENTS.md files
222
+ * - Inject system reminders after the tool output for newly visible AGENTS.md files
223
+ * - Track visibility to avoid duplication in subsequent tool results
224
+ */
225
+ async function entryToMessage(
226
+ entry: ConversationEntry,
227
+ viewingAgentPubkey: string,
228
+ truncationContext: TruncationContext | undefined,
229
+ agentPubkeys: Set<string>,
230
+ imageTracker: ImageTracker,
231
+ agentsMdContext?: AgentsMdContext,
232
+ enableMultimodal: boolean = true
233
+ ): Promise<ModelMessage> {
234
+ const role = deriveRole(entry, viewingAgentPubkey);
235
+
236
+ if (entry.messageType === "tool-call" && entry.toolData) {
237
+ // Track paths from file-read tool calls for AGENTS.md injection
238
+ if (agentsMdContext) {
239
+ for (const part of entry.toolData as ToolCallPart[]) {
240
+ if (shouldInjectForTool(part.toolName)) {
241
+ const toolInput = getToolInput(part);
242
+ const path = extractPathFromToolInput(toolInput);
243
+ if (path) {
244
+ agentsMdContext.toolCallPaths.set(part.toolCallId, path);
245
+ }
246
+ }
247
+ }
248
+ }
249
+ return { role: "assistant", content: entry.toolData as ToolCallPart[] };
250
+ }
251
+
252
+ if (entry.messageType === "tool-result" && entry.toolData) {
253
+ // First: Apply image placeholder strategy (tracks & replaces seen images)
254
+ const imageProcessingResult = processToolResultWithImageTracking(
255
+ entry.toolData as ToolResultPart[],
256
+ imageTracker,
257
+ entry.eventId
258
+ );
259
+ let toolData = imageProcessingResult.processedParts;
260
+
261
+ // Check if result will be truncated (for AGENTS.md visibility tracking)
262
+ const willBeTruncated = truncationContext
263
+ ? shouldTruncateToolResult(toolData, truncationContext)
264
+ : false;
265
+
266
+ // Then: Apply truncation for buried tool results to save context
267
+ toolData = truncationContext
268
+ ? processToolResult(toolData, truncationContext)
269
+ : toolData;
270
+
271
+ // Inject AGENTS.md system reminders after file-read tool results
272
+ // Only inject if:
273
+ // 1. AGENTS.md context is available
274
+ // 2. Tool result is NOT truncated (reminders would be lost)
275
+ // 3. Tool is a file-read operation with a tracked path
276
+ if (agentsMdContext && !willBeTruncated) {
277
+ for (const part of toolData) {
278
+ const path = agentsMdContext.toolCallPaths.get(part.toolCallId);
279
+ if (path) {
280
+ const reminders = await getSystemRemindersForPath(
281
+ path,
282
+ agentsMdContext.projectRoot,
283
+ agentsMdContext.tracker,
284
+ willBeTruncated
285
+ );
286
+
287
+ if (reminders.hasReminders) {
288
+ // Cast is needed because appendSystemReminderToOutput returns unknown
289
+ (part as { output: unknown }).output = appendSystemReminderToOutput(part.output, reminders.content);
290
+
291
+ // Telemetry for AGENTS.md injection
292
+ trace.getActiveSpan?.()?.addEvent("conversation.agents_md_injected", {
293
+ "agents_md.file_count": reminders.includedFiles.length,
294
+ "agents_md.paths": reminders.includedFiles.map(f => f.directory).join(","),
295
+ "agents_md.target_path": path,
296
+ });
297
+ }
298
+
299
+ // Clean up the tracked path
300
+ agentsMdContext.toolCallPaths.delete(part.toolCallId);
301
+ }
302
+ }
303
+ }
304
+
305
+ return {
306
+ role: "tool",
307
+ content: toolData,
308
+ _imageReplacementStats: imageProcessingResult.replacedCount > 0
309
+ ? { replacedCount: imageProcessingResult.replacedCount, uniqueReplacedCount: imageProcessingResult.uniqueReplacedCount }
310
+ : undefined,
311
+ } as unknown as ModelMessage;
312
+ }
313
+
314
+ // Text message - compute attribution prefix for multi-agent conversations
315
+ const prefix = computeAttributionPrefix(entry, viewingAgentPubkey, agentPubkeys);
316
+ let messageContent = prefix ? `${prefix}${entry.content}` : entry.content;
317
+
318
+ // Track any images in text messages (but don't replace them - user content)
319
+ // This ensures that if the same image appears later in a tool result,
320
+ // it will be replaced with a placeholder
321
+ const imageUrls = extractImageUrls(messageContent);
322
+ for (const url of imageUrls) {
323
+ imageTracker.markAsSeen(url);
324
+ }
325
+
326
+ // Convert to multimodal format if content contains image URLs, but ONLY for user messages
327
+ // AND only when enableMultimodal is true (i.e., the most recent user message with images).
328
+ //
329
+ // The AI SDK ModelMessage[] schema only allows ImagePart in user role messages (UserModelMessage).
330
+ // AssistantModelMessage content only supports TextPart, ReasoningPart, ToolCallPart, etc. — no ImagePart.
331
+ // Applying multimodal conversion to assistant messages causes:
332
+ // AI_InvalidPromptError: Invalid prompt: The messages do not match the ModelMessage[] schema.
333
+ //
334
+ // For older user messages that contained images, we keep the URL as plain text in the string —
335
+ // the LLM has already seen them, no need to re-fetch and waste context window on image tokens.
336
+ const content = (role === "user" && enableMultimodal) ? convertToMultimodalContent(messageContent) : messageContent;
337
+
338
+ return { role, content } as ModelMessage;
339
+ }
340
+
341
+ /**
342
+ * Expand a delegation marker into a formatted transcript message.
343
+ *
344
+ * This function formats messages from a delegation conversation into a flat
345
+ * transcript. The transcript includes:
346
+ * - Text messages only (not tool calls/results)
347
+ * - Messages with p-tags (targeted to specific recipients)
348
+ * - No nested delegation markers (flat expansion only)
349
+ *
350
+ * Format: [@sender -> @recipient]: message content
351
+ *
352
+ * @param marker - The delegation marker to expand
353
+ * @param delegationMessages - Messages from the delegation conversation, or undefined if not found
354
+ * @returns Formatted transcript as a ModelMessage
355
+ */
356
+ async function expandDelegationMarker(
357
+ marker: DelegationMarker,
358
+ delegationMessages: ConversationEntry[] | undefined
359
+ ): Promise<ModelMessage> {
360
+ const pubkeyService = getPubkeyService();
361
+
362
+ // Handle pending delegations - show that work is in progress
363
+ if (marker.status === "pending") {
364
+ try {
365
+ const recipientName = await pubkeyService.getName(marker.recipientPubkey);
366
+ return {
367
+ role: "user",
368
+ content: `# DELEGATION IN PROGRESS\n\n@${recipientName} is currently working on this task.`,
369
+ };
370
+ } catch {
371
+ return {
372
+ role: "user",
373
+ content: `# DELEGATION IN PROGRESS\n\nAgent ${marker.recipientPubkey.substring(0, 12)} is currently working on this task.`,
374
+ };
375
+ }
376
+ }
377
+
378
+ if (!delegationMessages) {
379
+ // Delegation conversation not found - return placeholder
380
+ try {
381
+ const recipientName = await pubkeyService.getName(marker.recipientPubkey);
382
+ const statusText = marker.status === "aborted"
383
+ ? `was aborted: ${marker.abortReason || "unknown reason"}`
384
+ : "completed (transcript unavailable)";
385
+ return {
386
+ role: "user",
387
+ content: `# DELEGATION ${marker.status.toUpperCase()}\n\n@${recipientName} ${statusText}`,
388
+ };
389
+ } catch {
390
+ return {
391
+ role: "user",
392
+ content: `# DELEGATION ${marker.status.toUpperCase()}\n\nAgent ${marker.recipientPubkey.substring(0, 12)} ${marker.status === "aborted" ? "was aborted" : "completed"} (transcript unavailable)`,
393
+ };
394
+ }
395
+ }
396
+
397
+ // Build flat transcript from delegation conversation
398
+ const lines: string[] = [];
399
+
400
+ // Header based on status
401
+ if (marker.status === "aborted") {
402
+ lines.push("# DELEGATION ABORTED");
403
+ lines.push("");
404
+ if (marker.abortReason) {
405
+ lines.push(`**Reason:** ${marker.abortReason}`);
406
+ lines.push("");
407
+ }
408
+ } else {
409
+ lines.push("# DELEGATION COMPLETED");
410
+ lines.push("");
411
+ }
412
+
413
+ // Filter for targeted text messages only (no tool calls, no nested markers)
414
+ const transcriptMessages = delegationMessages.filter(msg =>
415
+ msg.messageType === "text" &&
416
+ msg.targetedPubkeys &&
417
+ msg.targetedPubkeys.length > 0
418
+ );
419
+
420
+ if (transcriptMessages.length === 0) {
421
+ lines.push("(No messages in delegation transcript)");
422
+ } else {
423
+ lines.push("### Transcript:");
424
+ for (const msg of transcriptMessages) {
425
+ try {
426
+ const senderName = await pubkeyService.getName(msg.pubkey);
427
+ const recipientName = msg.targetedPubkeys?.[0]
428
+ ? await pubkeyService.getName(msg.targetedPubkeys[0])
429
+ : "unknown";
430
+ lines.push(`[@${senderName} -> @${recipientName}]: ${msg.content}`);
431
+ } catch {
432
+ // Fallback to shortened pubkeys
433
+ const senderFallback = msg.pubkey.substring(0, 12);
434
+ const recipientFallback = msg.targetedPubkeys?.[0]?.substring(0, 12) || "unknown";
435
+ lines.push(`[@${senderFallback} -> @${recipientFallback}]: ${msg.content}`);
436
+ }
437
+ }
438
+ }
439
+
440
+ return {
441
+ role: "user",
442
+ content: lines.join("\n"),
443
+ };
444
+ }
445
+
446
+ /**
447
+ * Format a nested delegation marker as a minimal reference.
448
+ *
449
+ * For nested delegations (delegations that occurred within another delegation),
450
+ * we don't expand the full transcript to avoid exponential bloat. Instead, we
451
+ * display a minimal marker showing:
452
+ * - The recipient agent
453
+ * - The delegation conversation ID (shortened)
454
+ * - The status (completed/aborted)
455
+ *
456
+ * This provides visibility that a delegation happened without including
457
+ * potentially large transcripts in the parent conversation context.
458
+ *
459
+ * @param marker - The delegation marker to format
460
+ * @returns Minimal reference message as a ModelMessage
461
+ */
462
+ async function formatNestedDelegationMarker(
463
+ marker: DelegationMarker
464
+ ): Promise<ModelMessage> {
465
+ const pubkeyService = getPubkeyService();
466
+
467
+ let recipientName: string;
468
+ try {
469
+ recipientName = await pubkeyService.getName(marker.recipientPubkey);
470
+ } catch {
471
+ recipientName = marker.recipientPubkey.substring(0, 12);
472
+ }
473
+
474
+ const shortConversationId = marker.delegationConversationId.substring(0, 12);
475
+
476
+ // Simple one-line format: [Delegation to @recipient (conv: abc123...) - status]
477
+ let statusSuffix: string;
478
+ if (marker.status === "aborted") {
479
+ statusSuffix = ` - aborted${marker.abortReason ? `: ${marker.abortReason}` : ""}`;
480
+ } else if (marker.status === "pending") {
481
+ statusSuffix = " - pending";
482
+ } else {
483
+ statusSuffix = " - completed";
484
+ }
485
+
486
+ return {
487
+ role: "user",
488
+ content: `[Delegation to @${recipientName} (conv: ${shortConversationId}...)${statusSuffix}]`,
489
+ };
490
+ }
491
+
492
+ /**
493
+ * Build ModelMessages from conversation entries.
494
+ *
495
+ * This function handles the complex logic of:
496
+ * 1. Filtering messages by RAL visibility rules
497
+ * 2. Ensuring tool-call/tool-result ordering for AI SDK validation
498
+ * 3. Deferring non-tool messages while tool-calls are pending
499
+ * 4. Injecting synthetic results for orphaned tool-calls
500
+ * 5. Pruning superseded delegation completion messages
501
+ *
502
+ * The AI SDK (Vercel AI) validates that every tool-call message is immediately
503
+ * followed by its corresponding tool-result message. This function ensures
504
+ * that validation passes even when messages arrive out of order.
505
+ */
506
+ export async function buildMessagesFromEntries(
507
+ entries: ConversationEntry[],
508
+ ctx: MessageBuilderContext
509
+ ): Promise<ModelMessage[]> {
510
+ const {
511
+ viewingAgentPubkey,
512
+ ralNumber,
513
+ activeRals,
514
+ indexOffset = 0,
515
+ totalMessages,
516
+ agentPubkeys = new Set<string>(),
517
+ projectRoot,
518
+ } = ctx;
519
+
520
+ const result: ModelMessage[] = [];
521
+ const delegationCompletionPrefix = "# DELEGATION COMPLETED";
522
+
523
+ // Image placeholder strategy: Track seen images across all messages
524
+ // First appearance = full image, subsequent = placeholder
525
+ const imageTracker = createImageTracker();
526
+
527
+ // AGENTS.md system reminder context (only if projectRoot is provided)
528
+ const agentsMdContext: AgentsMdContext | undefined = projectRoot
529
+ ? {
530
+ projectRoot,
531
+ tracker: createAgentsMdVisibilityTracker(),
532
+ toolCallPaths: new Map(),
533
+ }
534
+ : undefined;
535
+
536
+ // Track latest delegation completion for each RAL (to prune superseded ones)
537
+ const latestDelegationCompletionIndexByRal = new Map<number, number>();
538
+ const getDelegationCompletionRal = (entry: ConversationEntry): number | undefined => {
539
+ if (entry.messageType !== "text") return undefined;
540
+ if (typeof entry.ral !== "number") return undefined;
541
+ if (!entry.content.trimStart().startsWith(delegationCompletionPrefix)) return undefined;
542
+ if (!(entry.targetedPubkeys?.includes(viewingAgentPubkey) ?? false)) return undefined;
543
+ return entry.ral;
544
+ };
545
+
546
+ // First pass: identify latest delegation completion for each RAL
547
+ for (let i = 0; i < entries.length; i++) {
548
+ const entry = entries[i];
549
+ const ral = getDelegationCompletionRal(entry);
550
+ if (ral !== undefined) {
551
+ latestDelegationCompletionIndexByRal.set(ral, i);
552
+ }
553
+ }
554
+
555
+ // Pre-scan: find the last user text message that contains image URLs.
556
+ // Only that message gets multimodal conversion (ImagePart objects that trigger image fetching).
557
+ // Older user messages with images keep URLs as plain text — the LLM already saw them,
558
+ // no need to re-fetch and consume context window with image tokens.
559
+ let lastUserImageEntryIndex = -1;
560
+ for (let i = 0; i < entries.length; i++) {
561
+ const entry = entries[i];
562
+ if (entry.messageType !== "text") continue;
563
+ if (deriveRole(entry, viewingAgentPubkey) !== "user") continue;
564
+ if (hasImageUrls(entry.content)) {
565
+ lastUserImageEntryIndex = i;
566
+ }
567
+ }
568
+
569
+ let prunedDelegationCompletions = 0;
570
+
571
+ // ============================================================================
572
+ // TOOL-CALL / TOOL-RESULT ORDERING FIX
573
+ // ============================================================================
574
+ //
575
+ // The AI SDK (Vercel AI) validates that every tool-call message is immediately
576
+ // followed by its corresponding tool-result message. If any other message type
577
+ // (user, assistant text, system) appears between a tool-call and its result,
578
+ // the SDK throws: "Tool result is missing for tool call <id>"
579
+ //
580
+ // PROBLEM 1: User messages arriving mid-tool-execution
581
+ // ----------------------------------------------------
582
+ // Messages are stored in ConversationStore in chronological order:
583
+ // 1. Agent issues tool-call (stored immediately)
584
+ // 2. User sends a new message (stored while tool executes)
585
+ // 3. Tool completes, result stored
586
+ //
587
+ // This results in: [tool-call, user-message, tool-result]
588
+ // But AI SDK requires: [tool-call, tool-result, user-message]
589
+ //
590
+ // PROBLEM 2: Orphaned tool-calls from RAL interruption
591
+ // ----------------------------------------------------
592
+ // When a RAL is aborted mid-execution (e.g., due to a delegation completion
593
+ // triggering an injection), tool-calls may be stored but their results never
594
+ // recorded. The tool continues executing in the background, but the stream
595
+ // handler that would record the result has been torn down.
596
+ //
597
+ // When the RAL resumes, it has orphaned tool-calls with no matching results.
598
+ //
599
+ // SOLUTION:
600
+ // ---------
601
+ // 1. Track pending tool-calls (those without results yet)
602
+ // 2. Defer non-tool messages while tool-calls are pending
603
+ // 3. Flush deferred messages only after all pending results arrive
604
+ // 4. For orphaned tool-calls (no result ever stored), inject synthetic
605
+ // error results to satisfy the AI SDK validation
606
+ //
607
+ // ============================================================================
608
+
609
+ // Map of toolCallId -> {toolName, resultIndex} for pending tool-calls
610
+ // resultIndex tracks where the synthetic result should be inserted if needed
611
+ const pendingToolCalls = new Map<string, { toolName: string; resultIndex: number }>();
612
+
613
+ // Messages deferred because they arrived while tool-calls were pending
614
+ const deferredMessages: Array<{ entry: ConversationEntry; truncationContext: TruncationContext; enableMultimodal: boolean }> = [];
615
+
616
+ for (let i = 0; i < entries.length; i++) {
617
+ const entry = entries[i];
618
+
619
+ // Skip superseded delegation completions
620
+ const ral = getDelegationCompletionRal(entry);
621
+ if (ral !== undefined) {
622
+ const latestIndex = latestDelegationCompletionIndexByRal.get(ral);
623
+ if (latestIndex !== undefined && latestIndex !== i) {
624
+ prunedDelegationCompletions += 1;
625
+ continue;
626
+ }
627
+ }
628
+
629
+ // Create truncation context for tool result processing
630
+ const truncationContext: TruncationContext = {
631
+ currentIndex: indexOffset + i,
632
+ totalMessages,
633
+ eventId: entry.eventId,
634
+ };
635
+
636
+ // Helper to check if entry should be included based on RAL visibility
637
+ const shouldInclude = (): boolean => {
638
+ // User messages (no RAL) - always include
639
+ if (!entry.ral) return true;
640
+
641
+ // Same agent
642
+ if (entry.pubkey === viewingAgentPubkey) {
643
+ if (entry.ral === ralNumber) return true; // Current RAL
644
+ if (activeRals.has(entry.ral)) return false; // Other active RAL - skip
645
+ return true; // Completed RAL
646
+ }
647
+
648
+ // Other agent's message - only include text content
649
+ return entry.messageType === "text" && !!entry.content;
650
+ };
651
+
652
+ if (!shouldInclude()) continue;
653
+
654
+ // TOOL-CALL: Add to result and register as pending
655
+ if (entry.messageType === "tool-call" && entry.toolData) {
656
+ const resultIndex = result.length;
657
+ result.push(await entryToMessage(entry, viewingAgentPubkey, truncationContext, agentPubkeys, imageTracker, agentsMdContext));
658
+
659
+ for (const part of entry.toolData as ToolCallPart[]) {
660
+ pendingToolCalls.set(part.toolCallId, {
661
+ toolName: part.toolName,
662
+ // Result should be inserted right after the tool-call message
663
+ resultIndex: resultIndex + 1,
664
+ });
665
+ }
666
+ continue;
667
+ }
668
+
669
+ // TOOL-RESULT: Add to result and mark tool-call as resolved
670
+ if (entry.messageType === "tool-result" && entry.toolData) {
671
+ result.push(await entryToMessage(entry, viewingAgentPubkey, truncationContext, agentPubkeys, imageTracker, agentsMdContext));
672
+
673
+ for (const part of entry.toolData as ToolResultPart[]) {
674
+ pendingToolCalls.delete(part.toolCallId);
675
+ }
676
+
677
+ // All tool-calls resolved - flush deferred messages now
678
+ if (pendingToolCalls.size === 0 && deferredMessages.length > 0) {
679
+ for (const deferred of deferredMessages) {
680
+ result.push(await entryToMessage(deferred.entry, viewingAgentPubkey, deferred.truncationContext, agentPubkeys, imageTracker, agentsMdContext, deferred.enableMultimodal));
681
+ }
682
+ deferredMessages.length = 0;
683
+ }
684
+ continue;
685
+ }
686
+
687
+ // DELEGATION-MARKER: Expand only if direct child of current conversation
688
+ if (entry.messageType === "delegation-marker" && entry.delegationMarker) {
689
+ const marker = entry.delegationMarker;
690
+
691
+ // Guard: conversationId must be present for marker expansion
692
+ // If missing, log a warning and skip - this indicates a bug in the caller
693
+ if (!ctx.conversationId) {
694
+ trace.getActiveSpan?.()?.addEvent("conversation.delegation_marker_skipped", {
695
+ "delegation.conversation_id": marker.delegationConversationId.substring(0, 12),
696
+ "delegation.parent_conversation_id": marker.parentConversationId.substring(0, 12),
697
+ "skip.reason": "missing_conversation_id",
698
+ "skip.severity": "warning",
699
+ });
700
+ continue;
701
+ }
702
+
703
+ // Only expand direct children - skip nested delegations
704
+ // This prevents exponential transcript bloat
705
+ if (marker.parentConversationId === ctx.conversationId) {
706
+ // Get delegation messages using the provided callback
707
+ const delegationMessages = ctx.getDelegationMessages?.(marker.delegationConversationId);
708
+ const expandedMessage = await expandDelegationMarker(marker, delegationMessages);
709
+
710
+ // CRITICAL: Delegation markers can arrive mid-tool-execution when a delegation
711
+ // completes while tools are running. We must defer them just like regular
712
+ // user messages to maintain tool-call/tool-result adjacency for AI SDK validation.
713
+ if (pendingToolCalls.size > 0) {
714
+ // Create a synthetic entry to defer the already-expanded ModelMessage
715
+ // We store the expanded content as a text entry for the deferred queue
716
+ // CRITICAL: Explicit role: "user" ensures consistency with expandDelegationMarker()
717
+ // which always returns user role. Without this, if recipientPubkey === viewingAgentPubkey
718
+ // (self-delegation), deriveRole() would incorrectly produce "assistant" role.
719
+ deferredMessages.push({
720
+ entry: {
721
+ pubkey: marker.recipientPubkey,
722
+ role: "user",
723
+ content: expandedMessage.content as string,
724
+ messageType: "text",
725
+ // Use the delegation marker's entry properties for context
726
+ eventId: entry.eventId,
727
+ ral: entry.ral,
728
+ },
729
+ truncationContext,
730
+ enableMultimodal: false,
731
+ });
732
+ } else {
733
+ result.push(expandedMessage);
734
+ }
735
+
736
+ trace.getActiveSpan?.()?.addEvent("conversation.delegation_marker_expanded", {
737
+ "delegation.conversation_id": marker.delegationConversationId.substring(0, 12),
738
+ "delegation.status": marker.status,
739
+ "delegation.transcript_found": !!delegationMessages,
740
+ "delegation.deferred": pendingToolCalls.size > 0,
741
+ });
742
+ } else {
743
+ // Nested delegation marker - show minimal reference only
744
+ // Don't expand transcript to avoid exponential bloat
745
+ const nestedMarkerMessage = await formatNestedDelegationMarker(marker);
746
+
747
+ // Same deferral logic for nested markers
748
+ // CRITICAL: Explicit role: "user" ensures consistency with formatNestedDelegationMarker()
749
+ // which always returns user role.
750
+ if (pendingToolCalls.size > 0) {
751
+ deferredMessages.push({
752
+ entry: {
753
+ pubkey: marker.recipientPubkey,
754
+ role: "user",
755
+ content: nestedMarkerMessage.content as string,
756
+ messageType: "text",
757
+ eventId: entry.eventId,
758
+ ral: entry.ral,
759
+ },
760
+ truncationContext,
761
+ enableMultimodal: false,
762
+ });
763
+ } else {
764
+ result.push(nestedMarkerMessage);
765
+ }
766
+
767
+ trace.getActiveSpan?.()?.addEvent("conversation.nested_delegation_marker_displayed", {
768
+ "delegation.conversation_id": marker.delegationConversationId.substring(0, 12),
769
+ "delegation.parent_conversation_id": marker.parentConversationId.substring(0, 12),
770
+ "current.conversation_id": ctx.conversationId.substring(0, 12),
771
+ "delegation.status": marker.status,
772
+ "delegation.recipient_pubkey": marker.recipientPubkey.substring(0, 12),
773
+ "delegation.deferred": pendingToolCalls.size > 0,
774
+ });
775
+ }
776
+ continue;
777
+ }
778
+
779
+ // NON-TOOL MESSAGE (user/assistant text, system, etc.)
780
+ // Only the most recent user message with images gets multimodal conversion
781
+ const enableMultimodal = i === lastUserImageEntryIndex;
782
+ // If tool-calls are pending, defer this message
783
+ if (pendingToolCalls.size > 0) {
784
+ deferredMessages.push({ entry, truncationContext, enableMultimodal });
785
+ } else {
786
+ result.push(await entryToMessage(entry, viewingAgentPubkey, truncationContext, agentPubkeys, imageTracker, agentsMdContext, enableMultimodal));
787
+ }
788
+ }
789
+
790
+ // ============================================================================
791
+ // ORPHANED TOOL-CALL RECONCILIATION
792
+ // ============================================================================
793
+ // If we reach the end of all entries and still have pending tool-calls,
794
+ // those are orphans - tool-calls whose results were never stored.
795
+ //
796
+ // We insert synthetic error results at the correct positions to maintain
797
+ // valid message structure for the AI SDK.
798
+ // ============================================================================
799
+ if (pendingToolCalls.size > 0) {
800
+ // Sort by resultIndex descending so splice operations don't shift indices
801
+ const orphans = Array.from(pendingToolCalls.entries())
802
+ .sort((a, b) => b[1].resultIndex - a[1].resultIndex);
803
+
804
+ for (const [toolCallId, info] of orphans) {
805
+ const syntheticResult: ModelMessage = {
806
+ role: "tool",
807
+ content: [{
808
+ type: "tool-result" as const,
809
+ toolCallId,
810
+ toolName: info.toolName,
811
+ output: {
812
+ type: "text" as const,
813
+ value: "[Error: Tool execution was interrupted - result unavailable]",
814
+ },
815
+ }] as ToolResultPart[],
816
+ };
817
+ // Insert synthetic result right after the tool-call
818
+ result.splice(info.resultIndex, 0, syntheticResult);
819
+ }
820
+
821
+ // Log for debugging/monitoring orphaned tool-calls in production
822
+ trace.getActiveSpan?.()?.addEvent("conversation.orphaned_tool_calls_reconciled", {
823
+ "orphan.count": pendingToolCalls.size,
824
+ "orphan.tool_call_ids": Array.from(pendingToolCalls.keys()).join(","),
825
+ });
826
+ }
827
+
828
+ // Flush any remaining deferred messages
829
+ for (const deferred of deferredMessages) {
830
+ result.push(await entryToMessage(deferred.entry, viewingAgentPubkey, deferred.truncationContext, agentPubkeys, imageTracker, agentsMdContext, deferred.enableMultimodal));
831
+ }
832
+
833
+ if (prunedDelegationCompletions > 0) {
834
+ trace.getActiveSpan?.()?.addEvent("conversation.delegation_completion_pruned", {
835
+ "delegation.pruned_count": prunedDelegationCompletions,
836
+ "delegation.kept_count": latestDelegationCompletionIndexByRal.size,
837
+ });
838
+ }
839
+
840
+ // Image placeholder telemetry: Aggregate replacement statistics
841
+ // The _imageReplacementStats field is set during entryToMessage for tool-result messages
842
+ let totalReplacedCount = 0;
843
+ let totalUniqueReplacedCount = 0;
844
+ for (const msg of result) {
845
+ const stats = (msg as unknown as { _imageReplacementStats?: { replacedCount: number; uniqueReplacedCount: number } })._imageReplacementStats;
846
+ if (stats) {
847
+ totalReplacedCount += stats.replacedCount;
848
+ totalUniqueReplacedCount += stats.uniqueReplacedCount;
849
+ // Clean up the internal field (don't send to API)
850
+ delete (msg as unknown as { _imageReplacementStats?: unknown })._imageReplacementStats;
851
+ }
852
+ }
853
+
854
+ // Only emit telemetry when actual replacements occurred
855
+ if (totalReplacedCount > 0) {
856
+ trace.getActiveSpan?.()?.addEvent("conversation.image_placeholder_applied", {
857
+ "image.replaced_count": totalReplacedCount, // Total occurrences replaced
858
+ "image.unique_replaced_count": totalUniqueReplacedCount, // Unique URLs that were replaced
859
+ "image.unique_urls_tracked": imageTracker.getSeenUrls().size, // All unique URLs seen (including first appearances)
860
+ // Estimated token savings: ~1,600 tokens per replacement
861
+ "image.estimated_tokens_saved": totalReplacedCount * 1600,
862
+ });
863
+ }
864
+
865
+ return result;
866
+ }