@tenex-chat/backend 0.9.4 → 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 -46778
  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 -215
  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,384 +0,0 @@
1
- import { NDKKind } from "@/nostr/kinds";
2
- import type NDK from "@nostr-dev-kit/ndk";
3
- import { NDKEvent, type NDKRawEvent } from "@nostr-dev-kit/ndk";
4
-
5
- /**
6
- * NDKProjectStatus represents a TenexProjectStatus event
7
- * Used to indicate project status including online agents and model configurations
8
- */
9
- export class NDKProjectStatus extends NDKEvent {
10
- static kind = NDKKind.TenexProjectStatus;
11
- static kinds = [NDKKind.TenexProjectStatus];
12
-
13
- constructor(ndk?: NDK, event?: NDKEvent | NDKRawEvent) {
14
- super(ndk, event);
15
- this.kind ??= NDKKind.TenexProjectStatus;
16
- }
17
-
18
- static from(event: NDKEvent): NDKProjectStatus {
19
- return new NDKProjectStatus(event.ndk, event);
20
- }
21
-
22
- /**
23
- * Get the project this status refers to
24
- * Returns the value of the "a" tag (replaceable event reference)
25
- */
26
- get projectReference(): string | undefined {
27
- return this.tagValue("a");
28
- }
29
-
30
- /**
31
- * Set the project this status refers to
32
- * @param projectTagId The tag ID of the NDKProject event (format: kind:pubkey:dTag)
33
- */
34
- set projectReference(projectTagId: string | undefined) {
35
- this.removeTag("a");
36
- if (projectTagId) {
37
- this.tags.push(["a", projectTagId]);
38
- }
39
- }
40
-
41
- /**
42
- * Get all agent entries from this status event
43
- * Returns an array of {pubkey, slug} objects
44
- */
45
- get agents(): Array<{ pubkey: string; slug: string }> {
46
- const agentTags = this.tags.filter((tag) => tag[0] === "agent" && tag[1] && tag[2]);
47
- return agentTags.map((tag) => ({
48
- pubkey: tag[1],
49
- slug: tag[2],
50
- }));
51
- }
52
-
53
- /**
54
- * Add an agent to the status
55
- * @param pubkey The agent's public key
56
- * @param slug The agent's slug/identifier
57
- */
58
- addAgent(pubkey: string, slug: string): void {
59
- this.tags.push(["agent", pubkey, slug]);
60
- }
61
-
62
- /**
63
- * Remove an agent from the status
64
- * @param pubkey The agent's public key to remove
65
- */
66
- removeAgent(pubkey: string): void {
67
- this.tags = this.tags.filter((tag) => !(tag[0] === "agent" && tag[1] === pubkey));
68
- }
69
-
70
- /**
71
- * Clear all agents from the status
72
- */
73
- clearAgents(): void {
74
- this.tags = this.tags.filter((tag) => tag[0] !== "agent");
75
- }
76
-
77
- /**
78
- * Check if a specific agent is in the status
79
- * @param pubkey The agent's public key
80
- */
81
- hasAgent(pubkey: string): boolean {
82
- return this.tags.some((tag) => tag[0] === "agent" && tag[1] === pubkey);
83
- }
84
-
85
- /**
86
- * Get all model configurations from this status event
87
- * Returns an array of {modelSlug, agents} objects where agents is an array of agent slugs
88
- */
89
- get models(): Array<{ modelSlug: string; agents: string[] }> {
90
- const modelTags = this.tags.filter((tag) => tag[0] === "model" && tag[1]);
91
- return modelTags.map((tag) => ({
92
- modelSlug: tag[1],
93
- agents: tag.slice(2).filter((a) => a), // Get all agent slugs from index 2 onwards
94
- }));
95
- }
96
-
97
- /**
98
- * Add a model with its agent access list
99
- * @param modelSlug The model slug identifier (e.g., "gpt-4", "claude-3")
100
- * @param agentSlugs Array of agent slugs that use this model
101
- */
102
- addModel(modelSlug: string, agentSlugs: string[]): void {
103
- // Remove existing model tag if it exists
104
- this.removeModel(modelSlug);
105
- // Add new model tag with all agent slugs
106
- this.tags.push(["model", modelSlug, ...agentSlugs]);
107
- }
108
-
109
- /**
110
- * Remove a model from the status
111
- * @param modelSlug The model slug to remove
112
- */
113
- removeModel(modelSlug: string): void {
114
- this.tags = this.tags.filter((tag) => !(tag[0] === "model" && tag[1] === modelSlug));
115
- }
116
-
117
- /**
118
- * Clear all model configurations from the status
119
- */
120
- clearModels(): void {
121
- this.tags = this.tags.filter((tag) => tag[0] !== "model");
122
- }
123
-
124
- /**
125
- * Check if a specific model exists
126
- * @param modelSlug The model slug
127
- */
128
- hasModel(modelSlug: string): boolean {
129
- return this.tags.some((tag) => tag[0] === "model" && tag[1] === modelSlug);
130
- }
131
-
132
- /**
133
- * Get agents that use a specific model
134
- * @param modelSlug The model slug
135
- * @returns Array of agent slugs that use this model
136
- */
137
- getModelAgents(modelSlug: string): string[] {
138
- const modelTag = this.tags.find((tag) => tag[0] === "model" && tag[1] === modelSlug);
139
- return modelTag ? modelTag.slice(2).filter((a) => a) : [];
140
- }
141
-
142
- /**
143
- * Check if a specific agent uses a model
144
- * @param modelSlug The model slug
145
- * @param agentSlug The agent slug
146
- */
147
- agentUsesModel(modelSlug: string, agentSlug: string): boolean {
148
- const agents = this.getModelAgents(modelSlug);
149
- return agents.includes(agentSlug);
150
- }
151
-
152
- /**
153
- * Get all models used by a specific agent
154
- * @param agentSlug The agent slug
155
- * @returns Array of model slugs used by this agent
156
- */
157
- getAgentModels(agentSlug: string): string[] {
158
- return this.models
159
- .filter((model) => model.agents.includes(agentSlug))
160
- .map((model) => model.modelSlug);
161
- }
162
-
163
- /**
164
- * Get the status message/content
165
- */
166
- get status(): string {
167
- return this.content;
168
- }
169
-
170
- /**
171
- * Set the status message/content
172
- */
173
- set status(value: string) {
174
- this.content = value;
175
- }
176
-
177
- /**
178
- * Get all tools with their agent access information
179
- * Returns an array of {toolName, agents} objects where agents is an array of agent slugs
180
- */
181
- get tools(): Array<{ toolName: string; agents: string[] }> {
182
- const toolTags = this.tags.filter((tag) => tag[0] === "tool" && tag[1]);
183
- return toolTags.map((tag) => ({
184
- toolName: tag[1],
185
- agents: tag.slice(2).filter((a) => a), // Get all agent slugs from index 2 onwards
186
- }));
187
- }
188
-
189
- /**
190
- * Add a tool with its agent access list
191
- * @param toolName The name of the tool
192
- * @param agentSlugs Array of agent slugs that have access to this tool
193
- */
194
- addTool(toolName: string, agentSlugs: string[]): void {
195
- // Remove existing tool tag if it exists
196
- this.removeTool(toolName);
197
- // Add new tool tag with all agent slugs
198
- this.tags.push(["tool", toolName, ...agentSlugs]);
199
- }
200
-
201
- /**
202
- * Remove a tool from the status
203
- * @param toolName The tool name to remove
204
- */
205
- removeTool(toolName: string): void {
206
- this.tags = this.tags.filter((tag) => !(tag[0] === "tool" && tag[1] === toolName));
207
- }
208
-
209
- /**
210
- * Clear all tools from the status
211
- */
212
- clearTools(): void {
213
- this.tags = this.tags.filter((tag) => tag[0] !== "tool");
214
- }
215
-
216
- /**
217
- * Check if a specific tool exists
218
- * @param toolName The tool name
219
- */
220
- hasTool(toolName: string): boolean {
221
- return this.tags.some((tag) => tag[0] === "tool" && tag[1] === toolName);
222
- }
223
-
224
- /**
225
- * Get agents that have access to a specific tool
226
- * @param toolName The tool name
227
- * @returns Array of agent slugs that have access to this tool
228
- */
229
- getToolAgents(toolName: string): string[] {
230
- const toolTag = this.tags.find((tag) => tag[0] === "tool" && tag[1] === toolName);
231
- return toolTag ? toolTag.slice(2).filter((a) => a) : [];
232
- }
233
-
234
- /**
235
- * Check if a specific agent has access to a tool
236
- * @param toolName The tool name
237
- * @param agentSlug The agent slug
238
- */
239
- agentHasTool(toolName: string, agentSlug: string): boolean {
240
- const agents = this.getToolAgents(toolName);
241
- return agents.includes(agentSlug);
242
- }
243
-
244
- /**
245
- * Get all tools accessible by a specific agent
246
- * @param agentSlug The agent slug
247
- * @returns Array of tool names accessible by this agent
248
- */
249
- getAgentTools(agentSlug: string): string[] {
250
- return this.tools
251
- .filter((tool) => tool.agents.includes(agentSlug))
252
- .map((tool) => tool.toolName);
253
- }
254
-
255
- /**
256
- * Get all worktrees from this status event
257
- * Returns an array of branch names, with the first being the default branch
258
- */
259
- get worktrees(): string[] {
260
- const worktreeTags = this.tags.filter((tag) => tag[0] === "branch" && tag[1]);
261
- return worktreeTags.map((tag) => tag[1]);
262
- }
263
-
264
- /**
265
- * Add a worktree to the status
266
- * @param branchName The branch name
267
- */
268
- addWorktree(branchName: string): void {
269
- this.tags.push(["branch", branchName]);
270
- }
271
-
272
- /**
273
- * Get the default worktree (first worktree in the list)
274
- */
275
- get defaultWorktree(): string | undefined {
276
- return this.worktrees[0];
277
- }
278
-
279
- // ========================================================================
280
- // Scheduled Tasks
281
- // ========================================================================
282
-
283
- /**
284
- * Get all scheduled tasks from this status event
285
- * Tag format: ["scheduled-task", id, title, schedule, targetAgent, type, lastRunTimestamp]
286
- */
287
- get scheduledTasks(): Array<{
288
- id: string;
289
- title: string;
290
- schedule: string;
291
- /** Agent slug when resolvable, otherwise a truncated pubkey prefix */
292
- targetAgent: string;
293
- type: "cron" | "oneoff";
294
- lastRun?: number;
295
- }> {
296
- const taskTags = this.tags.filter((tag) => tag[0] === "scheduled-task" && tag[1]);
297
- return taskTags.map((tag) => {
298
- const rawType = tag[5];
299
- const type = rawType === "cron" || rawType === "oneoff" ? rawType : "cron";
300
- const parsedLastRun = tag[6] ? Number(tag[6]) : NaN;
301
- const lastRun = Number.isFinite(parsedLastRun) ? parsedLastRun : undefined;
302
-
303
- return {
304
- id: tag[1],
305
- title: tag[2] || "",
306
- schedule: tag[3] || "",
307
- targetAgent: tag[4] || "",
308
- type,
309
- lastRun,
310
- };
311
- });
312
- }
313
-
314
- /**
315
- * Add a scheduled task to the status
316
- * @param id Task identifier
317
- * @param title Human-readable task title
318
- * @param schedule Cron expression or ISO timestamp
319
- * @param targetAgent Agent slug or pubkey-prefix label for the target agent
320
- * @param type Task type: "cron" or "oneoff"
321
- * @param lastRun Optional last run Unix timestamp in seconds
322
- */
323
- addScheduledTask(
324
- id: string,
325
- title: string,
326
- schedule: string,
327
- targetAgent: string,
328
- type: "cron" | "oneoff",
329
- lastRun?: number
330
- ): void {
331
- this.tags.push([
332
- "scheduled-task",
333
- id,
334
- title,
335
- schedule,
336
- targetAgent,
337
- type,
338
- lastRun !== undefined ? String(lastRun) : "",
339
- ]);
340
- }
341
-
342
- /**
343
- * Remove a scheduled task from the status
344
- * @param id The task ID to remove
345
- */
346
- removeScheduledTask(id: string): void {
347
- this.tags = this.tags.filter(
348
- (tag) => !(tag[0] === "scheduled-task" && tag[1] === id)
349
- );
350
- }
351
-
352
- /**
353
- * Clear all scheduled tasks from the status
354
- */
355
- clearScheduledTasks(): void {
356
- this.tags = this.tags.filter((tag) => tag[0] !== "scheduled-task");
357
- }
358
-
359
- /**
360
- * Check if a specific scheduled task exists
361
- * @param id The task ID
362
- */
363
- hasScheduledTask(id: string): boolean {
364
- return this.tags.some((tag) => tag[0] === "scheduled-task" && tag[1] === id);
365
- }
366
-
367
- /**
368
- * Get all scheduled tasks targeting a specific agent
369
- * @param agentIdentifier The agent slug or truncated pubkey prefix
370
- */
371
- getScheduledTasksForAgent(agentIdentifier: string): Array<{
372
- id: string;
373
- title: string;
374
- schedule: string;
375
- /** Agent slug when resolvable, otherwise a truncated pubkey prefix */
376
- targetAgent: string;
377
- type: "cron" | "oneoff";
378
- lastRun?: number;
379
- }> {
380
- return this.scheduledTasks.filter(
381
- (task) => task.targetAgent === agentIdentifier
382
- );
383
- }
384
- }
@@ -1,4 +0,0 @@
1
- export { NDKAgentDefinition } from "./NDKAgentDefinition";
2
- export { NDKAgentLesson } from "./NDKAgentLesson";
3
- export { NDKMCPTool } from "./NDKMCPTool";
4
- export { NDKProjectStatus } from "./NDKProjectStatus";
@@ -1,30 +0,0 @@
1
- /**
2
- * Safely parse JSON with markdown code block cleanup
3
- * @param text The text to parse, potentially containing markdown code blocks
4
- * @param context Optional context for error logging
5
- * @returns Parsed object or null if parsing fails
6
- */
7
- export function safeParseJSON<T = unknown>(text: string, context?: string): T | null {
8
- try {
9
- // Clean up response - remove markdown code blocks if present
10
- let cleanText = text.trim();
11
-
12
- // Remove ```json or ``` wrapper if present
13
- if (cleanText.startsWith("```json")) {
14
- cleanText = cleanText.replace(/^```json\s*/, "").replace(/```\s*$/, "");
15
- } else if (cleanText.startsWith("```")) {
16
- cleanText = cleanText.replace(/^```\s*/, "").replace(/```\s*$/, "");
17
- }
18
-
19
- return JSON.parse(cleanText);
20
- } catch (error) {
21
- if (context) {
22
- // Use console.error since lib/ should not depend on TENEX logger
23
- console.error(`[JSON Parser] Failed to parse JSON in ${context}`, {
24
- error: error instanceof Error ? error.message : String(error),
25
- text: text.substring(0, 200),
26
- });
27
- }
28
- return null;
29
- }
30
- }
@@ -1,37 +0,0 @@
1
- import { EventEmitter } from "tseep";
2
-
3
- /**
4
- * Global recording state singleton.
5
- * Controls whether the flight recorder middleware saves LLM interactions.
6
- */
7
- class RecordingStateManager extends EventEmitter<{
8
- "state-changed": (isRecording: boolean) => void;
9
- }> {
10
- private _isRecording = false;
11
-
12
- get isRecording(): boolean {
13
- return this._isRecording;
14
- }
15
-
16
- toggle(): boolean {
17
- this._isRecording = !this._isRecording;
18
- this.emit("state-changed", this._isRecording);
19
- return this._isRecording;
20
- }
21
-
22
- start(): void {
23
- if (!this._isRecording) {
24
- this._isRecording = true;
25
- this.emit("state-changed", true);
26
- }
27
- }
28
-
29
- stop(): void {
30
- if (this._isRecording) {
31
- this._isRecording = false;
32
- this.emit("state-changed", false);
33
- }
34
- }
35
- }
36
-
37
- export const recordingState = new RecordingStateManager();
@@ -1,40 +0,0 @@
1
- import { logger } from "@/utils/logger";
2
- import type { LocalStreamChunk } from "./types";
3
-
4
- /**
5
- * Interface for stream transports (Unix socket, future Nostr ephemeral)
6
- */
7
- export interface StreamTransport {
8
- write(chunk: LocalStreamChunk): void;
9
- isConnected(): boolean;
10
- }
11
-
12
- /**
13
- * Publishes AI SDK chunks to connected transports
14
- * Fire-and-forget: silently drops if no transport connected
15
- */
16
- export class StreamPublisher {
17
- private transport: StreamTransport | null = null;
18
-
19
- setTransport(transport: StreamTransport | null): void {
20
- this.transport = transport;
21
- }
22
-
23
- write(chunk: LocalStreamChunk): void {
24
- logger.debug("[StreamPublisher] write called", {
25
- hasTransport: !!this.transport,
26
- isConnected: this.transport?.isConnected() ?? false,
27
- chunkType: (chunk.data as { type?: string })?.type,
28
- });
29
- if (this.transport?.isConnected()) {
30
- this.transport.write(chunk);
31
- }
32
- }
33
-
34
- isConnected(): boolean {
35
- return this.transport?.isConnected() ?? false;
36
- }
37
- }
38
-
39
- /** Singleton instance */
40
- export const streamPublisher = new StreamPublisher();
@@ -1,188 +0,0 @@
1
- import type { LanguageModelMiddleware } from "ai";
2
- import { mkdir, writeFile } from "fs/promises";
3
- import { join } from "path";
4
- import { getTenexBasePath } from "@/constants";
5
- import { recordingState } from "../RecordingState";
6
-
7
- type WrapGenerateParams = Parameters<NonNullable<LanguageModelMiddleware["wrapGenerate"]>>[0];
8
- type WrapStreamParams = Parameters<NonNullable<LanguageModelMiddleware["wrapStream"]>>[0];
9
-
10
- // Recording types - using unknown for SDK types that may change between versions
11
- interface RecordingResponse {
12
- content: unknown;
13
- finishReason: unknown;
14
- usage: unknown;
15
- }
16
-
17
- interface RecordingError {
18
- message: string;
19
- }
20
-
21
- interface ToolCallRecord {
22
- toolCallId: string;
23
- toolName: string;
24
- args: unknown;
25
- }
26
-
27
- /**
28
- * Flight recorder configuration
29
- */
30
- export interface FlightRecorderConfig {
31
- /** Base directory for recordings (defaults to ~/.tenex/recordings) */
32
- baseDir?: string;
33
- }
34
-
35
- /**
36
- * Creates a flight recorder middleware that records LLM interactions when enabled.
37
- * Recording is controlled by the global recordingState - toggle with Ctrl+R in daemon.
38
- *
39
- * Recordings are saved to: {baseDir}/YYYY-MM-DD/{timestamp}-{hash}.json
40
- * Respects TENEX_BASE_DIR environment variable for instance isolation.
41
- */
42
- export function createFlightRecorderMiddleware(
43
- config: FlightRecorderConfig = {}
44
- ): LanguageModelMiddleware {
45
- const baseDir = config.baseDir || join(getTenexBasePath(), "recordings");
46
-
47
- return {
48
- specificationVersion: "v3" as const,
49
-
50
- async wrapGenerate({ doGenerate, params, model }: WrapGenerateParams) {
51
- if (!recordingState.isRecording) {
52
- return doGenerate();
53
- }
54
-
55
- const startTime = Date.now();
56
- const hash = createSimpleHash(JSON.stringify(params.prompt));
57
-
58
- const recording = {
59
- timestamp: new Date().toISOString(),
60
- type: "generate",
61
- model: { provider: model.provider, modelId: model.modelId },
62
- request: {
63
- prompt: params.prompt,
64
- temperature: params.temperature,
65
- maxOutputTokens: params.maxOutputTokens,
66
- tools: params.tools,
67
- toolChoice: params.toolChoice,
68
- },
69
- response: null as RecordingResponse | null,
70
- error: null as RecordingError | null,
71
- duration: 0,
72
- };
73
-
74
- try {
75
- const result = await doGenerate();
76
- recording.response = {
77
- content: result.content,
78
- finishReason: result.finishReason,
79
- usage: result.usage,
80
- };
81
- recording.duration = Date.now() - startTime;
82
- await saveRecording(baseDir, hash, recording);
83
- return result;
84
- } catch (error) {
85
- recording.error = {
86
- message: error instanceof Error ? error.message : String(error),
87
- };
88
- recording.duration = Date.now() - startTime;
89
- await saveRecording(baseDir, hash, recording);
90
- throw error;
91
- }
92
- },
93
-
94
- wrapStream({ doStream, params, model }: WrapStreamParams) {
95
- if (!recordingState.isRecording) {
96
- return doStream();
97
- }
98
-
99
- const startTime = Date.now();
100
- const hash = createSimpleHash(JSON.stringify(params.prompt));
101
-
102
- // Buffer to collect stream content
103
- const textParts: string[] = [];
104
- const toolCalls: ToolCallRecord[] = [];
105
- let finishReason: string | undefined;
106
- let usage: unknown;
107
-
108
- // Wrap the stream with a TransformStream to intercept chunks
109
- return doStream().then((result) => {
110
- const transform = new TransformStream({
111
- transform(chunk, controller) {
112
- // Pass through the chunk
113
- controller.enqueue(chunk);
114
-
115
- // Collect data for recording
116
- if (chunk.type === "text-delta" && chunk.textDelta) {
117
- textParts.push(chunk.textDelta);
118
- }
119
- if (chunk.type === "tool-call") {
120
- toolCalls.push({
121
- toolCallId: chunk.toolCallId,
122
- toolName: chunk.toolName,
123
- args: chunk.args,
124
- });
125
- }
126
- if (chunk.type === "finish") {
127
- finishReason = chunk.finishReason;
128
- usage = chunk.usage;
129
- }
130
- },
131
- flush() {
132
- // Stream finished - save the recording
133
- const recording = {
134
- timestamp: new Date().toISOString(),
135
- type: "stream",
136
- model: { provider: model.provider, modelId: model.modelId },
137
- request: {
138
- prompt: params.prompt,
139
- temperature: params.temperature,
140
- maxOutputTokens: params.maxOutputTokens,
141
- tools: params.tools,
142
- toolChoice: params.toolChoice,
143
- },
144
- response: {
145
- content: [{ type: "text", text: textParts.join("") }],
146
- toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
147
- finishReason,
148
- usage,
149
- },
150
- duration: Date.now() - startTime,
151
- };
152
- saveRecording(baseDir, hash, recording).catch(() => {});
153
- },
154
- });
155
-
156
- return {
157
- ...result,
158
- stream: result.stream.pipeThrough(transform),
159
- };
160
- });
161
- },
162
- };
163
- }
164
-
165
- async function saveRecording(baseDir: string, hash: string, recording: Record<string, unknown>): Promise<void> {
166
- try {
167
- const now = new Date();
168
- const dateStr = now.toISOString().split("T")[0];
169
- const dirPath = join(baseDir, dateStr);
170
- await mkdir(dirPath, { recursive: true });
171
-
172
- const timestamp = now.toISOString().replace(/[:.]/g, "-");
173
- const filename = `${timestamp}-${hash}.json`;
174
- await writeFile(join(dirPath, filename), JSON.stringify(recording, null, 2), "utf-8");
175
- } catch (error) {
176
- console.error("[FlightRecorder] Failed to save recording:", error);
177
- }
178
- }
179
-
180
- function createSimpleHash(input: string): string {
181
- let hash = 0;
182
- for (let i = 0; i < input.length; i++) {
183
- const char = input.charCodeAt(i);
184
- hash = (hash << 5) - hash + char;
185
- hash = hash & hash;
186
- }
187
- return Math.abs(hash).toString(16).substring(0, 8);
188
- }