@tenex-chat/backend 0.9.5 → 0.9.7

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 +34 -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
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // src/wrapper.ts
4
+ import { spawn } from "node:child_process";
5
+ import { existsSync } from "node:fs";
6
+ import * as path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ var __filename = fileURLToPath(import.meta.url);
9
+ var __dirname = path.dirname(__filename);
10
+ function resolveEntryPoint() {
11
+ const srcIndex = path.join(__dirname, "index.ts");
12
+ if (existsSync(srcIndex)) {
13
+ return srcIndex;
14
+ }
15
+ const distIndex = path.join(__dirname, "index.js");
16
+ if (existsSync(distIndex)) {
17
+ return distIndex;
18
+ }
19
+ const parentSrcIndex = path.join(path.dirname(__dirname), "src", "index.ts");
20
+ if (existsSync(parentSrcIndex)) {
21
+ return parentSrcIndex;
22
+ }
23
+ const parentDistIndex = path.join(path.dirname(__dirname), "dist", "index.js");
24
+ if (existsSync(parentDistIndex)) {
25
+ return parentDistIndex;
26
+ }
27
+ return srcIndex;
28
+ }
29
+ var MIN_UPTIME_MS = 5e3;
30
+ var MAX_CRASHES = 5;
31
+ var CRASH_WINDOW_MS = 6e4;
32
+ var DaemonWrapper = class {
33
+ child = null;
34
+ crashHistory = [];
35
+ startTime = 0;
36
+ isShuttingDown = false;
37
+ /**
38
+ * Start the daemon wrapper
39
+ */
40
+ async start(args2) {
41
+ console.log("[Wrapper] Starting TENEX daemon supervisor");
42
+ console.log("[Wrapper] Daemon arguments:", args2.join(" ") || "(none)");
43
+ this.setupSignalHandlers();
44
+ await this.runDaemonLoop(args2);
45
+ }
46
+ /**
47
+ * Setup signal handlers for the wrapper process
48
+ */
49
+ setupSignalHandlers() {
50
+ process.on("SIGHUP", () => {
51
+ if (this.child && this.child.pid) {
52
+ console.log("[Wrapper] SIGHUP received - forwarding to daemon");
53
+ this.child.kill("SIGHUP");
54
+ }
55
+ });
56
+ const handleTermination = (signal) => {
57
+ if (this.isShuttingDown) {
58
+ console.log(`[Wrapper] ${signal} received during shutdown - forcing exit`);
59
+ process.exit(1);
60
+ }
61
+ console.log(`[Wrapper] ${signal} received - shutting down`);
62
+ this.isShuttingDown = true;
63
+ if (this.child && this.child.pid) {
64
+ this.child.kill(signal === "SIGTERM" ? "SIGTERM" : "SIGINT");
65
+ setTimeout(() => {
66
+ if (this.child && this.child.pid) {
67
+ console.log("[Wrapper] Child still running after timeout - forcing kill");
68
+ this.child.kill("SIGKILL");
69
+ }
70
+ process.exit(1);
71
+ }, 3e4);
72
+ } else {
73
+ process.exit(0);
74
+ }
75
+ };
76
+ process.on("SIGTERM", () => handleTermination("SIGTERM"));
77
+ process.on("SIGINT", () => handleTermination("SIGINT"));
78
+ }
79
+ /**
80
+ * Main daemon loop - spawn daemon and respawn on clean exit
81
+ */
82
+ async runDaemonLoop(args2) {
83
+ while (!this.isShuttingDown) {
84
+ if (this.isInCrashLoop()) {
85
+ console.error("[Wrapper] Crash loop detected - too many crashes in short period");
86
+ console.error(`[Wrapper] ${this.crashHistory.length} crashes in last ${CRASH_WINDOW_MS / 1e3}s`);
87
+ console.error("[Wrapper] Giving up - please check daemon logs");
88
+ process.exit(1);
89
+ }
90
+ this.startTime = Date.now();
91
+ const exitCode = await this.spawnDaemon(args2);
92
+ const uptime = Date.now() - this.startTime;
93
+ console.log(`[Wrapper] Daemon exited with code ${exitCode} after ${Math.round(uptime / 1e3)}s`);
94
+ if (this.isShuttingDown) {
95
+ console.log("[Wrapper] Wrapper shutting down, not respawning");
96
+ break;
97
+ }
98
+ if (exitCode === 0) {
99
+ console.log("[Wrapper] Daemon exited cleanly - respawning for graceful restart");
100
+ if (uptime >= MIN_UPTIME_MS) {
101
+ this.crashHistory = [];
102
+ }
103
+ await this.sleep(100);
104
+ } else {
105
+ console.error(`[Wrapper] Daemon crashed with exit code ${exitCode}`);
106
+ this.recordCrash();
107
+ const backoffMs = Math.min(1e3 * Math.pow(2, this.crashHistory.length - 1), 3e4);
108
+ console.log(`[Wrapper] Waiting ${backoffMs / 1e3}s before respawn...`);
109
+ await this.sleep(backoffMs);
110
+ }
111
+ }
112
+ console.log("[Wrapper] Exiting");
113
+ process.exit(0);
114
+ }
115
+ /**
116
+ * Spawn the daemon process and wait for it to exit
117
+ */
118
+ spawnDaemon(args2) {
119
+ return new Promise((resolve) => {
120
+ const indexPath = resolveEntryPoint();
121
+ const daemonArgs = [indexPath, "daemon", "--supervised", ...args2];
122
+ const runtimeBinary = indexPath.endsWith(".ts") ? process.env.TENEX_BUN_BIN || "bun" : process.execPath;
123
+ console.log(`[Wrapper] Spawning: ${runtimeBinary} ${daemonArgs.join(" ")}`);
124
+ this.child = spawn(runtimeBinary, daemonArgs, {
125
+ stdio: "inherit",
126
+ env: process.env
127
+ });
128
+ this.child.on("exit", (code) => {
129
+ this.child = null;
130
+ resolve(code ?? 1);
131
+ });
132
+ this.child.on("error", (error) => {
133
+ console.error("[Wrapper] Failed to spawn daemon:", error.message);
134
+ this.child = null;
135
+ resolve(1);
136
+ });
137
+ });
138
+ }
139
+ /**
140
+ * Record a crash for crash loop detection
141
+ */
142
+ recordCrash() {
143
+ const now = Date.now();
144
+ this.crashHistory.push({ timestamp: now });
145
+ this.crashHistory = this.crashHistory.filter(
146
+ (c) => now - c.timestamp < CRASH_WINDOW_MS
147
+ );
148
+ }
149
+ /**
150
+ * Check if we're in a crash loop
151
+ */
152
+ isInCrashLoop() {
153
+ const now = Date.now();
154
+ const recentCrashes = this.crashHistory.filter(
155
+ (c) => now - c.timestamp < CRASH_WINDOW_MS
156
+ );
157
+ return recentCrashes.length >= MAX_CRASHES;
158
+ }
159
+ /**
160
+ * Sleep for a given duration
161
+ */
162
+ sleep(ms) {
163
+ return new Promise((resolve) => setTimeout(resolve, ms));
164
+ }
165
+ };
166
+ var wrapper = new DaemonWrapper();
167
+ var args = process.argv.slice(2);
168
+ wrapper.start(args).catch((error) => {
169
+ console.error("[Wrapper] Fatal error:", error);
170
+ process.exit(1);
171
+ });
package/package.json CHANGED
@@ -1,17 +1,9 @@
1
1
  {
2
2
  "name": "@tenex-chat/backend",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "TENEX Command Line Interface",
5
- "main": "./dist/index.js",
6
- "types": "./dist/index.d.ts",
7
- "exports": {
8
- ".": {
9
- "import": "./dist/index.js",
10
- "types": "./dist/index.d.ts"
11
- }
12
- },
13
5
  "bin": {
14
- "tenex-backend": "./dist/tenex-backend-wrapper.cjs"
6
+ "tenex-backend": "./dist/daemon-wrapper.cjs"
15
7
  },
16
8
  "files": [
17
9
  "dist/**/*.js",
@@ -54,23 +46,23 @@
54
46
  "registry": "https://registry.npmjs.org/"
55
47
  },
56
48
  "dependencies": {
57
- "@ai-sdk/anthropic": "^3.0.46",
58
- "@ai-sdk/openai": "^3.0.30",
49
+ "@ai-sdk/anthropic": "^3.0.58",
50
+ "@ai-sdk/openai": "^3.0.41",
59
51
  "@huggingface/transformers": "^3.8.1",
60
52
  "@lancedb/lancedb": "^0.26.2",
61
- "@modelcontextprotocol/sdk": "^1.26.0",
62
- "@nostr-dev-kit/ndk": "3.0.0",
63
- "@openrouter/ai-sdk-provider": "^2.1.1",
53
+ "@modelcontextprotocol/sdk": "^1.27.1",
54
+ "@nostr-dev-kit/ndk": "3.0.3",
55
+ "@openrouter/ai-sdk-provider": "^2.2.5",
64
56
  "@opentelemetry/api": "^1.9.0",
65
- "@opentelemetry/auto-instrumentations-node": "^0.70.0",
66
- "@opentelemetry/exporter-metrics-otlp-http": "^0.212.0",
67
- "@opentelemetry/exporter-trace-otlp-http": "^0.212.0",
68
- "@opentelemetry/resources": "^2.3.0",
69
- "@opentelemetry/sdk-node": "^0.212.0",
70
- "@opentelemetry/sdk-trace-node": "^2.3.0",
71
- "@opentelemetry/semantic-conventions": "^1.38.0",
72
- "ai": "^6.0.65",
73
- "ai-sdk-provider-claude-code": "*",
57
+ "@opentelemetry/auto-instrumentations-node": "^0.71.0",
58
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.213.0",
59
+ "@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
60
+ "@opentelemetry/resources": "^2.6.0",
61
+ "@opentelemetry/sdk-node": "^0.213.0",
62
+ "@opentelemetry/sdk-trace-node": "^2.6.0",
63
+ "@opentelemetry/semantic-conventions": "^1.40.0",
64
+ "ai": "^6.0.116",
65
+ "ai-sdk-provider-claude-code": "3.4.3",
74
66
  "ai-sdk-provider-codex-app-server": "1.1.7",
75
67
  "chalk": "^5.6.2",
76
68
  "commander": "^14.0.3",
@@ -81,7 +73,7 @@
81
73
  "lodash": "^4.17.23",
82
74
  "node-cron": "^4.2.1",
83
75
  "node-html-markdown": "^2.0.0",
84
- "nostr-tools": "^2.15.0",
76
+ "nostr-tools": "^2.23.3",
85
77
  "ollama-ai-provider-v2": "^3.0.3",
86
78
  "tseep": "^1.3.1",
87
79
  "zod": "^4.3.6"
@@ -90,10 +82,10 @@
90
82
  "@eslint/js": "^10.0.1",
91
83
  "esbuild": "^0.27.2",
92
84
  "esbuild-plugin-alias": "^0.2.1",
93
- "eslint": "^10.0.0",
85
+ "eslint": "^10.0.3",
94
86
  "husky": "^9.1.7",
95
87
  "typescript": "^5.9.3",
96
- "typescript-eslint": "^8.53.0",
88
+ "typescript-eslint": "^8.56.1",
97
89
  "vitest": "^4.0.16"
98
90
  },
99
91
  "type": "module",
@@ -141,18 +141,20 @@ export class AgentRegistry {
141
141
  }
142
142
  }
