foliko 1.0.53 → 1.0.55

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.
@@ -0,0 +1,1134 @@
1
+ /**
2
+ * Ambient Agent Plugin
3
+ * Continuous background agent that monitors events and takes proactive actions
4
+ */
5
+
6
+ const { Plugin } = require('../src/core/plugin-base')
7
+ const { z } = require('zod')
8
+ const fs = require('fs')
9
+ const path = require('path')
10
+
11
+ // Goal lifecycle states
12
+ const GoalState = {
13
+ PENDING: 'pending',
14
+ ACTIVE: 'active',
15
+ COMPLETED: 'completed',
16
+ FAILED: 'failed'
17
+ }
18
+
19
+ // Generate unique IDs
20
+ function generateId() {
21
+ if (require('crypto').randomUUID) {
22
+ return require('crypto').randomUUID()
23
+ }
24
+ return `goal_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
25
+ }
26
+
27
+ // ============================================================================
28
+ // StateStore - Persists goals and memories to .agent/data/ambient/*.json
29
+ // ============================================================================
30
+ class StateStore {
31
+ constructor(persistencePath) {
32
+ this._persistencePath = persistencePath
33
+ this._ensureDir()
34
+ }
35
+
36
+ _ensureDir() {
37
+ if (!fs.existsSync(this._persistencePath)) {
38
+ fs.mkdirSync(this._persistencePath, { recursive: true })
39
+ }
40
+ }
41
+
42
+ _getGoalsPath() {
43
+ return path.join(this._persistencePath, 'goals.json')
44
+ }
45
+
46
+ _getMemoriesPath() {
47
+ return path.join(this._persistencePath, 'memories.json')
48
+ }
49
+
50
+ saveGoals(goals) {
51
+ try {
52
+ fs.writeFileSync(this._getGoalsPath(), JSON.stringify(goals, null, 2))
53
+ } catch (err) {
54
+ console.error('[Ambient] Failed to save goals:', err.message)
55
+ }
56
+ }
57
+
58
+ loadGoals() {
59
+ try {
60
+ const filePath = this._getGoalsPath()
61
+ if (fs.existsSync(filePath)) {
62
+ const data = fs.readFileSync(filePath, 'utf-8')
63
+ return JSON.parse(data)
64
+ }
65
+ } catch (err) {
66
+ console.error('[Ambient] Failed to load goals:', err.message)
67
+ }
68
+ return []
69
+ }
70
+
71
+ saveMemories(memories) {
72
+ try {
73
+ fs.writeFileSync(this._getMemoriesPath(), JSON.stringify(memories, null, 2))
74
+ } catch (err) {
75
+ console.error('[Ambient] Failed to save memories:', err.message)
76
+ }
77
+ }
78
+
79
+ loadMemories() {
80
+ try {
81
+ const filePath = this._getMemoriesPath()
82
+ if (fs.existsSync(filePath)) {
83
+ const data = fs.readFileSync(filePath, 'utf-8')
84
+ return JSON.parse(data)
85
+ }
86
+ } catch (err) {
87
+ console.error('[Ambient] Failed to load memories:', err.message)
88
+ }
89
+ return []
90
+ }
91
+ }
92
+
93
+ // ============================================================================
94
+ // GoalManager - Manages goals with lifecycle states
95
+ // ============================================================================
96
+ class GoalManager {
97
+ constructor(stateStore) {
98
+ this._goals = new Map()
99
+ this._stateStore = stateStore
100
+ this._loadGoals()
101
+ }
102
+
103
+ _loadGoals() {
104
+ const savedGoals = this._stateStore.loadGoals()
105
+ for (const goal of savedGoals) {
106
+ this._goals.set(goal.id, goal)
107
+ }
108
+ }
109
+
110
+ _persist() {
111
+ const goalsArray = Array.from(this._goals.values())
112
+ this._stateStore.saveGoals(goalsArray)
113
+ }
114
+
115
+ createGoal(goalData) {
116
+ const goal = {
117
+ id: generateId(),
118
+ title: goalData.title || 'Untitled Goal',
119
+ description: goalData.description || '',
120
+ priority: goalData.priority || 5,
121
+ state: GoalState.PENDING,
122
+ actions: goalData.actions || [],
123
+ conditions: goalData.conditions || {},
124
+ createdAt: new Date(),
125
+ updatedAt: new Date(),
126
+ activatedAt: null,
127
+ completedAt: null,
128
+ failedAt: null,
129
+ attempts: 0,
130
+ maxAttempts: goalData.maxAttempts || 10,
131
+ consecutiveSameActions: 0,
132
+ lastActionId: null,
133
+ eventsReceived: []
134
+ }
135
+ this._goals.set(goal.id, goal)
136
+ this._persist()
137
+ return goal
138
+ }
139
+
140
+ getGoal(id) {
141
+ return this._goals.get(id)
142
+ }
143
+
144
+ getAllGoals() {
145
+ return Array.from(this._goals.values())
146
+ }
147
+
148
+ getActiveGoals() {
149
+ return Array.from(this._goals.values()).filter(g => g.state === GoalState.ACTIVE)
150
+ }
151
+
152
+ getPendingGoals() {
153
+ return Array.from(this._goals.values()).filter(g => g.state === GoalState.PENDING)
154
+ }
155
+
156
+ updateGoal(id, updates) {
157
+ const goal = this._goals.get(id)
158
+ if (!goal) return null
159
+ Object.assign(goal, updates, { updatedAt: new Date() })
160
+ this._persist()
161
+ return goal
162
+ }
163
+
164
+ activateGoal(id) {
165
+ const goal = this._goals.get(id)
166
+ if (!goal || goal.state !== GoalState.PENDING) return null
167
+ goal.state = GoalState.ACTIVE
168
+ goal.activatedAt = new Date()
169
+ goal.updatedAt = new Date()
170
+ this._persist()
171
+ return goal
172
+ }
173
+
174
+ completeGoal(id) {
175
+ const goal = this._goals.get(id)
176
+ if (!goal) return null
177
+ goal.state = GoalState.COMPLETED
178
+ goal.completedAt = new Date()
179
+ goal.updatedAt = new Date()
180
+ this._persist()
181
+ return goal
182
+ }
183
+
184
+ failGoal(id, reason) {
185
+ const goal = this._goals.get(id)
186
+ if (!goal) return null
187
+ goal.state = GoalState.FAILED
188
+ goal.failedAt = new Date()
189
+ goal.failureReason = reason
190
+ goal.updatedAt = new Date()
191
+ this._persist()
192
+ return goal
193
+ }
194
+
195
+ deleteGoal(id) {
196
+ const deleted = this._goals.delete(id)
197
+ if (deleted) this._persist()
198
+ return deleted
199
+ }
200
+
201
+ addEventToGoal(goalId, event) {
202
+ const goal = this._goals.get(goalId)
203
+ if (!goal) return
204
+ goal.eventsReceived.push({
205
+ event: event.type, // "email:received"
206
+ data: event.data, // the actual event data (contains email object)
207
+ timestamp: new Date()
208
+ })
209
+ this._persist()
210
+ }
211
+ }
212
+
213
+ // ============================================================================
214
+ // EventWatcher - Subscribes to framework events, filters relevant ones
215
+ // ============================================================================
216
+ class EventWatcher {
217
+ constructor(goalManager, framework) {
218
+ this._goalManager = goalManager
219
+ this._framework = framework
220
+ this._handlers = []
221
+ this._relevantEvents = new Set([
222
+ 'tool:result',
223
+ 'scheduler:reminder',
224
+ 'agent:message',
225
+ 'think:thought_completed',
226
+ 'scheduler:task_completed',
227
+ 'scheduler:task_failed',
228
+ 'email:received',
229
+ 'webhook:received'
230
+ ])
231
+ }
232
+
233
+ start() {
234
+ this._handlers.push(
235
+ this._framework.on('tool:result', (data) => this._handleEvent('tool:result', data)),
236
+ this._framework.on('scheduler:reminder', (data) => this._handleEvent('scheduler:reminder', data)),
237
+ this._framework.on('agent:message', (data) => this._handleEvent('agent:message', data)),
238
+ this._framework.on('think:thought_completed', (data) => this._handleEvent('think:thought_completed', data)),
239
+ this._framework.on('scheduler:task_completed', (data) => this._handleEvent('scheduler:task_completed', data)),
240
+ this._framework.on('scheduler:task_failed', (data) => this._handleEvent('scheduler:task_failed', data)),
241
+ this._framework.on('email:received', (data) => this._handleEvent('email:received', data)),
242
+ this._framework.on('webhook:received', (data) => this._handleEvent('webhook:received', data))
243
+ )
244
+ }
245
+
246
+ stop() {
247
+ for (const handler of this._handlers) {
248
+ handler()
249
+ }
250
+ this._handlers = []
251
+ }
252
+
253
+ _handleEvent(type, data) {
254
+ // Check active goals
255
+ const activeGoals = this._goalManager.getActiveGoals()
256
+ for (const goal of activeGoals) {
257
+ if (this._isRelevantToGoal(goal, type, data)) {
258
+ this._goalManager.addEventToGoal(goal.id, { type, data })
259
+ }
260
+ }
261
+
262
+ // Also check pending goals - activate if conditions match
263
+ const pendingGoals = this._goalManager.getPendingGoals()
264
+ for (const goal of pendingGoals) {
265
+ if (this._isRelevantToGoal(goal, type, data)) {
266
+ this._goalManager.activateGoal(goal.id)
267
+ this._goalManager.addEventToGoal(goal.id, { type, data })
268
+ }
269
+ }
270
+ }
271
+
272
+ _isRelevantToGoal(goal, eventType, data) {
273
+ // Check explicit event types in conditions
274
+ if (goal.conditions && goal.conditions.events) {
275
+ const allowedEvents = Array.isArray(goal.conditions.events)
276
+ ? goal.conditions.events
277
+ : [goal.conditions.events]
278
+ if (!allowedEvents.includes(eventType)) {
279
+ return false
280
+ }
281
+ }
282
+ // Check tool name filters
283
+ if (goal.conditions && goal.conditions.toolNames) {
284
+ const toolNames = Array.isArray(goal.conditions.toolNames)
285
+ ? goal.conditions.toolNames
286
+ : [goal.conditions.toolNames]
287
+ if (data && data.toolName && !toolNames.includes(data.toolName)) {
288
+ return false
289
+ }
290
+ }
291
+ return true
292
+ }
293
+ }
294
+
295
+ // ============================================================================
296
+ // Reflector - Evaluates action outcomes, updates goal states
297
+ // ============================================================================
298
+ class Reflector {
299
+ constructor(goalManager) {
300
+ this._goalManager = goalManager
301
+ }
302
+
303
+ evaluateOutcome(goal, actionResult) {
304
+ if (!goal) return null
305
+
306
+ // Check for errors
307
+ if (actionResult && actionResult.error) {
308
+ // Increment attempts but don't immediately fail
309
+ goal.attempts++
310
+ return {
311
+ shouldContinue: goal.attempts < goal.maxAttempts,
312
+ actionTaken: false
313
+ }
314
+ }
315
+
316
+ // Check for successful completion
317
+ if (actionResult && actionResult.success === true) {
318
+ goal.attempts++
319
+
320
+ // Check if goal is complete
321
+ if (goal.actions.length === 0 || goal.attempts >= goal.maxAttempts) {
322
+ return { shouldContinue: false, goalComplete: true }
323
+ }
324
+
325
+ return { shouldContinue: true, actionTaken: true }
326
+ }
327
+
328
+ // Tool executed without error - consider it successful
329
+ goal.attempts++
330
+
331
+ // Check if goal is complete (no more actions)
332
+ if (goal.actions.length === 0 || goal.attempts >= goal.maxAttempts) {
333
+ return { shouldContinue: false, goalComplete: true }
334
+ }
335
+
336
+ return { shouldContinue: true, actionTaken: true }
337
+ }
338
+
339
+ checkLoopDetection(goal, actionId) {
340
+ // If same action repeats 3x consecutively, fail goal
341
+ if (goal.lastActionId === actionId) {
342
+ goal.consecutiveSameActions++
343
+ if (goal.consecutiveSameActions >= 3) {
344
+ this._goalManager.failGoal(goal.id, 'Loop detected: same action repeated 3 times')
345
+ return true
346
+ }
347
+ } else {
348
+ goal.consecutiveSameActions = 1
349
+ goal.lastActionId = actionId
350
+ }
351
+ return false
352
+ }
353
+ }
354
+
355
+ // ============================================================================
356
+ // ExplorerLoop - Main autonomous loop that decides and executes actions
357
+ // ============================================================================
358
+ class ExplorerLoop {
359
+ constructor(goalManager, reflector, framework, config) {
360
+ this._goalManager = goalManager
361
+ this._reflector = reflector
362
+ this._framework = framework
363
+ this._config = config
364
+ this._running = false
365
+ this._timer = null
366
+ this._lastActionTime = 0
367
+ this._tickCount = 0
368
+ this._recentActivities = []
369
+ }
370
+
371
+ start() {
372
+ if (this._running) return
373
+ this._running = true
374
+ this._scheduleNext()
375
+ }
376
+
377
+ stop() {
378
+ this._running = false
379
+ if (this._timer) {
380
+ clearTimeout(this._timer)
381
+ this._timer = null
382
+ }
383
+ }
384
+
385
+ pause() {
386
+ this._running = false
387
+ if (this._timer) {
388
+ clearTimeout(this._timer)
389
+ this._timer = null
390
+ }
391
+ }
392
+
393
+ resume() {
394
+ if (!this._running) {
395
+ this._running = true
396
+ this._scheduleNext()
397
+ }
398
+ }
399
+
400
+ isRunning() {
401
+ return this._running
402
+ }
403
+
404
+ getStatus() {
405
+ return {
406
+ running: this._running,
407
+ tickCount: this._tickCount,
408
+ lastTick: this._lastActionTime ? new Date(this._lastActionTime) : null,
409
+ recentActivities: this._recentActivities.slice(-10),
410
+ activeGoals: this._goalManager.getActiveGoals().length,
411
+ pendingGoals: this._goalManager.getPendingGoals().length
412
+ }
413
+ }
414
+
415
+ _scheduleNext() {
416
+ if (!this._running) return
417
+ this._timer = setTimeout(() => this._tick(), this._config.tickInterval)
418
+ }
419
+
420
+ async _tick() {
421
+ if (!this._running) return
422
+
423
+ // Skip tick if in nested execution context
424
+ try {
425
+ if (this._framework.getExecutionContext) {
426
+ const ctx = this._framework.getExecutionContext()
427
+ if (ctx && ctx.id) {
428
+ // Skip this tick, reschedule
429
+ this._scheduleNext()
430
+ return
431
+ }
432
+ }
433
+ } catch (e) {
434
+ // getExecutionContext might not exist, continue
435
+ }
436
+
437
+ this._tickCount++
438
+
439
+ try {
440
+ await this._processGoals()
441
+ } catch (err) {
442
+ console.error('[Ambient] Tick error:', err.message)
443
+ this._addActivity('error', { message: err.message })
444
+ }
445
+
446
+ // Schedule next tick
447
+ this._scheduleNext()
448
+ }
449
+
450
+ async _processGoals() {
451
+ const activeGoals = this._goalManager.getActiveGoals()
452
+
453
+ for (const goal of activeGoals) {
454
+ // Check cooldown
455
+ if (Date.now() - this._lastActionTime < this._config.cooldownPeriod) {
456
+ continue
457
+ }
458
+
459
+ // Check if goal has unprocessed events
460
+ const hasNewEvents = goal.eventsReceived && goal.eventsReceived.length > 0
461
+
462
+ // Check if goal is event-driven
463
+ const isEventDriven = goal.conditions && goal.conditions.events && goal.conditions.events.length > 0
464
+
465
+ // Get next action
466
+ if (goal.actions && goal.actions.length > 0) {
467
+ const action = goal.actions[0] // Simple: take first action
468
+
469
+ // For event-driven goals, only execute if there are new events
470
+ if (isEventDriven && !hasNewEvents) {
471
+ // Skip execution but don't fail - just wait for events
472
+ continue
473
+ }
474
+
475
+ // Loop detection (only for non-event-driven actions)
476
+ if (!hasNewEvents && this._reflector.checkLoopDetection(goal, action.id)) {
477
+ this._addActivity('goal_failed', { goalId: goal.id, reason: 'Loop detected' })
478
+ continue
479
+ }
480
+
481
+ // Execute action
482
+ const result = await this._executeAction(action, hasNewEvents ? goal.eventsReceived[0] : null)
483
+
484
+ // Evaluate outcome
485
+ const outcome = this._reflector.evaluateOutcome(goal, result)
486
+
487
+ if (outcome.goalComplete) {
488
+ // For event-driven goals, don't complete - reset and wait for more events
489
+ if (isEventDriven) {
490
+ goal.eventsReceived = []
491
+ goal.attempts = 0
492
+ this._goalManager.updateGoal(goal.id, { eventsReceived: [], attempts: 0 })
493
+ this._addActivity('goal_reset', { goalId: goal.id, reason: 'Event-driven, waiting for more events' })
494
+ } else {
495
+ this._goalManager.completeGoal(goal.id)
496
+ this._addActivity('goal_completed', { goalId: goal.id, attempts: goal.attempts })
497
+ }
498
+ } else if (!outcome.shouldContinue) {
499
+ this._goalManager.failGoal(goal.id, 'Max attempts reached')
500
+ this._addActivity('goal_failed', { goalId: goal.id, reason: 'Max attempts' })
501
+ } else if (outcome.actionTaken) {
502
+ // For event-driven goals, keep actions in queue for continuous processing
503
+ // Only shift if this is a one-time goal (no pending events)
504
+ if (!isEventDriven) {
505
+ goal.actions.shift()
506
+ }
507
+ this._addActivity('action_executed', { goalId: goal.id, action: action.id, hasEvents: hasNewEvents })
508
+ // Update last action time for cooldown
509
+ this._lastActionTime = Date.now()
510
+ }
511
+
512
+ // Clear processed events after action is taken
513
+ if (hasNewEvents) {
514
+ goal.eventsReceived = []
515
+ this._goalManager.updateGoal(goal.id, { eventsReceived: [] })
516
+ }
517
+ }
518
+ }
519
+
520
+ // Activate pending goals that have matching events
521
+ const pendingGoals = this._goalManager.getPendingGoals()
522
+ for (const goal of pendingGoals) {
523
+ // Auto-activate goals without conditions, or with satisfied conditions
524
+ if (!goal.conditions || Object.keys(goal.conditions).length === 0) {
525
+ this._goalManager.activateGoal(goal.id)
526
+ this._addActivity('goal_activated', { goalId: goal.id })
527
+ }
528
+ }
529
+ }
530
+
531
+ async _executeAction(action, eventData = null) {
532
+ if (!this._framework) {
533
+ return { success: false, error: 'Framework not available' }
534
+ }
535
+
536
+ try {
537
+ if (action.type === 'tool') {
538
+ // Use getTools() which returns array, find by name
539
+ const tools = this._framework.getTools()
540
+ const tool = tools.find(t => t.name === action.name)
541
+ if (!tool) {
542
+ return { success: false, error: `Tool not found: ${action.name}` }
543
+ }
544
+ // Merge event data into args if available
545
+ const args = { ...action.args }
546
+ if (eventData) {
547
+ args._event = eventData
548
+ }
549
+ const result = await tool.execute(args, this._framework)
550
+ return result
551
+ } else if (action.type === 'message') {
552
+ const agent = this._getActiveAgent()
553
+ if (!agent) {
554
+ return { success: false, error: 'No active agent' }
555
+ }
556
+ // Include event context in message if available
557
+ let content = action.content
558
+ if (eventData) {
559
+ content = `${action.content}\n\n[Event Context: ${JSON.stringify(eventData)}]`
560
+ }
561
+ const result = await agent.pushMessage(content)
562
+ return { success: true, result }
563
+ } else if (action.type === 'think') {
564
+ // Trigger thinking
565
+ const thinkPlugin = this._framework.pluginManager.get('think')
566
+ if (thinkPlugin) {
567
+ // Include event context in topic if available
568
+ let topic = action.topic || 'Ambient agent reflection'
569
+ if (eventData) {
570
+ topic = `${topic}\n\n[Event Context: ${JSON.stringify(eventData)}]`
571
+ }
572
+ const result = await thinkPlugin._triggerThinking({
573
+ topic,
574
+ mode: action.mode || 'reflect',
575
+ depth: action.depth || 2
576
+ })
577
+ return result
578
+ }
579
+ return { success: false, error: 'Think plugin not available' }
580
+ }
581
+
582
+ return { success: false, error: `Unknown action type: ${action.type}` }
583
+ } catch (err) {
584
+ return { success: false, error: err.message }
585
+ }
586
+ }
587
+
588
+ _getActiveAgent() {
589
+ if (this._framework._mainAgent) {
590
+ return this._framework._mainAgent
591
+ }
592
+ const agents = this._framework._agents || []
593
+ return agents.length > 0 ? agents[agents.length - 1] : null
594
+ }
595
+
596
+ _addActivity(type, data) {
597
+ this._recentActivities.push({
598
+ type,
599
+ data,
600
+ timestamp: new Date()
601
+ })
602
+ // Keep only last 50 activities
603
+ if (this._recentActivities.length > 50) {
604
+ this._recentActivities = this._recentActivities.slice(-50)
605
+ }
606
+ }
607
+ }
608
+
609
+ // ============================================================================
610
+ // Main Plugin Class
611
+ // ============================================================================
612
+ class AmbientAgentPlugin extends Plugin {
613
+ constructor(config = {}) {
614
+ super()
615
+ this.name = 'ambient'
616
+ this.version = '1.0.0'
617
+ this.description = `Ambient Agent - 持续运行的后台 Agent,用于主动监控和自动操作
618
+ 支持监听的事件:
619
+ - email:received - 收到新邮件(需配合 email_watch 启动监控)
620
+ - webhook:received - 收到 webhook 请求
621
+ - scheduler:reminder - 定时提醒触发
622
+ - scheduler:task_completed - 定时任务完成
623
+ - scheduler:task_failed - 定时任务失败
624
+ - think:thought_completed - 思考完成
625
+ - tool:result - 工具执行结果
626
+ - agent:message - Agent 消息`
627
+ this.priority = 18
628
+ this.system = true
629
+
630
+ this.config = {
631
+ enabled: config.enabled !== false,
632
+ tickInterval: config.tickInterval || 5000,
633
+ cooldownPeriod: config.cooldownPeriod || 3000,
634
+ persistencePath: config.persistencePath || '.agent/data/ambient',
635
+ defaultGoals: config.defaultGoals || []
636
+ }
637
+
638
+ this._framework = null
639
+ this._stateStore = null
640
+ this._goalManager = null
641
+ this._eventWatcher = null
642
+ this._reflector = null
643
+ this._explorerLoop = null
644
+ this._memories = []
645
+ }
646
+
647
+ install(framework) {
648
+ this._framework = framework
649
+ this._stateStore = new StateStore(this.config.persistencePath)
650
+ this._goalManager = new GoalManager(this._stateStore)
651
+ this._reflector = new Reflector(this._goalManager)
652
+
653
+ // Load persisted memories
654
+ this._memories = this._stateStore.loadMemories()
655
+
656
+ return this
657
+ }
658
+
659
+ start(framework) {
660
+ if (!this.config.enabled) {
661
+ return this
662
+ }
663
+
664
+ // Create default goals if none exist
665
+ if (this._goalManager.getAllGoals().length === 0 && this.config.defaultGoals.length > 0) {
666
+ for (const goalDef of this.config.defaultGoals) {
667
+ this._goalManager.createGoal(goalDef)
668
+ }
669
+ }
670
+
671
+ // Start event watcher
672
+ this._eventWatcher = new EventWatcher(this._goalManager, this._framework)
673
+ this._eventWatcher.start()
674
+
675
+ // Start explorer loop
676
+ this._explorerLoop = new ExplorerLoop(this._goalManager, this._reflector, this._framework, {
677
+ tickInterval: this.config.tickInterval,
678
+ cooldownPeriod: this.config.cooldownPeriod
679
+ })
680
+ this._explorerLoop.start()
681
+
682
+ // Register tools
683
+ this._registerTools(framework)
684
+
685
+ return this
686
+ }
687
+
688
+ _registerTools(framework) {
689
+ // ambient_goals - List/create/update/delete goals
690
+ framework.registerTool({
691
+ name: 'ambient_goals',
692
+ description: 'Manage ambient agent goals - list, create, update, delete goals',
693
+ inputSchema: z.object({
694
+ action: z.enum(['list', 'create', 'update', 'delete', 'activate']).describe('Operation to perform'),
695
+ goalId: z.string().optional().describe('Goal ID (required for update/delete/activate)'),
696
+ title: z.string().optional().describe('Goal title (for create/update)'),
697
+ description: z.string().optional().describe('Goal description (for create/update)'),
698
+ priority: z.number().optional().describe('Priority 1-10, higher = more important (for create/update)'),
699
+ actions: z.array(z.object({
700
+ id: z.string().optional().describe('Action identifier'),
701
+ type: z.enum(['tool', 'message', 'think']).describe('Action type'),
702
+ name: z.string().optional().describe('Tool name (for tool type)'),
703
+ args: z.record(z.any()).optional().describe('Tool arguments (for tool type)'),
704
+ content: z.string().optional().describe('Message content (for message type)'),
705
+ topic: z.string().optional().describe('Think topic (for think type)'),
706
+ mode: z.enum(['reflect', 'brainstorm', 'analyze', 'plan']).optional().describe('Think mode (for think type)')
707
+ })).optional().describe('Actions to execute (for create/update)'),
708
+ conditions: z.object({
709
+ events: z.union([z.string(), z.array(z.string())]).optional().describe('Event types to listen for'),
710
+ toolNames: z.union([z.string(), z.array(z.string())]).optional().describe('Filter by tool names')
711
+ }).optional().describe('Activation conditions (for create/update)')
712
+ }),
713
+ execute: async (args) => {
714
+ return this._handleGoalsTool(args)
715
+ }
716
+ })
717
+
718
+ // ambient_status - Get current loop state
719
+ framework.registerTool({
720
+ name: 'ambient_status',
721
+ description: 'Get current ambient agent status - loop state, active goals, recent activity',
722
+ inputSchema: z.object({}),
723
+ execute: async () => {
724
+ return this._handleStatusTool()
725
+ }
726
+ })
727
+
728
+ // ambient_think - Trigger proactive LLM thinking
729
+ framework.registerTool({
730
+ name: 'ambient_think',
731
+ description: 'Trigger proactive LLM thinking for a goal - modes: reflect, brainstorm, plan, analyze',
732
+ inputSchema: z.object({
733
+ goalId: z.string().optional().describe('Goal ID to think about'),
734
+ mode: z.enum(['reflect', 'brainstorm', 'plan', 'analyze']).describe('Thinking mode'),
735
+ topic: z.string().optional().describe('Topic to think about'),
736
+ depth: z.number().optional().describe('Thinking depth 1-5')
737
+ }),
738
+ execute: async (args) => {
739
+ return this._handleThinkTool(args)
740
+ }
741
+ })
742
+
743
+ // ambient_remember - Store/retrieve/search persistent memories
744
+ framework.registerTool({
745
+ name: 'ambient_remember',
746
+ description: 'Store or retrieve persistent memories for the ambient agent',
747
+ inputSchema: z.object({
748
+ action: z.enum(['store', 'retrieve', 'search']).describe('Action: store a memory, retrieve recent, or search'),
749
+ content: z.string().optional().describe('Memory content (for store)'),
750
+ key: z.string().optional().describe('Memory key (for store/retrieve)'),
751
+ query: z.string().optional().describe('Search query (for search)'),
752
+ limit: z.number().optional().describe('Max results (for retrieve/search)')
753
+ }),
754
+ execute: async (args) => {
755
+ return this._handleRememberTool(args)
756
+ }
757
+ })
758
+
759
+ // ambient_control - Pause/resume/adjust the explorer loop
760
+ framework.registerTool({
761
+ name: 'ambient_control',
762
+ description: 'Control the ambient agent explorer loop - pause, resume, or adjust settings',
763
+ inputSchema: z.object({
764
+ action: z.enum(['pause', 'resume', 'status', 'adjust']).describe('Control action'),
765
+ tickInterval: z.number().optional().describe('New tick interval in ms (for adjust)'),
766
+ cooldownPeriod: z.number().optional().describe('New cooldown period in ms (for adjust)')
767
+ }),
768
+ execute: async (args) => {
769
+ return this._handleControlTool(args)
770
+ }
771
+ })
772
+ }
773
+
774
+ _handleGoalsTool(args) {
775
+ const { action, goalId, title, description, priority, actions, conditions } = args
776
+
777
+ switch (action) {
778
+ case 'list': {
779
+ const allGoals = this._goalManager.getAllGoals()
780
+ return {
781
+ success: true,
782
+ goals: allGoals.map(g => ({
783
+ id: g.id,
784
+ title: g.title,
785
+ state: g.state,
786
+ priority: g.priority,
787
+ actionsCount: g.actions ? g.actions.length : 0,
788
+ attempts: g.attempts,
789
+ createdAt: g.createdAt,
790
+ activatedAt: g.activatedAt,
791
+ completedAt: g.completedAt,
792
+ failedAt: g.failedAt
793
+ })),
794
+ total: allGoals.length,
795
+ active: this._goalManager.getActiveGoals().length,
796
+ pending: this._goalManager.getPendingGoals().length
797
+ }
798
+ }
799
+
800
+ case 'create': {
801
+ if (!title) {
802
+ return { success: false, error: 'Title is required for creating a goal' }
803
+ }
804
+ const goal = this._goalManager.createGoal({ title, description, priority, actions, conditions })
805
+ return { success: true, goal }
806
+ }
807
+
808
+ case 'update': {
809
+ if (!goalId) {
810
+ return { success: false, error: 'Goal ID is required for update' }
811
+ }
812
+ const updates = {}
813
+ if (title !== undefined) updates.title = title
814
+ if (description !== undefined) updates.description = description
815
+ if (priority !== undefined) updates.priority = priority
816
+ if (actions !== undefined) updates.actions = actions
817
+ if (conditions !== undefined) updates.conditions = conditions
818
+ const goal = this._goalManager.updateGoal(goalId, updates)
819
+ return goal ? { success: true, goal } : { success: false, error: 'Goal not found' }
820
+ }
821
+
822
+ case 'delete': {
823
+ if (!goalId) {
824
+ return { success: false, error: 'Goal ID is required for delete' }
825
+ }
826
+ const deleted = this._goalManager.deleteGoal(goalId)
827
+ return { success: deleted }
828
+ }
829
+
830
+ case 'activate': {
831
+ if (!goalId) {
832
+ return { success: false, error: 'Goal ID is required for activate' }
833
+ }
834
+ const goal = this._goalManager.activateGoal(goalId)
835
+ return goal ? { success: true, goal } : { success: false, error: 'Goal not found or not pending' }
836
+ }
837
+
838
+ default:
839
+ return { success: false, error: `Unknown action: ${action}` }
840
+ }
841
+ }
842
+
843
+ _handleStatusTool() {
844
+ if (!this._explorerLoop) {
845
+ return { success: false, error: 'Explorer loop not initialized' }
846
+ }
847
+
848
+ const loopStatus = this._explorerLoop.getStatus()
849
+ const activeGoals = this._goalManager.getActiveGoals().map(g => ({
850
+ id: g.id,
851
+ title: g.title,
852
+ priority: g.priority,
853
+ attempts: g.attempts,
854
+ lastActionId: g.lastActionId
855
+ }))
856
+ const pendingGoals = this._goalManager.getPendingGoals().map(g => ({
857
+ id: g.id,
858
+ title: g.title,
859
+ priority: g.priority
860
+ }))
861
+
862
+ return {
863
+ success: true,
864
+ loop: {
865
+ running: loopStatus.running,
866
+ tickCount: loopStatus.tickCount,
867
+ lastTick: loopStatus.lastTick,
868
+ tickInterval: this.config.tickInterval,
869
+ cooldownPeriod: this.config.cooldownPeriod
870
+ },
871
+ activeGoals,
872
+ pendingGoals,
873
+ recentActivities: loopStatus.recentActivities
874
+ }
875
+ }
876
+
877
+ async _handleThinkTool(args) {
878
+ const { goalId, mode, topic, depth } = args
879
+
880
+ // Get active agent
881
+ const agent = this._getActiveAgent()
882
+ if (!agent) {
883
+ return { success: false, error: 'No active agent available' }
884
+ }
885
+
886
+ // Build thinking prompt
887
+ let prompt = ''
888
+ if (goalId) {
889
+ const goal = this._goalManager.getGoal(goalId)
890
+ if (goal) {
891
+ prompt = this._buildGoalThinkPrompt(goal, mode, topic)
892
+ } else {
893
+ return { success: false, error: 'Goal not found' }
894
+ }
895
+ } else {
896
+ prompt = this._buildFreeThinkPrompt(mode, topic)
897
+ }
898
+
899
+ // Wait for agent to be available if busy
900
+ let attempts = 0
901
+ const maxWaitAttempts = 10
902
+ const waitInterval = 500
903
+
904
+ while (agent._status === 'busy' && attempts < maxWaitAttempts) {
905
+ await new Promise(resolve => setTimeout(resolve, waitInterval))
906
+ attempts++
907
+ }
908
+
909
+ if (agent._status === 'busy') {
910
+ return {
911
+ success: false,
912
+ error: 'Agent is busy and could not become available in time. Please try again later.',
913
+ queued: true
914
+ }
915
+ }
916
+
917
+ // Execute thinking via agent
918
+ const thinkPlugin = this._framework.pluginManager.get('think')
919
+ if (thinkPlugin) {
920
+ const result = await thinkPlugin._triggerThinking({ topic: prompt, mode, depth: depth || 2 })
921
+ // If the result indicates busy, return a more helpful message
922
+ if (result && result.message && result.message.includes && result.message.includes('busy')) {
923
+ return {
924
+ success: true,
925
+ queued: true,
926
+ message: 'Thinking request was queued and will be processed when the agent is free.'
927
+ }
928
+ }
929
+ return result
930
+ }
931
+
932
+ // Fallback: push message directly
933
+ return agent.pushMessage(prompt).then(result => ({
934
+ success: true,
935
+ result: typeof result === 'string' ? result : JSON.stringify(result)
936
+ })).catch(err => ({
937
+ success: false,
938
+ error: err.message
939
+ }))
940
+ }
941
+
942
+ _buildGoalThinkPrompt(goal, mode, topic) {
943
+ const modePrompts = {
944
+ reflect: `As the Ambient Agent, reflect on the current goal:
945
+ Title: ${goal.title}
946
+ Description: ${goal.description || 'None'}
947
+ Priority: ${goal.priority}
948
+ State: ${goal.state}
949
+ Attempts: ${goal.attempts}
950
+
951
+ ${topic ? `Additional focus: ${topic}` : ''}
952
+
953
+ Reflect on:
954
+ 1. Is this goal still relevant and achievable?
955
+ 2. Are the actions appropriate for the goal?
956
+ 3. What could be improved?`,
957
+ brainstorm: `Brainstorm new approaches for the ambient agent goal:
958
+ Title: ${goal.title}
959
+ ${goal.description ? `Description: ${goal.description}` : ''}
960
+ Actions planned: ${goal.actions ? goal.actions.length : 0}
961
+
962
+ ${topic ? `Focus: ${topic}` : 'Generate new ideas for how this goal could be pursued more effectively.'}`,
963
+ plan: `Create a refined plan for the ambient agent goal:
964
+ Title: ${goal.title}
965
+ ${goal.description ? `Description: ${goal.description}` : ''}
966
+ ${topic ? `Additional context: ${topic}` : ''}
967
+
968
+ Develop a clear, actionable plan.`,
969
+ analyze: `Analyze the ambient agent goal:
970
+ Title: ${goal.title}
971
+ State: ${goal.state}
972
+ Priority: ${goal.priority}
973
+ Actions: ${goal.actions ? JSON.stringify(goal.actions) : 'None'}
974
+
975
+ ${topic ? `Analysis focus: ${topic}` : 'What are the strengths, weaknesses, and potential issues with this goal?'}`
976
+ }
977
+ return modePrompts[mode] || modePrompts.reflect
978
+ }
979
+
980
+ _buildFreeThinkPrompt(mode, topic) {
981
+ const modePrompts = {
982
+ reflect: `Ambient Agent reflection: ${topic || 'Consider the current state of the system and goals. What could be improved?'}`,
983
+ brainstorm: `Ambient Agent brainstorming: ${topic || 'Generate creative ideas for new goals or approaches.'}`,
984
+ plan: `Ambient Agent planning: ${topic || 'Develop plans for pending objectives.'}`,
985
+ analyze: `Ambient Agent analysis: ${topic || 'Analyze current goals and their progress.'}`
986
+ }
987
+ return modePrompts[mode] || modePrompts.reflect
988
+ }
989
+
990
+ _handleRememberTool(args) {
991
+ const { action, content, key, query, limit } = args
992
+ const maxResults = limit || 10
993
+
994
+ switch (action) {
995
+ case 'store': {
996
+ if (!content) {
997
+ return { success: false, error: 'Content is required for store' }
998
+ }
999
+ const memoryKey = key || `memory_${Date.now()}`
1000
+ const memory = {
1001
+ key: memoryKey,
1002
+ content,
1003
+ timestamp: new Date()
1004
+ }
1005
+ this._memories.push(memory)
1006
+ this._stateStore.saveMemories(this._memories)
1007
+ return { success: true, memory }
1008
+ }
1009
+
1010
+ case 'retrieve': {
1011
+ const memories = this._memories.slice(-maxResults).reverse()
1012
+ return {
1013
+ success: true,
1014
+ memories,
1015
+ total: this._memories.length
1016
+ }
1017
+ }
1018
+
1019
+ case 'search': {
1020
+ if (!query) {
1021
+ return { success: false, error: 'Query is required for search' }
1022
+ }
1023
+ const queryLower = query.toLowerCase()
1024
+ const results = this._memories
1025
+ .filter(m => m.content.toLowerCase().includes(queryLower))
1026
+ .slice(-maxResults)
1027
+ .reverse()
1028
+ return {
1029
+ success: true,
1030
+ results,
1031
+ total: results.length
1032
+ }
1033
+ }
1034
+
1035
+ default:
1036
+ return { success: false, error: `Unknown action: ${action}` }
1037
+ }
1038
+ }
1039
+
1040
+ _handleControlTool(args) {
1041
+ const { action, tickInterval, cooldownPeriod } = args
1042
+
1043
+ switch (action) {
1044
+ case 'pause': {
1045
+ if (!this._explorerLoop) {
1046
+ return { success: false, error: 'Explorer loop not initialized' }
1047
+ }
1048
+ this._explorerLoop.pause()
1049
+ return { success: true, message: 'Explorer loop paused' }
1050
+ }
1051
+
1052
+ case 'resume': {
1053
+ if (!this._explorerLoop) {
1054
+ return { success: false, error: 'Explorer loop not initialized' }
1055
+ }
1056
+ this._explorerLoop.resume()
1057
+ return { success: true, message: 'Explorer loop resumed' }
1058
+ }
1059
+
1060
+ case 'status': {
1061
+ if (!this._explorerLoop) {
1062
+ return { success: false, error: 'Explorer loop not initialized' }
1063
+ }
1064
+ const status = this._explorerLoop.getStatus()
1065
+ return {
1066
+ success: true,
1067
+ running: status.running,
1068
+ tickCount: status.tickCount,
1069
+ lastTick: status.lastTick,
1070
+ tickInterval: this.config.tickInterval,
1071
+ cooldownPeriod: this.config.cooldownPeriod
1072
+ }
1073
+ }
1074
+
1075
+ case 'adjust': {
1076
+ if (tickInterval) {
1077
+ this.config.tickInterval = tickInterval
1078
+ }
1079
+ if (cooldownPeriod) {
1080
+ this.config.cooldownPeriod = cooldownPeriod
1081
+ }
1082
+ return {
1083
+ success: true,
1084
+ message: 'Settings adjusted',
1085
+ tickInterval: this.config.tickInterval,
1086
+ cooldownPeriod: this.config.cooldownPeriod
1087
+ }
1088
+ }
1089
+
1090
+ default:
1091
+ return { success: false, error: `Unknown action: ${action}` }
1092
+ }
1093
+ }
1094
+
1095
+ _getActiveAgent() {
1096
+ if (this._framework._mainAgent) {
1097
+ return this._framework._mainAgent
1098
+ }
1099
+ const agents = this._framework._agents || []
1100
+ return agents.length > 0 ? agents[agents.length - 1] : null
1101
+ }
1102
+
1103
+ reload(framework) {
1104
+ this._framework = framework
1105
+ if (this._explorerLoop && this._explorerLoop.isRunning()) {
1106
+ // Restart with new framework reference
1107
+ this._eventWatcher = new EventWatcher(this._goalManager, this._framework)
1108
+ this._eventWatcher.start()
1109
+ }
1110
+ }
1111
+
1112
+ uninstall(framework) {
1113
+ // Stop explorer loop
1114
+ if (this._explorerLoop) {
1115
+ this._explorerLoop.stop()
1116
+ this._explorerLoop = null
1117
+ }
1118
+
1119
+ // Stop event watcher
1120
+ if (this._eventWatcher) {
1121
+ this._eventWatcher.stop()
1122
+ this._eventWatcher = null
1123
+ }
1124
+
1125
+ // Clear references
1126
+ this._framework = null
1127
+ this._goalManager = null
1128
+ this._reflector = null
1129
+ this._stateStore = null
1130
+ this._memories = []
1131
+ }
1132
+ }
1133
+
1134
+ module.exports = { AmbientAgentPlugin, GoalState }