@tenex-chat/backend 0.9.5 → 0.9.6

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 (148) hide show
  1. package/README.md +5 -1
  2. package/dist/daemon-wrapper.cjs +47 -0
  3. package/dist/index.js +59268 -0
  4. package/dist/wrapper.js +171 -0
  5. package/package.json +19 -27
  6. package/src/agents/AgentRegistry.ts +9 -7
  7. package/src/agents/AgentStorage.ts +24 -1
  8. package/src/agents/agent-installer.ts +6 -0
  9. package/src/agents/agent-loader.ts +7 -2
  10. package/src/agents/constants.ts +10 -2
  11. package/src/agents/execution/AgentExecutor.ts +35 -6
  12. package/src/agents/execution/StreamCallbacks.ts +53 -13
  13. package/src/agents/execution/StreamExecutionHandler.ts +110 -16
  14. package/src/agents/execution/StreamSetup.ts +19 -9
  15. package/src/agents/execution/ToolEventHandlers.ts +112 -0
  16. package/src/agents/role-categories.ts +53 -0
  17. package/src/agents/types/runtime.ts +7 -0
  18. package/src/agents/types/storage.ts +7 -0
  19. package/src/commands/agent/import/openclaw-distiller.ts +63 -7
  20. package/src/commands/agent/import/openclaw-reader.ts +54 -0
  21. package/src/commands/agent/import/openclaw.ts +120 -29
  22. package/src/commands/agent/index.ts +83 -2
  23. package/src/commands/setup/display.ts +123 -0
  24. package/src/commands/setup/embed.ts +13 -13
  25. package/src/commands/setup/global-system-prompt.ts +15 -17
  26. package/src/commands/setup/image.ts +17 -20
  27. package/src/commands/setup/interactive.ts +37 -20
  28. package/src/commands/setup/llm.ts +12 -7
  29. package/src/commands/setup/onboarding.ts +1580 -248
  30. package/src/commands/setup/providers.ts +3 -3
  31. package/src/conversations/ConversationStore.ts +23 -2
  32. package/src/conversations/MessageBuilder.ts +51 -73
  33. package/src/conversations/formatters/utils/conversation-transcript-formatter.ts +425 -0
  34. package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +40 -98
  35. package/src/conversations/search/embeddings/ConversationIndexingJob.ts +40 -52
  36. package/src/conversations/services/ConversationSummarizer.ts +1 -2
  37. package/src/conversations/types.ts +11 -0
  38. package/src/daemon/Daemon.ts +78 -57
  39. package/src/daemon/ProjectRuntime.ts +6 -12
  40. package/src/daemon/SubscriptionManager.ts +13 -0
  41. package/src/daemon/index.ts +0 -1
  42. package/src/event-handler/index.ts +1 -0
  43. package/src/index.ts +20 -1
  44. package/src/llm/ChunkHandler.ts +1 -1
  45. package/src/llm/FinishHandler.ts +28 -4
  46. package/src/llm/LLMConfigEditor.ts +218 -106
  47. package/src/llm/index.ts +0 -4
  48. package/src/llm/meta/MetaModelResolver.ts +3 -18
  49. package/src/llm/middleware/message-sanitizer.ts +153 -0
  50. package/src/llm/providers/ollama-models.ts +0 -38
  51. package/src/llm/service.ts +50 -15
  52. package/src/llm/types.ts +0 -12
  53. package/src/llm/utils/ConfigurationManager.ts +88 -465
  54. package/src/llm/utils/ConfigurationTester.ts +42 -185
  55. package/src/llm/utils/ModelSelector.ts +156 -92
  56. package/src/llm/utils/ProviderConfigUI.ts +10 -141
  57. package/src/llm/utils/models-dev-cache.ts +102 -23
  58. package/src/llm/utils/provider-select-prompt.ts +284 -0
  59. package/src/llm/utils/provider-setup.ts +81 -34
  60. package/src/llm/utils/variant-list-prompt.ts +361 -0
  61. package/src/nostr/AgentEventDecoder.ts +1 -0
  62. package/src/nostr/AgentEventEncoder.ts +37 -0
  63. package/src/nostr/AgentProfilePublisher.ts +13 -0
  64. package/src/nostr/AgentPublisher.ts +26 -0
  65. package/src/nostr/kinds.ts +1 -0
  66. package/src/nostr/ndkClient.ts +4 -1
  67. package/src/nostr/types.ts +12 -0
  68. package/src/prompts/fragments/25-rag-instructions.ts +22 -21
  69. package/src/prompts/fragments/31-agents-md-guidance.ts +7 -21
  70. package/src/prompts/fragments/index.ts +2 -0
  71. package/src/prompts/utils/systemPromptBuilder.ts +18 -28
  72. package/src/services/AgentDefinitionMonitor.ts +8 -0
  73. package/src/services/ConfigService.ts +34 -0
  74. package/src/services/PubkeyService.ts +7 -1
  75. package/src/services/compression/CompressionService.ts +133 -74
  76. package/src/services/compression/compression-utils.ts +110 -19
  77. package/src/services/config/types.ts +0 -6
  78. package/src/services/dispatch/AgentDispatchService.ts +79 -0
  79. package/src/services/intervention/InterventionService.ts +78 -5
  80. package/src/services/nip46/Nip46SigningService.ts +30 -1
  81. package/src/services/projects/ProjectContext.ts +8 -6
  82. package/src/services/rag/RAGCollectionRegistry.ts +199 -0
  83. package/src/services/rag/RAGDatabaseService.ts +2 -7
  84. package/src/services/rag/RAGOperations.ts +25 -45
  85. package/src/services/rag/RAGService.ts +0 -31
  86. package/src/services/rag/RagSubscriptionService.ts +71 -122
  87. package/src/services/rag/rag-utils.ts +13 -0
  88. package/src/services/ral/RALRegistry.ts +25 -184
  89. package/src/services/reports/ReportEmbeddingService.ts +63 -113
  90. package/src/services/search/UnifiedSearchService.ts +115 -4
  91. package/src/services/search/index.ts +1 -0
  92. package/src/services/search/projectFilter.ts +20 -4
  93. package/src/services/search/providers/ConversationSearchProvider.ts +1 -0
  94. package/src/services/search/providers/GenericCollectionSearchProvider.ts +81 -0
  95. package/src/services/search/providers/LessonSearchProvider.ts +1 -8
  96. package/src/services/search/providers/ReportSearchProvider.ts +1 -0
  97. package/src/services/search/types.ts +24 -3
  98. package/src/services/trust-pubkeys/SystemPubkeyListService.ts +148 -0
  99. package/src/services/trust-pubkeys/TrustPubkeyService.ts +70 -9
  100. package/src/telemetry/setup.ts +2 -13
  101. package/src/tools/implementations/ask.ts +3 -3
  102. package/src/tools/implementations/conversation_get.ts +28 -268
  103. package/src/tools/implementations/fs_grep.ts +6 -6
  104. package/src/tools/implementations/fs_read.ts +2 -0
  105. package/src/tools/implementations/fs_write.ts +2 -0
  106. package/src/tools/implementations/learn.ts +38 -50
  107. package/src/tools/implementations/rag_add_documents.ts +6 -4
  108. package/src/tools/implementations/rag_create_collection.ts +37 -4
  109. package/src/tools/implementations/rag_delete_collection.ts +9 -0
  110. package/src/tools/implementations/{search.ts → rag_search.ts} +31 -25
  111. package/src/tools/registry.ts +7 -8
  112. package/src/tools/types.ts +11 -2
  113. package/src/tools/utils/transcript-args.ts +13 -0
  114. package/src/utils/cli-theme.ts +13 -0
  115. package/src/utils/logger.ts +55 -0
  116. package/src/utils/metadataKeys.ts +17 -0
  117. package/src/utils/sqlEscaping.ts +39 -0
  118. package/src/wrapper.ts +7 -3
  119. package/dist/src/index.js +0 -46790
  120. package/dist/tenex-backend-wrapper.cjs +0 -3
  121. package/src/agents/execution/constants.ts +0 -16
  122. package/src/agents/execution/index.ts +0 -3
  123. package/src/agents/index.ts +0 -4
  124. package/src/commands/agent.ts +0 -235
  125. package/src/conversations/formatters/DelegationXmlFormatter.ts +0 -64
  126. package/src/conversations/formatters/index.ts +0 -9
  127. package/src/conversations/index.ts +0 -2
  128. package/src/conversations/utils/content-utils.ts +0 -69
  129. package/src/daemon/UnixSocketTransport.ts +0 -318
  130. package/src/event-handler/newConversation.ts +0 -165
  131. package/src/events/NDKProjectStatus.ts +0 -384
  132. package/src/events/index.ts +0 -4
  133. package/src/lib/json-parser.ts +0 -30
  134. package/src/llm/RecordingState.ts +0 -37
  135. package/src/llm/StreamPublisher.ts +0 -40
  136. package/src/llm/middleware/flight-recorder.ts +0 -188
  137. package/src/llm/utils/claudeCodePromptCompiler.ts +0 -141
  138. package/src/nostr/constants.ts +0 -38
  139. package/src/prompts/core/index.ts +0 -3
  140. package/src/prompts/index.ts +0 -21
  141. package/src/services/image/index.ts +0 -12
  142. package/src/services/status/index.ts +0 -11
  143. package/src/telemetry/diagnostics.ts +0 -27
  144. package/src/tools/implementations/rag_query.ts +0 -107
  145. package/src/types/index.ts +0 -46
  146. package/src/utils/agentFetcher.ts +0 -107
  147. package/src/utils/conversation-utils.ts +0 -1
  148. package/src/utils/process.ts +0 -49