143
143
 
144
- // Check if critical agents failed
144
+ // Log failed agents but don't block project boot
145
145
  if (failedAgents.length > 0) {
146
- // PM is the first agent in tags
147
146
  const pmEventId = agentEventIds[0];
148
147
  if (failedAgents.includes(pmEventId)) {
149
- throw new Error(
150
- `Critical agent failed to load. Agent event ID ${pmEventId} could not be fetched. This might be due to network issues or the event not being available on the configured relays.`
148
+ logger.error(
149
+ `PM agent (first in tags) failed to load — project will boot without a PM. Agent event ID ${pmEventId} could not be fetched. This might be due to network issues or the event not being available on the configured relays.`,
150
+ { pmEventId, failedAgents }
151
+ );
152
+ } else {
153
+ logger.warn(
154
+ `${failedAgents.length} agent(s) could not be installed but continuing with available agents`,
155
+ { failedAgents }
151
156
  );
152
157
  }
153
- logger.warn(
154
- `${failedAgents.length} agent(s) could not be installed but continuing with available agents`
155
- );
156
158
  }
157
159
 
158
160
  // Load locally-associated agents from storage
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import type { StoredAgentData, ProjectScopedConfig, AgentDefaultConfig, AgentProjectConfig } from "@/agents/types";
4
+ import type { AgentCategory } from "@/agents/role-categories";
4
5
  import type { MCPServerConfig } from "@/llm/providers/types";
5
6
  import {
6
7
  resolveEffectiveConfig,
@@ -100,6 +101,7 @@ export function createStoredAgent(config: {
100
101
  slug: string;
101
102
  name: string;
102
103
  role: string;
104
+ category?: AgentCategory;
103
105
  description?: string | null;
104
106
  instructions?: string | null;
105
107
  useCriteria?: string | null;
@@ -118,6 +120,7 @@ export function createStoredAgent(config: {
118
120
  slug: config.slug,
119
121
  name: config.name,
120
122
  role: config.role,
123
+ category: config.category,
121
124
  description: config.description ?? undefined,
122
125
  instructions: config.instructions ?? undefined,
123
126
  useCriteria: config.useCriteria ?? undefined,
@@ -658,7 +661,7 @@ export class AgentStorage {
658
661
 
659
662
  /**
660
663
  * Get agent by slug (uses index for O(1) lookup).
661
- * Returns the agent matching this slug regardless of project.
664
+ * Returns the agent regardless of project assignment.
662
665
  * Use getAgentBySlugForProject() when you need project-scoped lookups.
663
666
  */
664
667
  async getAgentBySlug(slug: string): Promise<StoredAgent | null> {
@@ -1098,6 +1101,26 @@ export class AgentStorage {
1098
1101
  return true;
1099
1102
  }
1100
1103
 
1104
+ /**
1105
+ * Get all known agent pubkeys across all projects from the index.
1106
+ * Returns pubkeys from the byProject index (source of truth for project associations).
1107
+ *
1108
+ * Used by the Daemon to seed the TrustPubkeyService global agent pubkeys set
1109
+ * at startup, covering agents from not-yet-running projects.
1110
+ */
1111
+ async getAllKnownPubkeys(): Promise<Set<string>> {
1112
+ if (!this.index) await this.loadIndex();
1113
+ if (!this.index) return new Set();
1114
+
1115
+ const pubkeys = new Set<string>();
1116
+ for (const projectPubkeys of Object.values(this.index.byProject)) {
1117
+ for (const pubkey of projectPubkeys) {
1118
+ pubkeys.add(pubkey);
1119
+ }
1120
+ }
1121
+ return pubkeys;
1122
+ }
1123
+
1101
1124
  /**
1102
1125
  * Get all agents (for debugging/admin purposes)
1103
1126
  */
@@ -1,5 +1,6 @@
1
1
  import { agentStorage, createStoredAgent, type StoredAgent } from "@/agents/AgentStorage";
2
2
  import { AgentNotFoundError, AgentValidationError } from "@/agents/errors";
3
+ import { isValidCategory, type AgentCategory } from "@/agents/role-categories";
3
4
  import { installAgentScripts } from "@/agents/script-installer";
4
5
  import { DEFAULT_AGENT_LLM_CONFIG } from "@/llm/constants";
5
6
  import { getNDK } from "@/nostr";
@@ -77,6 +78,7 @@ interface ParsedAgentEvent {
77
78
  slug: string;
78
79
  name: string;
79
80
  role: string;
81
+ category?: AgentCategory;
80
82
  description: string;
81
83
  instructions: string;
82
84
  useCriteria: string;
@@ -96,6 +98,8 @@ function parseAgentEvent(event: NDKEvent, slug: string): ParsedAgentEvent {
96
98
  const title = agentDef.title || "Unnamed Agent";
97
99
  const description = agentDef.description || "";
98
100
  const role = agentDef.role || "assistant";
101
+ const rawCategory = agentDef.category || undefined;
102
+ const category = rawCategory && isValidCategory(rawCategory) ? rawCategory : undefined;
99
103
  const instructions = agentDef.instructions || "";
100
104
  const useCriteria = agentDef.useCriteria || "";
101
105
 
@@ -117,6 +121,7 @@ function parseAgentEvent(event: NDKEvent, slug: string): ParsedAgentEvent {
117
121
  slug,
118
122
  name: title,
119
123
  role,
124
+ category,
120
125
  description,
121
126
  instructions,
122
127
  useCriteria,
@@ -191,6 +196,7 @@ export async function installAgentFromNostrEvent(
191
196
  slug: agentData.slug,
192
197
  name: agentData.name,
193
198
  role: agentData.role,
199
+ category: agentData.category,
194
200
  description: agentData.description,
195
201
  instructions: agentData.instructions,
196
202
  useCriteria: agentData.useCriteria,
@@ -2,6 +2,7 @@ import type { AgentRegistry } from "@/agents/AgentRegistry";
2
2
  import { agentStorage, type StoredAgent } from "@/agents/AgentStorage";
3
3
  import { installAgentFromNostr } from "@/agents/agent-installer";
4
4
  import { AgentSlugConflictError } from "@/agents/errors";
5
+ import { resolveCategory } from "@/agents/role-categories";
5
6
  import { processAgentTools } from "@/agents/tool-normalization";
6
7
  import type { AgentInstance } from "@/agents/types";
7
8
  import { AgentMetadataStore } from "@/services/agents";
@@ -42,7 +43,10 @@ export function createAgentInstance(
42
43
  const effectiveTools = resolvedConfig.tools;
43
44
 
44
45
  // Process tools using pure functions
45
- const validToolNames = processAgentTools(effectiveTools || [], storedAgent.slug);
46
+ const normalizedTools = processAgentTools(effectiveTools || [], storedAgent.slug);
47
+
48
+ // Resolve category for organizational purposes (no tool restrictions)
49
+ const resolvedCategory = resolveCategory(storedAgent.category);
46
50
 
47
51
  // Build agent-specific MCP config from stored mcpServers
48
52
  const agentMcpConfig: MCPConfig | undefined = storedAgent.mcpServers
@@ -57,11 +61,12 @@ export function createAgentInstance(
57
61
  pubkey,
58
62
  signer,
59
63
  role: storedAgent.role,
64
+ category: resolvedCategory,
60
65
  description: storedAgent.description,
61
66
  instructions: storedAgent.instructions,
62
67
  useCriteria: storedAgent.useCriteria,
63
68
  llmConfig: effectiveLLMConfig || DEFAULT_AGENT_LLM_CONFIG,
64
- tools: validToolNames,
69
+ tools: normalizedTools,
65
70
  eventId: storedAgent.eventId,
66
71
  slug: storedAgent.slug,
67
72
  mcpServers: storedAgent.mcpServers,
@@ -19,8 +19,16 @@ export const CORE_AGENT_TOOLS: ToolName[] = [
19
19
  // Conversation tools for project introspection
20
20
  "conversation_get", // All agents should access conversation details
21
21
  "conversation_list", // All agents should list conversations
22
- // Unified search across all project knowledge
23
- "search", // All agents should be able to search across reports, conversations, and lessons
22
+ // RAG tools for knowledge management
23
+ "rag_search", // All agents should be able to search across reports, conversations, and lessons
24
+ "rag_create_collection", // All agents should be able to create RAG collections
25
+ "rag_add_documents", // All agents should be able to add documents to collections
26
+ "rag_delete_collection", // All agents should be able to delete RAG collections
27
+ "rag_list_collections", // All agents should be able to list RAG collections
28
+ "rag_subscription_create", // All agents should be able to create RAG subscriptions
29
+ "rag_subscription_list", // All agents should be able to list RAG subscriptions
30
+ "rag_subscription_get", // All agents should be able to get RAG subscription details
31
+ "rag_subscription_delete", // All agents should be able to delete RAG subscriptions
24
32
  // Process control
25
33
  "kill", // All agents should be able to terminate processes
26
34
  // MCP resource reading and subscriptions (self-gating: only works if agent has MCP tools from that server)
@@ -241,6 +241,23 @@ export class AgentExecutor {
241
241
  }
242
242
  }
243
243
 
244
+ logger.writeToWarnLog({
245
+ timestamp: new Date().toISOString(),
246
+ level: "error",
247
+ component: "AgentExecutor",
248
+ message: isCreditsError
249
+ ? "Execution failed due to insufficient credits"
250
+ : "Agent execution failed",
251
+ context: {
252
+ agentSlug: context.agent.slug,
253
+ conversationId: conversation?.id?.substring(0, 12),
254
+ isCreditsError,
255
+ errorType: isCreditsError ? "insufficient_credits" : "execution_error",
256
+ },
257
+ error: errorMessage,
258
+ stack: error instanceof Error ? error.stack : undefined,
259
+ });
260
+
244
261
  logger.error(
245
262
  isCreditsError
246
263
  ? "[AgentExecutor] Execution failed due to insufficient credits"
@@ -320,6 +337,18 @@ export class AgentExecutor {
320
337
  agent: context.agent.slug,
321
338
  error: formatAnyError(streamError),
322
339
  });
340
+ logger.writeToWarnLog({
341
+ timestamp: new Date().toISOString(),
342
+ level: "error",
343
+ component: "AgentExecutor",
344
+ message: "LLM streaming execution failed",
345
+ context: {
346
+ agentSlug: context.agent.slug,
347
+ ralNumber,
348
+ },
349
+ error: formatAnyError(streamError),
350
+ stack: streamError instanceof Error ? streamError.stack : undefined,
351
+ });
323
352
  throw streamError;
324
353
  }
325
354
 
@@ -388,20 +417,18 @@ export class AgentExecutor {
388
417
  "outstanding.has_work": outstandingWork.hasWork,
389
418
  "outstanding.queued_injections": outstandingWork.details.queuedInjections,
390
419
  "outstanding.pending_delegations": outstandingWork.details.pendingDelegations,
391
- "fix_applied": "uses hasOutstandingWork() to check both injections and delegations",
420
+ "outstanding.completed_delegations": outstandingWork.details.completedDelegations,
392
421
  });
393
422
 
394
- // INVARIANT GUARD: If there's outstanding work (queued injections OR pending delegations),
395
- // we should NOT finalize. The executor should continue to allow the work to be processed.
423
+ // INVARIANT GUARD: If there's outstanding work (queued injections, pending delegations,
424
+ // or completed delegations not yet consumed), we should NOT finalize.
396
425
  const hasMessageContent = completionEvent?.message && completionEvent.message.length > 0;
397
426
  if (!hasMessageContent && outstandingWork.hasWork) {
398
- // This is the expected path when delegation results arrive via debounce.
399
- // The executor returns undefined to allow the dispatch loop to continue
400
- // and process the queued injections in the next iteration.
401
427
  trace.getActiveSpan()?.addEvent("executor.awaiting_outstanding_work", {
402
428
  "ral.number": ralNumber,
403
429
  "outstanding.queued_injections": outstandingWork.details.queuedInjections,
404
430
  "outstanding.pending_delegations": outstandingWork.details.pendingDelegations,
431
+ "outstanding.completed_delegations": outstandingWork.details.completedDelegations,
405
432
  "completion_event_exists": Boolean(completionEvent),
406
433
  "scenario": "injection_debounce_await",
407
434
  });
@@ -410,6 +437,7 @@ export class AgentExecutor {
410
437
  ralNumber,
411
438
  queuedInjections: outstandingWork.details.queuedInjections,
412
439
  pendingDelegations: outstandingWork.details.pendingDelegations,
440
+ completedDelegations: outstandingWork.details.completedDelegations,
413
441
  });
414
442
  return undefined;
415
443
  }
@@ -486,6 +514,7 @@ export class AgentExecutor {
486
514
  "outstanding.has_work": finalOutstandingWork.hasWork,
487
515
  "outstanding.queued_injections": finalOutstandingWork.details.queuedInjections,
488
516
  "outstanding.pending_delegations": finalOutstandingWork.details.pendingDelegations,
517
+ "outstanding.completed_delegations": finalOutstandingWork.details.completedDelegations,
489
518
  });
490
519
 
491
520
  let responseEvent: NDKEvent | undefined;
@@ -6,7 +6,6 @@
6
6
  */
7
7
 
8
8
  import { formatAnyError } from "@/lib/error-formatter";
9
- import { LLMService } from "@/llm/service";
10
9
  import { llmServiceFactory } from "@/llm/LLMServiceFactory";
11
10
  import { shortenConversationId } from "@/utils/conversation-id";
12
11
  import { config as configService } from "@/services/ConfigService";
@@ -14,7 +13,7 @@ import { RALRegistry } from "@/services/ral";
14
13
  import type { SkillData } from "@/services/skill";
15
14
  import { logger } from "@/utils/logger";
16
15
  import { SpanStatusCode, trace } from "@opentelemetry/api";
17
- import type { LanguageModel, ModelMessage } from "ai";
16
+ import type { LanguageModel, ModelMessage, ProviderRegistryProvider } from "ai";
18
17
 
19
18
  const tracer = trace.getTracer("tenex.stream-callbacks");
20
19
  import { MessageCompiler } from "./MessageCompiler";
@@ -53,7 +52,11 @@ export interface StepData {
53
52
  */
54
53
  export interface PrepareStepConfig {
55
54
  context: FullRuntimeContext;
56
- llmService: { provider: string; updateUsageFromSteps: (steps: StepData["steps"]) => void };
55
+ llmService: {
56
+ provider: string;
57
+ updateUsageFromSteps: (steps: StepData["steps"]) => void;
58
+ createLanguageModelFromRegistry: (provider: string, model: string, registry: ProviderRegistryProvider) => LanguageModel;
59
+ };
57
60
  messageCompiler: MessageCompiler;
58
61
  ephemeralMessages: Array<{ role: "user" | "system"; content: string }>;
59
62
  nudgeContent: string;
@@ -147,15 +150,25 @@ export function createPrepareStep(
147
150
  content: injection.content,
148
151
  });
149
152
  } else {
150
- conversationStore.addMessage({
151
- pubkey: context.triggeringEvent.pubkey,
152
- ral: ralNumber,
153
- content: injection.content,
154
- messageType: "text",
155
- targetedPubkeys: [context.agent.pubkey],
156
- senderPubkey: injection.senderPubkey,
157
- eventId: injection.eventId,
158
- });
153
+ const relocated = injection.eventId
154
+ ? conversationStore.relocateToEnd(injection.eventId, {
155
+ ral: ralNumber,
156
+ senderPubkey: injection.senderPubkey,
157
+ targetedPubkeys: [context.agent.pubkey],
158
+ })
159
+ : false;
160
+
161
+ if (!relocated) {
162
+ conversationStore.addMessage({
163
+ pubkey: context.triggeringEvent.pubkey,
164
+ ral: ralNumber,
165
+ content: injection.content,
166
+ messageType: "text",
167
+ targetedPubkeys: [context.agent.pubkey],
168
+ senderPubkey: injection.senderPubkey,
169
+ eventId: injection.eventId,
170
+ });
171
+ }
159
172
  }
160
173
  }
161
174
 
@@ -259,7 +272,7 @@ export function createPrepareStep(
259
272
 
260
273
  try {
261
274
  const registry = llmServiceFactory.getRegistry();
262
- const newModel = LLMService.createLanguageModelFromRegistry(
275
+ const newModel = llmService.createLanguageModelFromRegistry(
263
276
  newLlmConfig.provider,
264
277
  newLlmConfig.model,
265
278
  registry
@@ -291,6 +304,19 @@ export function createPrepareStep(
291
304
  variant: currentVariant,
292
305
  config: resolution.configName,
293
306
  });
307
+ logger.writeToWarnLog({
308
+ timestamp: new Date().toISOString(),
309
+ level: "error",
310
+ component: "StreamCallbacks",
311
+ message: "Failed to create new model for variant switch",
312
+ context: {
313
+ agent: context.agent.slug,
314
+ variant: currentVariant,
315
+ config: resolution.configName,
316
+ },
317
+ error: formatAnyError(modelError),
318
+ stack: modelError instanceof Error ? modelError.stack : undefined,
319
+ });
294
320
  }
295
321
  }
296
322
  }
@@ -302,6 +328,20 @@ export function createPrepareStep(
302
328
  } catch (error) {
303
329
  span.recordException(error as Error);
304
330
  span.setStatus({ code: SpanStatusCode.ERROR });
331
+ logger.writeToWarnLog({
332
+ timestamp: new Date().toISOString(),
333
+ level: "error",
334
+ component: "StreamCallbacks",
335
+ message: "LLM streaming prepareStep failed",
336
+ context: {
337
+ agent: context.agent.slug,
338
+ conversationId: context.conversationId,
339
+ stepNumber: step.stepNumber,
340
+ ralNumber,
341
+ },
342
+ error: error instanceof Error ? error.message : String(error),
343
+ stack: error instanceof Error ? error.stack : undefined,
344
+ });
305
345
  throw error;
306
346
  } finally {
307
347
  span.end();