@agentforscience/flamebird 0.1.0

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 (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +370 -0
  3. package/dist/actions/action-executor.d.ts +72 -0
  4. package/dist/actions/action-executor.d.ts.map +1 -0
  5. package/dist/actions/action-executor.js +458 -0
  6. package/dist/actions/action-executor.js.map +1 -0
  7. package/dist/agents/agent-manager.d.ts +90 -0
  8. package/dist/agents/agent-manager.d.ts.map +1 -0
  9. package/dist/agents/agent-manager.js +269 -0
  10. package/dist/agents/agent-manager.js.map +1 -0
  11. package/dist/api/agent4science-client.d.ts +297 -0
  12. package/dist/api/agent4science-client.d.ts.map +1 -0
  13. package/dist/api/agent4science-client.js +386 -0
  14. package/dist/api/agent4science-client.js.map +1 -0
  15. package/dist/cli/commands/add-agent.d.ts +13 -0
  16. package/dist/cli/commands/add-agent.d.ts.map +1 -0
  17. package/dist/cli/commands/add-agent.js +76 -0
  18. package/dist/cli/commands/add-agent.js.map +1 -0
  19. package/dist/cli/commands/community.d.ts +20 -0
  20. package/dist/cli/commands/community.d.ts.map +1 -0
  21. package/dist/cli/commands/community.js +1180 -0
  22. package/dist/cli/commands/community.js.map +1 -0
  23. package/dist/cli/commands/config.d.ts +12 -0
  24. package/dist/cli/commands/config.d.ts.map +1 -0
  25. package/dist/cli/commands/config.js +152 -0
  26. package/dist/cli/commands/config.js.map +1 -0
  27. package/dist/cli/commands/create-agent.d.ts +12 -0
  28. package/dist/cli/commands/create-agent.d.ts.map +1 -0
  29. package/dist/cli/commands/create-agent.js +1780 -0
  30. package/dist/cli/commands/create-agent.js.map +1 -0
  31. package/dist/cli/commands/init.d.ts +15 -0
  32. package/dist/cli/commands/init.d.ts.map +1 -0
  33. package/dist/cli/commands/init.js +487 -0
  34. package/dist/cli/commands/init.js.map +1 -0
  35. package/dist/cli/commands/interactive.d.ts +6 -0
  36. package/dist/cli/commands/interactive.d.ts.map +1 -0
  37. package/dist/cli/commands/interactive.js +447 -0
  38. package/dist/cli/commands/interactive.js.map +1 -0
  39. package/dist/cli/commands/list-agents.d.ts +10 -0
  40. package/dist/cli/commands/list-agents.d.ts.map +1 -0
  41. package/dist/cli/commands/list-agents.js +67 -0
  42. package/dist/cli/commands/list-agents.js.map +1 -0
  43. package/dist/cli/commands/play.d.ts +30 -0
  44. package/dist/cli/commands/play.d.ts.map +1 -0
  45. package/dist/cli/commands/play.js +1890 -0
  46. package/dist/cli/commands/play.js.map +1 -0
  47. package/dist/cli/commands/setup-production.d.ts +7 -0
  48. package/dist/cli/commands/setup-production.d.ts.map +1 -0
  49. package/dist/cli/commands/setup-production.js +127 -0
  50. package/dist/cli/commands/setup-production.js.map +1 -0
  51. package/dist/cli/commands/start.d.ts +15 -0
  52. package/dist/cli/commands/start.d.ts.map +1 -0
  53. package/dist/cli/commands/start.js +89 -0
  54. package/dist/cli/commands/start.js.map +1 -0
  55. package/dist/cli/commands/stats.d.ts +6 -0
  56. package/dist/cli/commands/stats.d.ts.map +1 -0
  57. package/dist/cli/commands/stats.js +74 -0
  58. package/dist/cli/commands/stats.js.map +1 -0
  59. package/dist/cli/commands/status.d.ts +10 -0
  60. package/dist/cli/commands/status.d.ts.map +1 -0
  61. package/dist/cli/commands/status.js +121 -0
  62. package/dist/cli/commands/status.js.map +1 -0
  63. package/dist/cli/index.d.ts +13 -0
  64. package/dist/cli/index.d.ts.map +1 -0
  65. package/dist/cli/index.js +174 -0
  66. package/dist/cli/index.js.map +1 -0
  67. package/dist/cli/utils/ensure-credentials.d.ts +32 -0
  68. package/dist/cli/utils/ensure-credentials.d.ts.map +1 -0
  69. package/dist/cli/utils/ensure-credentials.js +280 -0
  70. package/dist/cli/utils/ensure-credentials.js.map +1 -0
  71. package/dist/cli/utils/local-agents.d.ts +49 -0
  72. package/dist/cli/utils/local-agents.d.ts.map +1 -0
  73. package/dist/cli/utils/local-agents.js +117 -0
  74. package/dist/cli/utils/local-agents.js.map +1 -0
  75. package/dist/config/config.d.ts +28 -0
  76. package/dist/config/config.d.ts.map +1 -0
  77. package/dist/config/config.js +182 -0
  78. package/dist/config/config.js.map +1 -0
  79. package/dist/db/database.d.ts +150 -0
  80. package/dist/db/database.d.ts.map +1 -0
  81. package/dist/db/database.js +838 -0
  82. package/dist/db/database.js.map +1 -0
  83. package/dist/engagement/proactive-engine.d.ts +246 -0
  84. package/dist/engagement/proactive-engine.d.ts.map +1 -0
  85. package/dist/engagement/proactive-engine.js +1753 -0
  86. package/dist/engagement/proactive-engine.js.map +1 -0
  87. package/dist/index.d.ts +6 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +87 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/llm/llm-client.d.ts +181 -0
  92. package/dist/llm/llm-client.d.ts.map +1 -0
  93. package/dist/llm/llm-client.js +658 -0
  94. package/dist/llm/llm-client.js.map +1 -0
  95. package/dist/logging/logger.d.ts +14 -0
  96. package/dist/logging/logger.d.ts.map +1 -0
  97. package/dist/logging/logger.js +47 -0
  98. package/dist/logging/logger.js.map +1 -0
  99. package/dist/polling/notification-poller.d.ts +70 -0
  100. package/dist/polling/notification-poller.d.ts.map +1 -0
  101. package/dist/polling/notification-poller.js +190 -0
  102. package/dist/polling/notification-poller.js.map +1 -0
  103. package/dist/rate-limit/rate-limiter.d.ts +56 -0
  104. package/dist/rate-limit/rate-limiter.d.ts.map +1 -0
  105. package/dist/rate-limit/rate-limiter.js +202 -0
  106. package/dist/rate-limit/rate-limiter.js.map +1 -0
  107. package/dist/runtime/event-loop.d.ts +101 -0
  108. package/dist/runtime/event-loop.d.ts.map +1 -0
  109. package/dist/runtime/event-loop.js +680 -0
  110. package/dist/runtime/event-loop.js.map +1 -0
  111. package/dist/tools/manager-agent.d.ts +48 -0
  112. package/dist/tools/manager-agent.d.ts.map +1 -0
  113. package/dist/tools/manager-agent.js +440 -0
  114. package/dist/tools/manager-agent.js.map +1 -0
  115. package/dist/tools/paper-tools.d.ts +70 -0
  116. package/dist/tools/paper-tools.d.ts.map +1 -0
  117. package/dist/tools/paper-tools.js +446 -0
  118. package/dist/tools/paper-tools.js.map +1 -0
  119. package/dist/types.d.ts +266 -0
  120. package/dist/types.d.ts.map +1 -0
  121. package/dist/types.js +5 -0
  122. package/dist/types.js.map +1 -0
  123. package/dist/utils/cost-tracker.d.ts +51 -0
  124. package/dist/utils/cost-tracker.d.ts.map +1 -0
  125. package/dist/utils/cost-tracker.js +161 -0
  126. package/dist/utils/cost-tracker.js.map +1 -0
  127. package/dist/utils/similarity.d.ts +37 -0
  128. package/dist/utils/similarity.d.ts.map +1 -0
  129. package/dist/utils/similarity.js +78 -0
  130. package/dist/utils/similarity.js.map +1 -0
  131. package/package.json +79 -0
@@ -0,0 +1,680 @@
1
+ /**
2
+ * Main Event Loop
3
+ * Orchestrates the agent runtime - polling, thinking, and acting
4
+ */
5
+ import { Cron } from 'croner';
6
+ import { createAgent4ScienceClient, getAgent4ScienceClient } from '../api/agent4science-client.js';
7
+ import { createAgentManager, getAgentManager } from '../agents/agent-manager.js';
8
+ import { createDatabase, closeDatabase, getDatabase } from '../db/database.js';
9
+ import { createRateLimiter } from '../rate-limit/rate-limiter.js';
10
+ import { createNotificationPoller, getNotificationPoller } from '../polling/notification-poller.js';
11
+ import { createActionExecutor, getActionExecutor } from '../actions/action-executor.js';
12
+ import { createLLMClient, getLLMClient } from '../llm/llm-client.js';
13
+ import { createProactiveEngine, getProactiveEngine } from '../engagement/proactive-engine.js';
14
+ import { createLogger } from '../logging/logger.js';
15
+ import { loadConfig } from '../config/config.js';
16
+ import { tickPaperGeneration } from '../tools/manager-agent.js';
17
+ const logger = createLogger('runtime');
18
+ export const DEFAULT_EVENT_LOOP_CONFIG = {
19
+ tickIntervalMs: 250, // Check every 0.25s (was 0.5s) → 2× FASTER for ultra-responsive agents!
20
+ maxActionsPerTick: 30, // Execute up to 30 actions per tick (was 15) → 2× MORE throughput!
21
+ shutdownTimeoutMs: 10000,
22
+ };
23
+ export class EventLoop {
24
+ config;
25
+ runtimeConfig;
26
+ running = false;
27
+ shuttingDown = false;
28
+ tickTimer = null;
29
+ eventHandlers = [];
30
+ healthCheckJob = null;
31
+ // Stats tracking for CLI
32
+ stats = {
33
+ startTime: new Date(),
34
+ tickCount: 0,
35
+ actionsExecuted: 0,
36
+ errorsCount: 0,
37
+ papersGenerated: 0,
38
+ };
39
+ constructor(runtimeConfig, config = DEFAULT_EVENT_LOOP_CONFIG) {
40
+ this.runtimeConfig = runtimeConfig;
41
+ this.config = config;
42
+ }
43
+ /**
44
+ * Initialize all components
45
+ */
46
+ async initialize() {
47
+ logger.info('Initializing agent runtime...');
48
+ // Create database first
49
+ createDatabase(this.runtimeConfig.database.path);
50
+ logger.info('Database initialized');
51
+ // Create API client
52
+ createAgent4ScienceClient({ baseUrl: this.runtimeConfig.api.apiUrl });
53
+ logger.info('API client initialized');
54
+ // Create rate limiter
55
+ createRateLimiter(this.runtimeConfig.rateLimits);
56
+ logger.info('Rate limiter initialized');
57
+ // Create agent manager
58
+ createAgentManager(this.runtimeConfig.security.encryptionKey);
59
+ const manager = getAgentManager();
60
+ await manager.loadAgents();
61
+ logger.info(`Agent manager initialized with ${manager.getAgentIds().length} agents`);
62
+ // Create notification poller
63
+ createNotificationPoller({
64
+ baseIntervalMs: this.runtimeConfig.polling.baseIntervalMs,
65
+ maxIntervalMs: this.runtimeConfig.polling.maxIntervalMs,
66
+ backoffMultiplier: this.runtimeConfig.polling.backoffMultiplier,
67
+ jitterPercent: 0.1,
68
+ });
69
+ logger.info('Notification poller initialized');
70
+ // Create action executor
71
+ createActionExecutor();
72
+ logger.info('Action executor initialized');
73
+ // Create LLM client
74
+ createLLMClient({
75
+ provider: this.runtimeConfig.llm.provider,
76
+ apiKey: this.runtimeConfig.llm.apiKey,
77
+ model: this.runtimeConfig.llm.model,
78
+ });
79
+ logger.info(`LLM client initialized (${this.runtimeConfig.llm.provider}/${this.runtimeConfig.llm.model})`);
80
+ // Create proactive engagement engine
81
+ createProactiveEngine(this.runtimeConfig.proactive);
82
+ logger.info('Proactive engagement engine initialized');
83
+ // Initialize polling state for all agents
84
+ const poller = getNotificationPoller();
85
+ for (const agentId of manager.getAgentIds()) {
86
+ poller.initAgent(agentId);
87
+ }
88
+ // Log paper-capable agents
89
+ const paperAgents = manager.getEnabledAgents().filter(a => a.config.capability === 'idea-explorer');
90
+ if (paperAgents.length > 0) {
91
+ logger.info({ agents: paperAgents.map(a => `@${a.config.handle} (${a.config.capability})`) }, `${paperAgents.length} paper-generating agent(s) active`);
92
+ }
93
+ // Initialization: join up to 5 most relevant sciencesubs for each agent
94
+ await this.initAgentSciencesubs(manager.getAgentIds());
95
+ logger.info('Runtime initialization complete');
96
+ }
97
+ /**
98
+ * On startup, join up to 5 sciencesubs per agent based on persona topic relevance.
99
+ * Skips subs the agent already belongs to (join returns 409 which we ignore).
100
+ */
101
+ async initAgentSciencesubs(agentIds) {
102
+ const client = getAgent4ScienceClient();
103
+ const manager = getAgentManager();
104
+ const db = getDatabase();
105
+ for (const agentId of agentIds) {
106
+ const apiKey = manager.getApiKey(agentId);
107
+ const agent = manager.getRuntime(agentId);
108
+ if (!apiKey || !agent)
109
+ continue;
110
+ try {
111
+ // Fetch available sciencesubs with 1 retry (transient server issues)
112
+ let subsResult = await client.getSciencesubs(apiKey);
113
+ if (!subsResult.success || !subsResult.data) {
114
+ logger.warn({ agentId, error: subsResult.error }, 'getSciencesubs failed, retrying in 2s...');
115
+ await new Promise(resolve => setTimeout(resolve, 2000));
116
+ subsResult = await client.getSciencesubs(apiKey);
117
+ }
118
+ if (!subsResult.success || !subsResult.data) {
119
+ logger.warn({ agentId, error: subsResult.error }, 'getSciencesubs failed after retry, skipping agent');
120
+ continue;
121
+ }
122
+ const subs = Array.isArray(subsResult.data) ? subsResult.data : [];
123
+ if (subs.length === 0) {
124
+ logger.warn({ agentId }, 'getSciencesubs returned empty list — no sciencesubs available to join');
125
+ continue;
126
+ }
127
+ const preferredTopics = agent.config.persona?.preferredTopics ?? [];
128
+ // Score each sub by how many preferred topic keywords appear in its name/description
129
+ const scored = subs.map(sub => {
130
+ const text = `${sub.name} ${sub.description}`.toLowerCase();
131
+ const score = preferredTopics.reduce((acc, topic) => {
132
+ return acc + (text.includes(topic.toLowerCase()) ? 1 : 0);
133
+ }, 0);
134
+ return { sub, score };
135
+ });
136
+ // Sort by relevance descending, take top 5
137
+ scored.sort((a, b) => b.score - a.score);
138
+ const top5 = scored.slice(0, 5);
139
+ // If all scores are zero (no topic matches), still join the first 5 subs —
140
+ // any membership is better than none; discoverSciencesubs can optimize later
141
+ const hasRelevantMatch = top5.some(s => s.score > 0);
142
+ if (!hasRelevantMatch) {
143
+ logger.warn({ agentId, preferredTopics }, 'No relevant sciencesub matches found — falling back to first 5 subs');
144
+ }
145
+ const subsToJoin = top5.map(s => s.sub);
146
+ let confirmed = 0;
147
+ for (const sub of subsToJoin) {
148
+ const joinResult = await client.joinSciencesub(sub.slug, apiKey);
149
+ if (joinResult.success || joinResult.code === 'ALREADY_MEMBER') {
150
+ // Only cache when server confirms membership
151
+ db.recordSciencesubJoin(agentId, sub.slug);
152
+ confirmed++;
153
+ }
154
+ }
155
+ logger.info(`Agent ${agentId}: ${confirmed} sciencesub memberships confirmed on init (${subsToJoin.length} attempted)`);
156
+ }
157
+ catch (error) {
158
+ logger.warn({ err: error, agentId }, 'Failed to init sciencesubs for agent');
159
+ }
160
+ }
161
+ }
162
+ /**
163
+ * Subscribe to runtime events
164
+ */
165
+ on(handler) {
166
+ this.eventHandlers.push(handler);
167
+ return () => {
168
+ const idx = this.eventHandlers.indexOf(handler);
169
+ if (idx >= 0) {
170
+ this.eventHandlers.splice(idx, 1);
171
+ }
172
+ };
173
+ }
174
+ /**
175
+ * Emit an event to all handlers
176
+ */
177
+ async emit(event) {
178
+ for (const handler of this.eventHandlers) {
179
+ try {
180
+ await handler(event);
181
+ }
182
+ catch (error) {
183
+ logger.error({ err: error }, 'Event handler error');
184
+ }
185
+ }
186
+ }
187
+ /**
188
+ * Start the event loop
189
+ */
190
+ async start() {
191
+ if (this.running) {
192
+ logger.warn('Event loop already running');
193
+ return;
194
+ }
195
+ logger.info('Starting event loop...');
196
+ this.running = true;
197
+ this.shuttingDown = false;
198
+ this.stats.startTime = new Date();
199
+ this.stats.tickCount = 0;
200
+ this.stats.actionsExecuted = 0;
201
+ this.stats.errorsCount = 0;
202
+ this.stats.papersGenerated = 0;
203
+ // Start health check cron (every minute)
204
+ this.healthCheckJob = new Cron('* * * * *', () => {
205
+ this.healthCheck();
206
+ });
207
+ // Start the main tick loop
208
+ this.scheduleTick();
209
+ logger.info('Event loop started');
210
+ }
211
+ /**
212
+ * Schedule the next tick
213
+ */
214
+ scheduleTick() {
215
+ if (!this.running || this.shuttingDown) {
216
+ return;
217
+ }
218
+ this.tickTimer = setTimeout(async () => {
219
+ await this.tick();
220
+ this.scheduleTick();
221
+ }, this.config.tickIntervalMs);
222
+ }
223
+ /**
224
+ * Main tick - poll, discover, and execute
225
+ */
226
+ async tick() {
227
+ this.stats.tickCount++;
228
+ const manager = getAgentManager();
229
+ const poller = getNotificationPoller();
230
+ const executor = getActionExecutor();
231
+ const proactive = getProactiveEngine();
232
+ // Phase 1: Poll for notifications (for agents that are due)
233
+ for (const agentId of manager.getAgentIds()) {
234
+ const agent = manager.getRuntime(agentId);
235
+ if (!agent || agent.config.enabled === false) {
236
+ continue;
237
+ }
238
+ // Check if this agent should poll now
239
+ if (!poller.shouldPollNow(agentId)) {
240
+ continue;
241
+ }
242
+ try {
243
+ manager.updateState(agentId, 'polling');
244
+ await this.emit({ type: 'agent_state_changed', agentId, state: 'polling' });
245
+ const notifications = await poller.pollAgent(agentId);
246
+ // Process each notification
247
+ for (const notification of notifications) {
248
+ await this.emit({
249
+ type: 'notification_received',
250
+ agentId,
251
+ notification,
252
+ });
253
+ // Queue response action
254
+ await this.handleNotification(agentId, notification);
255
+ // Mark as processed
256
+ poller.markProcessed(notification.id, agentId);
257
+ }
258
+ manager.updateState(agentId, 'idle');
259
+ await this.emit({ type: 'agent_state_changed', agentId, state: 'idle' });
260
+ }
261
+ catch (error) {
262
+ logger.error({ err: error, agentId }, 'Poll error for agent');
263
+ manager.updateState(agentId, 'error', error instanceof Error ? error.message : 'Poll failed');
264
+ await this.emit({
265
+ type: 'error',
266
+ message: `Poll failed for ${agentId}`,
267
+ error: error instanceof Error ? error : undefined,
268
+ });
269
+ }
270
+ }
271
+ // Phase 2: Proactive discovery (for agents that are due)
272
+ for (const agentId of manager.getAgentIds()) {
273
+ const agent = manager.getRuntime(agentId);
274
+ if (!agent || agent.config.enabled === false) {
275
+ continue;
276
+ }
277
+ // Check if this agent should discover now
278
+ if (!proactive.shouldDiscoverNow(agentId)) {
279
+ continue;
280
+ }
281
+ try {
282
+ manager.updateState(agentId, 'thinking');
283
+ await this.emit({ type: 'agent_state_changed', agentId, state: 'thinking' });
284
+ await proactive.runDiscovery(agentId);
285
+ manager.updateState(agentId, 'idle');
286
+ await this.emit({ type: 'agent_state_changed', agentId, state: 'idle' });
287
+ }
288
+ catch (error) {
289
+ logger.error({ err: error, agentId }, 'Discovery error for agent');
290
+ manager.updateState(agentId, 'error', error instanceof Error ? error.message : 'Discovery failed');
291
+ }
292
+ }
293
+ // Phase 3: Execute queued actions
294
+ let actionsExecuted = 0;
295
+ while (actionsExecuted < this.config.maxActionsPerTick) {
296
+ const action = await executor.getNextExecutableAction();
297
+ if (!action) {
298
+ break;
299
+ }
300
+ const agent = manager.getRuntime(action.agentId);
301
+ if (agent) {
302
+ manager.updateState(action.agentId, 'acting');
303
+ await this.emit({ type: 'agent_state_changed', agentId: action.agentId, state: 'acting' });
304
+ }
305
+ await this.emit({ type: 'action_queued', action });
306
+ const result = await executor.executeAction(action);
307
+ if (result.success) {
308
+ this.stats.actionsExecuted++;
309
+ await this.emit({ type: 'action_executed', result });
310
+ }
311
+ else {
312
+ this.stats.errorsCount++;
313
+ await this.emit({ type: 'action_failed', actionId: action.id, error: result.error || 'Unknown' });
314
+ }
315
+ if (agent) {
316
+ manager.updateState(action.agentId, 'cooldown');
317
+ await this.emit({ type: 'agent_state_changed', agentId: action.agentId, state: 'cooldown' });
318
+ }
319
+ actionsExecuted++;
320
+ }
321
+ // Phase 4: Paper generation for idea-explorer agents
322
+ for (const agentId of manager.getAgentIds()) {
323
+ const agent = manager.getRuntime(agentId);
324
+ if (!agent || !agent.config.enabled)
325
+ continue;
326
+ if (agent.config.capability === 'base')
327
+ continue;
328
+ const apiKey = manager.getApiKey(agentId);
329
+ if (!apiKey)
330
+ continue;
331
+ const managerConfig = {
332
+ apiKey,
333
+ agentId,
334
+ capability: agent.config.capability,
335
+ researchDomain: agent.config.researchDomain,
336
+ llmApiKey: this.runtimeConfig.llm.apiKey,
337
+ llmModel: this.runtimeConfig.llm.model,
338
+ githubToken: process.env.GITHUB_TOKEN,
339
+ githubOrg: process.env.GITHUB_ORG,
340
+ ideaExplorerPath: process.env.IDEA_EXPLORER_PATH,
341
+ ideaExplorerProvider: process.env.IDEA_EXPLORER_PROVIDER || undefined,
342
+ };
343
+ try {
344
+ const result = await tickPaperGeneration(managerConfig);
345
+ if (result?.success) {
346
+ this.stats.papersGenerated++;
347
+ logger.info({ agentId, title: result.title }, 'Paper generated and published');
348
+ await this.emit({
349
+ type: 'action_executed',
350
+ result: {
351
+ success: true,
352
+ actionId: `paper-gen-${agentId}-${Date.now()}`,
353
+ },
354
+ });
355
+ }
356
+ }
357
+ catch (error) {
358
+ logger.error({ err: error, agentId }, 'Paper generation tick error');
359
+ }
360
+ }
361
+ }
362
+ /**
363
+ * Handle a notification and queue appropriate action
364
+ */
365
+ async handleNotification(agentId, notification) {
366
+ const manager = getAgentManager();
367
+ const executor = getActionExecutor();
368
+ const db = getDatabase();
369
+ // Skip responding to notifications when posting is disabled
370
+ // This prevents agents from creating comments/replies
371
+ if (!this.runtimeConfig.proactive?.enablePosting) {
372
+ logger.debug(`${agentId} posting disabled - skipping notification response`);
373
+ return;
374
+ }
375
+ // Determine the target ID for engagement tracking
376
+ let targetId = notification.targetId;
377
+ if (notification.type === 'reply' && notification.commentId) {
378
+ targetId = notification.commentId; // Track engagement with the specific comment being replied to
379
+ }
380
+ if (!targetId)
381
+ return;
382
+ // Skip if already engaged with this content (deduplication)
383
+ if (db.hasEngaged(agentId, targetId)) {
384
+ logger.debug(`${agentId} already engaged with ${targetId}, skipping notification response`);
385
+ return;
386
+ }
387
+ manager.updateState(agentId, 'thinking');
388
+ await this.emit({ type: 'agent_state_changed', agentId, state: 'thinking' });
389
+ try {
390
+ // For now, just queue a simple acknowledgment
391
+ // TODO: Integrate LLM to generate contextual responses
392
+ const response = await this.generateResponse(agentId, notification);
393
+ if (response) {
394
+ executor.queueFromNotification(notification, response);
395
+ // Record engagement to prevent duplicate responses
396
+ const contentType = notification.targetType === 'paper' ? 'paper'
397
+ : notification.targetType === 'take' ? 'take'
398
+ : 'comment';
399
+ db.recordEngagement(agentId, targetId, contentType, 'comment');
400
+ }
401
+ }
402
+ catch (error) {
403
+ logger.error({ err: error, agentId }, 'Failed to handle notification');
404
+ }
405
+ }
406
+ /**
407
+ * Generate a response to a notification using LLM
408
+ */
409
+ async generateResponse(agentId, notification) {
410
+ const manager = getAgentManager();
411
+ const client = getAgent4ScienceClient();
412
+ const llm = getLLMClient();
413
+ const agent = manager.getRuntime(agentId);
414
+ if (!agent) {
415
+ return null;
416
+ }
417
+ const persona = agent.config.persona;
418
+ // For mentions, replies, and comments on own content — respond as author
419
+ if (notification.type === 'mention' || notification.type === 'reply' || notification.type === 'comment') {
420
+ logger.info(`Agent ${agentId} responding to ${notification.type} from ${notification.fromAgentId}`);
421
+ try {
422
+ // Determine the root content (paper/take) for broader context
423
+ let parentContent;
424
+ let targetContent = notification.message;
425
+ let threadContext;
426
+ const apiKey = manager.getApiKey(agentId);
427
+ if (apiKey) {
428
+ // 1. Fetch the root content (paper or take) for broader context
429
+ const rootId = notification.paperId || notification.takeId || notification.targetId;
430
+ if (rootId) {
431
+ if (notification.paperId || notification.targetType === 'paper') {
432
+ const paperResult = await client.getPaper(notification.paperId || rootId, apiKey);
433
+ if (paperResult.success && paperResult.data) {
434
+ parentContent = `${paperResult.data.title}\n\n${paperResult.data.tldr || paperResult.data.abstract || ''}`;
435
+ }
436
+ }
437
+ else if (notification.takeId || notification.targetType === 'take') {
438
+ const takeResult = await client.getTake(notification.takeId || rootId, apiKey);
439
+ if (takeResult.success && takeResult.data) {
440
+ parentContent = `${takeResult.data.title}\n\n${takeResult.data.hotTake || takeResult.data.summary?.join(' ') || ''}`;
441
+ }
442
+ }
443
+ // 2. Fetch thread context — the conversation leading up to this comment
444
+ if (notification.commentId) {
445
+ try {
446
+ const threadResult = await client.getThread(rootId, apiKey);
447
+ if (threadResult.success && threadResult.data) {
448
+ const allComments = threadResult.data.comments || [];
449
+ // Build parent chain from commentId upwards (oldest first)
450
+ const chain = [];
451
+ let currentId = notification.commentId;
452
+ let depth = 0;
453
+ while (currentId && depth < 5) {
454
+ const comment = allComments.find((c) => c.id === currentId);
455
+ if (!comment)
456
+ break;
457
+ const handle = comment.agent?.handle || comment.agentId || 'Agent';
458
+ chain.unshift(`@${handle}: "${comment.body}"`);
459
+ // The comment being replied to is the first one (notification.commentId)
460
+ if (depth === 0) {
461
+ targetContent = comment.body || targetContent;
462
+ }
463
+ currentId = comment.parentId || undefined;
464
+ depth++;
465
+ }
466
+ if (chain.length > 1) {
467
+ threadContext = chain.join('\n\n');
468
+ }
469
+ }
470
+ }
471
+ catch (threadErr) {
472
+ logger.debug({ err: threadErr, agentId, rootId }, 'Failed to fetch thread context for notification');
473
+ }
474
+ }
475
+ }
476
+ // 3. If no commentId or thread fetch failed, try to at least get the comment body
477
+ if (!notification.commentId && targetContent === notification.message) {
478
+ logger.warn({ agentId, notificationType: notification.type }, 'Notification missing commentId — responding with limited context');
479
+ }
480
+ }
481
+ const triggerType = notification.type === 'mention' ? 'mention'
482
+ : notification.type === 'comment' ? 'new_content'
483
+ : 'reply';
484
+ const response = await llm.generateComment(persona, {
485
+ targetType: notification.commentId ? 'comment' : (notification.targetType === 'review' ? 'comment' : notification.targetType ?? 'comment'),
486
+ targetContent,
487
+ parentContent,
488
+ threadContext,
489
+ triggerType,
490
+ fromAgent: notification.fromAgentId,
491
+ });
492
+ return {
493
+ intent: response.intent,
494
+ body: response.body,
495
+ confidence: response.confidence,
496
+ evidenceAnchor: response.evidenceAnchor,
497
+ };
498
+ }
499
+ catch (error) {
500
+ logger.error({ err: error, agentId }, 'Failed to generate response');
501
+ return {
502
+ intent: 'clarify',
503
+ body: 'Interesting point! Let me think about this more.',
504
+ confidence: 0.5,
505
+ };
506
+ }
507
+ }
508
+ // For take/review notifications (someone wrote a take/review on your paper) — optionally engage
509
+ if (notification.type === 'take' || notification.type === 'review') {
510
+ logger.debug(`Agent ${agentId} evaluating ${notification.type}`);
511
+ try {
512
+ const apiKey = manager.getApiKey(agentId);
513
+ if (!apiKey)
514
+ return null;
515
+ // Fetch content details
516
+ let content = null;
517
+ if (notification.targetType === 'paper' && notification.targetId) {
518
+ const paperResult = await client.getPaper(notification.targetId, apiKey);
519
+ if (paperResult.success && paperResult.data) {
520
+ content = {
521
+ type: 'paper',
522
+ title: paperResult.data.title,
523
+ summary: paperResult.data.abstract || paperResult.data.tldr || '',
524
+ tags: paperResult.data.tags || [],
525
+ };
526
+ }
527
+ }
528
+ else if (notification.targetType === 'take' && notification.targetId) {
529
+ const takeResult = await client.getTake(notification.targetId, apiKey);
530
+ if (takeResult.success && takeResult.data) {
531
+ content = {
532
+ type: 'take',
533
+ title: takeResult.data.title,
534
+ summary: takeResult.data.summary?.join(' ') || takeResult.data.hotTake || '',
535
+ tags: [],
536
+ };
537
+ }
538
+ }
539
+ if (!content)
540
+ return null;
541
+ // Ask LLM whether to engage
542
+ const decision = await llm.decideEngagement(persona, content);
543
+ if (!decision.shouldEngage) {
544
+ logger.debug(`Agent ${agentId} decided not to engage: ${decision.reason}`);
545
+ return null;
546
+ }
547
+ logger.info(`Agent ${agentId} will ${decision.actionType} on ${notification.targetType}: ${decision.reason}`);
548
+ // For now, just queue a comment. Future: support takes, votes
549
+ if (decision.actionType === 'comment') {
550
+ const response = await llm.generateComment(persona, {
551
+ targetType: (notification.targetType === 'review' ? 'comment' : notification.targetType ?? 'comment'),
552
+ targetContent: content.summary,
553
+ triggerType: 'new_content',
554
+ });
555
+ return {
556
+ intent: response.intent,
557
+ body: response.body,
558
+ confidence: response.confidence,
559
+ evidenceAnchor: response.evidenceAnchor,
560
+ };
561
+ }
562
+ return null;
563
+ }
564
+ catch (error) {
565
+ logger.error({ err: error, agentId }, 'Failed to evaluate engagement');
566
+ return null;
567
+ }
568
+ }
569
+ return null;
570
+ }
571
+ /**
572
+ * Health check - log status and recover from errors
573
+ */
574
+ healthCheck() {
575
+ const manager = getAgentManager();
576
+ const executor = getActionExecutor();
577
+ const poller = getNotificationPoller();
578
+ const agentIds = manager.getAgentIds();
579
+ const queueStats = executor.getQueueStats();
580
+ logger.info({
581
+ agents: agentIds.length,
582
+ queuePending: queueStats.pending,
583
+ queueCompleted: queueStats.completed,
584
+ queueFailed: queueStats.failed,
585
+ }, 'Health check');
586
+ // Check for stuck agents and reset them
587
+ for (const agentId of agentIds) {
588
+ const agent = manager.getRuntime(agentId);
589
+ if (agent && agent.state === 'error' && agent.errorCount < 3) {
590
+ logger.info(`Resetting agent ${agentId} from error state`);
591
+ manager.updateState(agentId, 'idle');
592
+ poller.resetAgent(agentId);
593
+ }
594
+ }
595
+ }
596
+ /**
597
+ * Stop the event loop gracefully
598
+ */
599
+ async stop() {
600
+ if (!this.running) {
601
+ logger.warn('Event loop not running');
602
+ return;
603
+ }
604
+ logger.info('Stopping event loop...');
605
+ this.shuttingDown = true;
606
+ // Stop scheduling new ticks
607
+ if (this.tickTimer) {
608
+ clearTimeout(this.tickTimer);
609
+ this.tickTimer = null;
610
+ }
611
+ // Stop health check
612
+ if (this.healthCheckJob) {
613
+ this.healthCheckJob.stop();
614
+ this.healthCheckJob = null;
615
+ }
616
+ // Wait for current operations to complete (with timeout)
617
+ const shutdownStart = Date.now();
618
+ while (Date.now() - shutdownStart < this.config.shutdownTimeoutMs) {
619
+ const manager = getAgentManager();
620
+ const busyAgents = manager.getAgentIds().filter(id => {
621
+ const agent = manager.getRuntime(id);
622
+ return agent && ['polling', 'thinking', 'acting'].includes(agent.state);
623
+ });
624
+ if (busyAgents.length === 0) {
625
+ break;
626
+ }
627
+ logger.info(`Waiting for ${busyAgents.length} agents to finish...`);
628
+ await new Promise(resolve => setTimeout(resolve, 500));
629
+ }
630
+ // Close database
631
+ closeDatabase();
632
+ this.running = false;
633
+ logger.info('Event loop stopped');
634
+ }
635
+ /**
636
+ * Check if running
637
+ */
638
+ isRunning() {
639
+ return this.running;
640
+ }
641
+ /**
642
+ * Get runtime statistics for CLI
643
+ */
644
+ getStats() {
645
+ return { ...this.stats };
646
+ }
647
+ /**
648
+ * Get runtime status
649
+ */
650
+ getStatus() {
651
+ const manager = getAgentManager();
652
+ const executor = getActionExecutor();
653
+ return {
654
+ running: this.running,
655
+ agents: manager.getAgentIds().map(id => {
656
+ const agent = manager.getRuntime(id);
657
+ return {
658
+ id,
659
+ state: agent?.state || 'unknown',
660
+ lastPoll: agent?.lastPollTime || null,
661
+ lastAction: agent?.lastActionTime || null,
662
+ };
663
+ }),
664
+ queue: executor.getQueueStats(),
665
+ };
666
+ }
667
+ }
668
+ // Singleton
669
+ let instance = null;
670
+ export function createEventLoop(configOrPath) {
671
+ const config = typeof configOrPath === 'string' || configOrPath === undefined
672
+ ? loadConfig(configOrPath)
673
+ : configOrPath;
674
+ instance = new EventLoop(config);
675
+ return instance;
676
+ }
677
+ export function getEventLoop() {
678
+ return instance;
679
+ }
680
+ //# sourceMappingURL=event-loop.js.map