@@ -1,318 +0,0 @@
1
- import * as net from "net";
2
- import * as fs from "fs";
3
- import * as path from "path";
4
- import type { StreamTransport } from "@/llm";
5
- import type { LocalStreamChunk } from "@/llm";
6
- import { logger } from "@/utils/logger";
7
-
8
- const LOG_PREFIX = "[UnixSocketTransport]";
9
-
10
- /** Extract a brief caller context from stack trace for debugging (only computed when debug logging enabled) */
11
- function getCallerContext(): string | undefined {
12
- // Only compute stack trace if debug logging is actually enabled
13
- // This avoids the expensive stack capture in production
14
- if (!logger.isLevelEnabled?.("debug")) {
15
- return undefined;
16
- }
17
- const stack = new Error().stack;
18
- if (!stack) return undefined;
19
- // Skip first 3 lines: Error, getCallerContext, cleanup/caller
20
- const lines = stack.split("\n").slice(3, 6);
21
- return lines.map((l) => l.trim()).join(" <- ");
22
- }
23
-
24
- /** Result of checking socket file info */
25
- type SocketFileInfoResult =
26
- | { status: "ok"; exists: true; isSocket: boolean; isFile: boolean; mode: string; mtime: string; age: string }
27
- | { status: "ok"; exists: false }
28
- | { status: "error"; error: string };
29
-
30
- /**
31
- * Unix domain socket transport for local streaming
32
- * Single client connection model
33
- */
34
- export class UnixSocketTransport implements StreamTransport {
35
- private server: net.Server | null = null;
36
- private client: net.Socket | null = null;
37
- private socketPath: string;
38
-
39
- constructor(socketPath?: string) {
40
- this.socketPath = socketPath ?? this.defaultSocketPath();
41
- }
42
-
43
- private defaultSocketPath(): string {
44
- const runtimeDir = process.env.XDG_RUNTIME_DIR;
45
- if (runtimeDir) {
46
- return path.join(runtimeDir, "tenex-stream.sock");
47
- }
48
- return "/tmp/tenex-stream.sock";
49
- }
50
-
51
- async start(): Promise<void> {
52
- logger.info(`${LOG_PREFIX} start() called`, {
53
- socketPath: this.socketPath,
54
- });
55
-
56
- // Check for existing socket and log details
57
- const existingSocketInfo = this.getSocketFileInfo();
58
- if (existingSocketInfo.status === "ok" && existingSocketInfo.exists) {
59
- logger.warn(`${LOG_PREFIX} Stale socket found before start`, {
60
- socketPath: this.socketPath,
61
- isSocket: existingSocketInfo.isSocket,
62
- age: existingSocketInfo.age,
63
- });
64
- } else if (existingSocketInfo.status === "error") {
65
- logger.error(`${LOG_PREFIX} Failed to check socket file before start`, {
66
- socketPath: this.socketPath,
67
- error: existingSocketInfo.error,
68
- });
69
- }
70
-
71
- // Clean up stale socket, passing the file info we already have
72
- this.cleanup("start() - cleaning stale socket before creating new one", existingSocketInfo);
73
-
74
- return new Promise((resolve, reject) => {
75
- this.server = net.createServer((socket) => {
76
- logger.info(`${LOG_PREFIX} Client connected to streaming socket`, {
77
- socketPath: this.socketPath,
78
- });
79
-
80
- // Only one client at a time
81
- if (this.client) {
82
- logger.warn(`${LOG_PREFIX} Replacing existing client connection`, {
83
- socketPath: this.socketPath,
84
- });
85
- this.client.destroy();
86
- }
87
-
88
- this.client = socket;
89
-
90
- socket.on("close", () => {
91
- logger.info(`${LOG_PREFIX} Client disconnected from streaming socket`, {
92
- socketPath: this.socketPath,
93
- });
94
- if (this.client === socket) {
95
- this.client = null;
96
- }
97
- });
98
-
99
- socket.on("error", (err) => {
100
- logger.error(`${LOG_PREFIX} Socket client error`, {
101
- error: err.message,
102
- socketPath: this.socketPath,
103
- });
104
- if (this.client === socket) {
105
- this.client = null;
106
- }
107
- });
108
- });
109
-
110
- this.server.on("error", (err) => {
111
- logger.error(`${LOG_PREFIX} Socket server error`, {
112
- error: err.message,
113
- socketPath: this.socketPath,
114
- });
115
- reject(err);
116
- });
117
-
118
- this.server.listen(this.socketPath, () => {
119
- logger.info(`${LOG_PREFIX} Streaming socket started and listening`, {
120
- socketPath: this.socketPath,
121
- });
122
- resolve();
123
- });
124
- });
125
- }
126
-
127
- write(chunk: LocalStreamChunk): void {
128
- if (!this.client) return;
129
-
130
- try {
131
- const line = JSON.stringify(chunk) + "\n";
132
- this.client.write(line);
133
- } catch (err) {
134
- logger.error(`${LOG_PREFIX} Failed to write chunk`, {
135
- error: err instanceof Error ? err.message : String(err),
136
- socketPath: this.socketPath,
137
- });
138
- }
139
- }
140
-
141
- isConnected(): boolean {
142
- return this.client !== null && !this.client.destroyed;
143
- }
144
-
145
- async stop(): Promise<void> {
146
- const callerContext = getCallerContext();
147
- logger.info(`${LOG_PREFIX} stop() called`, {
148
- socketPath: this.socketPath,
149
- hasClient: !!this.client,
150
- hasServer: !!this.server,
151
- ...(callerContext && { callerContext }),
152
- });
153
-
154
- if (this.client) {
155
- logger.info(`${LOG_PREFIX} Destroying client connection in stop()`, {
156
- socketPath: this.socketPath,
157
- });
158
- this.client.destroy();
159
- this.client = null;
160
- }
161
-
162
- return new Promise((resolve) => {
163
- if (this.server) {
164
- logger.info(`${LOG_PREFIX} Closing server in stop()`, {
165
- socketPath: this.socketPath,
166
- });
167
- this.server.close(() => {
168
- logger.info(`${LOG_PREFIX} Server closed, now calling cleanup()`, {
169
- socketPath: this.socketPath,
170
- });
171
- this.cleanup("stop() - server closed callback");
172
- logger.info(`${LOG_PREFIX} stop() completed`, {
173
- socketPath: this.socketPath,
174
- });
175
- resolve();
176
- });
177
- } else {
178
- logger.info(`${LOG_PREFIX} stop() called but no server to close`, {
179
- socketPath: this.socketPath,
180
- });
181
- resolve();
182
- }
183
- });
184
- }
185
-
186
- /**
187
- * Get file info for the socket path (for debugging)
188
- * Returns discriminated result: ok (exists/not), or error
189
- */
190
- private getSocketFileInfo(): SocketFileInfoResult {
191
- try {
192
- const stats = fs.lstatSync(this.socketPath);
193
- const ageMs = Date.now() - stats.mtime.getTime();
194
- return {
195
- status: "ok",
196
- exists: true,
197
- isSocket: stats.isSocket(),
198
- isFile: stats.isFile(),
199
- mode: stats.mode.toString(8),
200
- mtime: stats.mtime.toISOString(),
201
- age: `${Math.floor(ageMs / 1000)}s`,
202
- };
203
- } catch (err) {
204
- // ENOENT means file doesn't exist - that's a valid "missing" result
205
- if (err instanceof Error && "code" in err && (err as NodeJS.ErrnoException).code === "ENOENT") {
206
- return { status: "ok", exists: false };
207
- }
208
- // Any other error (permission, IO, etc.) is an actual error
209
- return {
210
- status: "error",
211
- error: err instanceof Error ? err.message : String(err),
212
- };
213
- }
214
- }
215
-
216
- /**
217
- * Check if socket exists and is healthy
218
- */
219
- checkSocketHealth(): {
220
- exists: boolean;
221
- isSocket: boolean;
222
- serverRunning: boolean;
223
- clientConnected: boolean;
224
- } {
225
- const info = this.getSocketFileInfo();
226
- const exists = info.status === "ok" && info.exists;
227
- const isSocket = info.status === "ok" && info.exists && info.isSocket;
228
- const result = {
229
- exists,
230
- isSocket,
231
- serverRunning: this.server !== null && this.server.listening,
232
- clientConnected: this.isConnected(),
233
- };
234
- logger.debug(`${LOG_PREFIX} Health check`, {
235
- socketPath: this.socketPath,
236
- ...result,
237
- });
238
- return result;
239
- }
240
-
241
- /**
242
- * Clean up socket file if it exists and is safe to remove
243
- * @param reason - Why cleanup is being called (for logging)
244
- * @param fileInfo - Optional pre-fetched file info to avoid redundant FS calls
245
- */
246
- private cleanup(reason: string, fileInfo?: SocketFileInfoResult): void {
247
- const callerContext = getCallerContext();
248
-
249
- logger.info(`${LOG_PREFIX} cleanup() called`, {
250
- socketPath: this.socketPath,
251
- reason,
252
- ...(callerContext && { callerContext }),
253
- });
254
-
255
- // Use provided file info or fetch it
256
- const info = fileInfo ?? this.getSocketFileInfo();
257
-
258
- // Handle error case
259
- if (info.status === "error") {
260
- logger.error(`${LOG_PREFIX} cleanup() cannot proceed - failed to check socket file`, {
261
- socketPath: this.socketPath,
262
- reason,
263
- error: info.error,
264
- });
265
- return;
266
- }
267
-
268
- // Nothing to clean up
269
- if (!info.exists) {
270
- logger.debug(`${LOG_PREFIX} cleanup() - socket does not exist, nothing to unlink`, {
271
- socketPath: this.socketPath,
272
- reason,
273
- });
274
- return;
275
- }
276
-
277
- // CRITICAL SAFETY CHECK: Only unlink if it's actually a socket
278
- if (!info.isSocket) {
279
- logger.error(`${LOG_PREFIX} cleanup() REFUSED to unlink - path is not a socket`, {
280
- socketPath: this.socketPath,
281
- reason,
282
- isFile: info.isFile,
283
- mode: info.mode,
284
- });
285
- return;
286
- }
287
-
288
- // Safe to unlink - it's a socket
289
- try {
290
- logger.info(`${LOG_PREFIX} About to unlink socket`, {
291
- socketPath: this.socketPath,
292
- reason,
293
- age: info.age,
294
- });
295
-
296
- fs.unlinkSync(this.socketPath);
297
-
298
- logger.info(`${LOG_PREFIX} Socket unlinked successfully`, {
299
- socketPath: this.socketPath,
300
- reason,
301
- });
302
- } catch (err) {
303
- const errorMessage = err instanceof Error ? err.message : String(err);
304
- const errorCode = err instanceof Error && "code" in err ? (err as NodeJS.ErrnoException).code : undefined;
305
-
306
- logger.error(`${LOG_PREFIX} cleanup() failed to unlink socket`, {
307
- socketPath: this.socketPath,
308
- reason,
309
- error: errorMessage,
310
- errorCode,
311
- });
312
- }
313
- }
314
-
315
- getSocketPath(): string {
316
- return this.socketPath;
317
- }
318
- }
@@ -1,165 +0,0 @@
1
- import type { NDKEvent } from "@nostr-dev-kit/ndk";
2
- import { NDKArticle } from "@nostr-dev-kit/ndk";
3
- import { trace } from "@opentelemetry/api";
4
- import chalk from "chalk";
5
- import type { AgentExecutor } from "../agents/execution/AgentExecutor";
6
- import { createExecutionContext } from "../agents/execution/ExecutionContextFactory";
7
- import { ConversationStore } from "../conversations/ConversationStore";
8
- import type { ConversationMetadata } from "../conversations/types";
9
- import { AgentEventDecoder } from "../nostr/AgentEventDecoder";
10
- import { getNDK } from "../nostr/ndkClient";
11
- import { TagExtractor } from "../nostr/TagExtractor";
12
- import { getProjectContext } from "@/services/projects";
13
- import { formatAnyError } from "@/lib/error-formatter";
14
- import { logger } from "../utils/logger";
15
- import { AgentRouter } from "@/services/dispatch/AgentRouter";
16
-
17
- /**
18
- * Fetch a kind 30023 (NDKArticle) from an a-tag reference.
19
- * @param aTagValue - The a-tag value in format "30023:pubkey:d-tag"
20
- * @returns The article metadata or null if not found
21
- */
22
- async function fetchReferencedArticle(
23
- aTagValue: string
24
- ): Promise<ConversationMetadata["referencedArticle"] | null> {
25
- try {
26
- const parts = aTagValue.split(":");
27
- if (parts.length < 3 || parts[0] !== "30023") {
28
- return null;
29
- }
30
-
31
- const [, pubkey, ...dTagParts] = parts;
32
- const dTag = dTagParts.join(":"); // Handle d-tags that contain colons
33
-
34
- const ndk = getNDK();
35
- const filter = {
36
- kinds: [30023],
37
- authors: [pubkey],
38
- "#d": [dTag],
39
- };
40
-
41
- const events = await ndk.fetchEvents(filter);
42
- if (events.size === 0) {
43
- logger.debug(chalk.yellow(`Referenced article not found: ${aTagValue}`));
44
- return null;
45
- }
46
-
47
- const event = Array.from(events)[0];
48
- const article = NDKArticle.from(event);
49
-
50
- logger.info(chalk.cyan(`📄 Fetched referenced article: "${article.title || dTag}"`));
51
-
52
- return {
53
- title: article.title || dTag,
54
- content: article.content || "",
55
- dTag,
56
- };
57
- } catch (error) {
58
- logger.debug(chalk.yellow(`Failed to fetch referenced article: ${formatAnyError(error)}`));
59
- return null;
60
- }
61
- }
62
-
63
- /**
64
- * Extract and fetch the first kind 30023 article reference from an event's a-tags.
65
- * @param event - The event to extract article references from
66
- * @returns The article metadata or null if none found
67
- */
68
- async function extractReferencedArticle(
69
- event: NDKEvent
70
- ): Promise<ConversationMetadata["referencedArticle"] | null> {
71
- const aTags = TagExtractor.getATags(event);
72
-
73
- // Find the first a-tag referencing a kind 30023 (article)
74
- const articleATag = aTags.find((tag) => tag.startsWith("30023:"));
75
- if (!articleATag) {
76
- return null;
77
- }
78
-
79
- return fetchReferencedArticle(articleATag);
80
- }
81
-
82
- interface EventHandlerContext {
83
- agentExecutor: AgentExecutor;
84
- /**
85
- * Project directory (normal git repository root).
86
- * Worktrees are in .worktrees/ subdirectory.
87
- */
88
- projectBasePath: string;
89
- }
90
-
91
- export const handleNewConversation = async (
92
- event: NDKEvent,
93
- context: EventHandlerContext
94
- ): Promise<void> => {
95
- try {
96
- // Create conversation
97
- const conversation = await ConversationStore.create(event);
98
-
99
- // Check for referenced kind 30023 articles and populate metadata
100
- const referencedArticle = await extractReferencedArticle(event);
101
- if (referencedArticle) {
102
- conversation.updateMetadata({ referencedArticle });
103
- await conversation.save();
104
-
105
- const activeSpan = trace.getActiveSpan();
106
- if (activeSpan) {
107
- activeSpan.addEvent("referenced_article_loaded", {
108
- "article.title": referencedArticle.title,
109
- "article.dTag": referencedArticle.dTag,
110
- "article.content_length": referencedArticle.content.length,
111
- });
112
- }
113
- }
114
-
115
- // Get project context
116
- const projectCtx = getProjectContext();
117
-
118
- // Use AgentRouter to resolve target agents (includes project validation for global agents)
119
- const targetAgents = AgentRouter.resolveTargetAgents(event, projectCtx);
120
-
121
- // Add telemetry for routing decision
122
- const activeSpan = trace.getActiveSpan();
123
- if (activeSpan) {
124
- const mentionedPubkeys = AgentEventDecoder.getMentionedPubkeys(event);
125
- activeSpan.addEvent("agent_routing", {
126
- "routing.mentioned_pubkeys_count": mentionedPubkeys.length,
127
- "routing.resolved_agent_count": targetAgents.length,
128
- "routing.agent_names": targetAgents.map((a) => a.name).join(", "),
129
- "routing.agent_roles": targetAgents.map((a) => a.role).join(", "),
130
- });
131
- }
132
-
133
- // If no valid agents found (filtered by project context), return
134
- if (targetAgents.length === 0) {
135
- logger.info(
136
- chalk.gray(
137
- "New conversation - no valid agents to route to (may have been filtered by project context)"
138
- )
139
- );
140
- if (activeSpan) {
141
- activeSpan.addEvent("agent_routing_failed", { reason: "no_agents_resolved" });
142
- }
143
- return;
144
- }
145
-
146
- // Use first agent for new conversation
147
- const targetAgent = targetAgents[0];
148
-
149
- // Create execution context (new conversations don't have branch tags)
150
- const executionContext = await createExecutionContext({
151
- agent: targetAgent,
152
- conversationId: conversation.id,
153
- projectBasePath: context.projectBasePath,
154
- triggeringEvent: event,
155
- mcpManager: projectCtx.mcpManager,
156
- });
157
-
158
- // Execute with the appropriate agent
159
- await context.agentExecutor.execute(executionContext);
160
-
161
- logger.info(chalk.green("✅ Conversation routed successfully"));
162
- } catch (error) {
163
- logger.info(chalk.red(`❌ Failed to route conversation: ${formatAnyError(error)}`));
164
- }
165
- };