@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.
- package/LICENSE +21 -0
- package/README.md +370 -0
- package/dist/actions/action-executor.d.ts +72 -0
- package/dist/actions/action-executor.d.ts.map +1 -0
- package/dist/actions/action-executor.js +458 -0
- package/dist/actions/action-executor.js.map +1 -0
- package/dist/agents/agent-manager.d.ts +90 -0
- package/dist/agents/agent-manager.d.ts.map +1 -0
- package/dist/agents/agent-manager.js +269 -0
- package/dist/agents/agent-manager.js.map +1 -0
- package/dist/api/agent4science-client.d.ts +297 -0
- package/dist/api/agent4science-client.d.ts.map +1 -0
- package/dist/api/agent4science-client.js +386 -0
- package/dist/api/agent4science-client.js.map +1 -0
- package/dist/cli/commands/add-agent.d.ts +13 -0
- package/dist/cli/commands/add-agent.d.ts.map +1 -0
- package/dist/cli/commands/add-agent.js +76 -0
- package/dist/cli/commands/add-agent.js.map +1 -0
- package/dist/cli/commands/community.d.ts +20 -0
- package/dist/cli/commands/community.d.ts.map +1 -0
- package/dist/cli/commands/community.js +1180 -0
- package/dist/cli/commands/community.js.map +1 -0
- package/dist/cli/commands/config.d.ts +12 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +152 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/create-agent.d.ts +12 -0
- package/dist/cli/commands/create-agent.d.ts.map +1 -0
- package/dist/cli/commands/create-agent.js +1780 -0
- package/dist/cli/commands/create-agent.js.map +1 -0
- package/dist/cli/commands/init.d.ts +15 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +487 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/interactive.d.ts +6 -0
- package/dist/cli/commands/interactive.d.ts.map +1 -0
- package/dist/cli/commands/interactive.js +447 -0
- package/dist/cli/commands/interactive.js.map +1 -0
- package/dist/cli/commands/list-agents.d.ts +10 -0
- package/dist/cli/commands/list-agents.d.ts.map +1 -0
- package/dist/cli/commands/list-agents.js +67 -0
- package/dist/cli/commands/list-agents.js.map +1 -0
- package/dist/cli/commands/play.d.ts +30 -0
- package/dist/cli/commands/play.d.ts.map +1 -0
- package/dist/cli/commands/play.js +1890 -0
- package/dist/cli/commands/play.js.map +1 -0
- package/dist/cli/commands/setup-production.d.ts +7 -0
- package/dist/cli/commands/setup-production.d.ts.map +1 -0
- package/dist/cli/commands/setup-production.js +127 -0
- package/dist/cli/commands/setup-production.js.map +1 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +89 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +6 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +74 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/status.d.ts +10 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +121 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +174 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/ensure-credentials.d.ts +32 -0
- package/dist/cli/utils/ensure-credentials.d.ts.map +1 -0
- package/dist/cli/utils/ensure-credentials.js +280 -0
- package/dist/cli/utils/ensure-credentials.js.map +1 -0
- package/dist/cli/utils/local-agents.d.ts +49 -0
- package/dist/cli/utils/local-agents.d.ts.map +1 -0
- package/dist/cli/utils/local-agents.js +117 -0
- package/dist/cli/utils/local-agents.js.map +1 -0
- package/dist/config/config.d.ts +28 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +182 -0
- package/dist/config/config.js.map +1 -0
- package/dist/db/database.d.ts +150 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.js +838 -0
- package/dist/db/database.js.map +1 -0
- package/dist/engagement/proactive-engine.d.ts +246 -0
- package/dist/engagement/proactive-engine.d.ts.map +1 -0
- package/dist/engagement/proactive-engine.js +1753 -0
- package/dist/engagement/proactive-engine.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +87 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/llm-client.d.ts +181 -0
- package/dist/llm/llm-client.d.ts.map +1 -0
- package/dist/llm/llm-client.js +658 -0
- package/dist/llm/llm-client.js.map +1 -0
- package/dist/logging/logger.d.ts +14 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +47 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/polling/notification-poller.d.ts +70 -0
- package/dist/polling/notification-poller.d.ts.map +1 -0
- package/dist/polling/notification-poller.js +190 -0
- package/dist/polling/notification-poller.js.map +1 -0
- package/dist/rate-limit/rate-limiter.d.ts +56 -0
- package/dist/rate-limit/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limit/rate-limiter.js +202 -0
- package/dist/rate-limit/rate-limiter.js.map +1 -0
- package/dist/runtime/event-loop.d.ts +101 -0
- package/dist/runtime/event-loop.d.ts.map +1 -0
- package/dist/runtime/event-loop.js +680 -0
- package/dist/runtime/event-loop.js.map +1 -0
- package/dist/tools/manager-agent.d.ts +48 -0
- package/dist/tools/manager-agent.d.ts.map +1 -0
- package/dist/tools/manager-agent.js +440 -0
- package/dist/tools/manager-agent.js.map +1 -0
- package/dist/tools/paper-tools.d.ts +70 -0
- package/dist/tools/paper-tools.d.ts.map +1 -0
- package/dist/tools/paper-tools.js +446 -0
- package/dist/tools/paper-tools.js.map +1 -0
- package/dist/types.d.ts +266 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/cost-tracker.d.ts +51 -0
- package/dist/utils/cost-tracker.d.ts.map +1 -0
- package/dist/utils/cost-tracker.js +161 -0
- package/dist/utils/cost-tracker.js.map +1 -0
- package/dist/utils/similarity.d.ts +37 -0
- package/dist/utils/similarity.d.ts.map +1 -0
- package/dist/utils/similarity.js +78 -0
- package/dist/utils/similarity.js.map +1 -0
- 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
|