foliko 1.0.52 → 1.0.54

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,1097 @@
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.event || event.type,
206
+ timestamp: new Date()
207
+ })
208
+ this._persist()
209
+ }
210
+ }
211
+
212
+ // ============================================================================
213
+ // EventWatcher - Subscribes to framework events, filters relevant ones
214
+ // ============================================================================
215
+ class EventWatcher {
216
+ constructor(goalManager, framework) {
217
+ this._goalManager = goalManager
218
+ this._framework = framework
219
+ this._handlers = []
220
+ this._relevantEvents = new Set([
221
+ 'tool:result',
222
+ 'scheduler:reminder',
223
+ 'agent:message',
224
+ 'think:thought_completed',
225
+ 'scheduler:task_completed',
226
+ 'scheduler:task_failed',
227
+ 'email:received',
228
+ 'webhook:received'
229
+ ])
230
+ }
231
+
232
+ start() {
233
+ this._handlers.push(
234
+ this._framework.on('tool:result', (data) => this._handleEvent('tool:result', data)),
235
+ this._framework.on('scheduler:reminder', (data) => this._handleEvent('scheduler:reminder', data)),
236
+ this._framework.on('agent:message', (data) => this._handleEvent('agent:message', data)),
237
+ this._framework.on('think:thought_completed', (data) => this._handleEvent('think:thought_completed', data)),
238
+ this._framework.on('scheduler:task_completed', (data) => this._handleEvent('scheduler:task_completed', data)),
239
+ this._framework.on('scheduler:task_failed', (data) => this._handleEvent('scheduler:task_failed', data)),
240
+ this._framework.on('email:received', (data) => this._handleEvent('email:received', data)),
241
+ this._framework.on('webhook:received', (data) => this._handleEvent('webhook:received', data))
242
+ )
243
+ }
244
+
245
+ stop() {
246
+ for (const handler of this._handlers) {
247
+ handler()
248
+ }
249
+ this._handlers = []
250
+ }
251
+
252
+ _handleEvent(type, data) {
253
+ const activeGoals = this._goalManager.getActiveGoals()
254
+ for (const goal of activeGoals) {
255
+ // Check if goal's conditions match this event
256
+ if (this._isRelevantToGoal(goal, type, data)) {
257
+ this._goalManager.addEventToGoal(goal.id, { type, data })
258
+ }
259
+ }
260
+ }
261
+
262
+ _isRelevantToGoal(goal, eventType, data) {
263
+ // Check explicit event types in conditions
264
+ if (goal.conditions && goal.conditions.events) {
265
+ const allowedEvents = Array.isArray(goal.conditions.events)
266
+ ? goal.conditions.events
267
+ : [goal.conditions.events]
268
+ if (!allowedEvents.includes(eventType)) {
269
+ return false
270
+ }
271
+ }
272
+ // Check tool name filters
273
+ if (goal.conditions && goal.conditions.toolNames) {
274
+ const toolNames = Array.isArray(goal.conditions.toolNames)
275
+ ? goal.conditions.toolNames
276
+ : [goal.conditions.toolNames]
277
+ if (data && data.toolName && !toolNames.includes(data.toolName)) {
278
+ return false
279
+ }
280
+ }
281
+ return true
282
+ }
283
+ }
284
+
285
+ // ============================================================================
286
+ // Reflector - Evaluates action outcomes, updates goal states
287
+ // ============================================================================
288
+ class Reflector {
289
+ constructor(goalManager) {
290
+ this._goalManager = goalManager
291
+ }
292
+
293
+ evaluateOutcome(goal, actionResult) {
294
+ if (!goal) return null
295
+
296
+ // Check for errors
297
+ if (actionResult && actionResult.error) {
298
+ // Increment attempts but don't immediately fail
299
+ goal.attempts++
300
+ return {
301
+ shouldContinue: goal.attempts < goal.maxAttempts,
302
+ actionTaken: false
303
+ }
304
+ }
305
+
306
+ // Check for successful completion
307
+ if (actionResult && actionResult.success === true) {
308
+ goal.attempts++
309
+
310
+ // Check if goal is complete
311
+ if (goal.actions.length === 0 || goal.attempts >= goal.maxAttempts) {
312
+ return { shouldContinue: false, goalComplete: true }
313
+ }
314
+
315
+ return { shouldContinue: true, actionTaken: true }
316
+ }
317
+
318
+ // Tool executed without error - consider it successful
319
+ goal.attempts++
320
+
321
+ // Check if goal is complete (no more actions)
322
+ if (goal.actions.length === 0 || goal.attempts >= goal.maxAttempts) {
323
+ return { shouldContinue: false, goalComplete: true }
324
+ }
325
+
326
+ return { shouldContinue: true, actionTaken: true }
327
+ }
328
+
329
+ checkLoopDetection(goal, actionId) {
330
+ // If same action repeats 3x consecutively, fail goal
331
+ if (goal.lastActionId === actionId) {
332
+ goal.consecutiveSameActions++
333
+ if (goal.consecutiveSameActions >= 3) {
334
+ this._goalManager.failGoal(goal.id, 'Loop detected: same action repeated 3 times')
335
+ return true
336
+ }
337
+ } else {
338
+ goal.consecutiveSameActions = 1
339
+ goal.lastActionId = actionId
340
+ }
341
+ return false
342
+ }
343
+ }
344
+
345
+ // ============================================================================
346
+ // ExplorerLoop - Main autonomous loop that decides and executes actions
347
+ // ============================================================================
348
+ class ExplorerLoop {
349
+ constructor(goalManager, reflector, framework, config) {
350
+ this._goalManager = goalManager
351
+ this._reflector = reflector
352
+ this._framework = framework
353
+ this._config = config
354
+ this._running = false
355
+ this._timer = null
356
+ this._lastActionTime = 0
357
+ this._tickCount = 0
358
+ this._recentActivities = []
359
+ }
360
+
361
+ start() {
362
+ if (this._running) return
363
+ this._running = true
364
+ this._scheduleNext()
365
+ }
366
+
367
+ stop() {
368
+ this._running = false
369
+ if (this._timer) {
370
+ clearTimeout(this._timer)
371
+ this._timer = null
372
+ }
373
+ }
374
+
375
+ pause() {
376
+ this._running = false
377
+ if (this._timer) {
378
+ clearTimeout(this._timer)
379
+ this._timer = null
380
+ }
381
+ }
382
+
383
+ resume() {
384
+ if (!this._running) {
385
+ this._running = true
386
+ this._scheduleNext()
387
+ }
388
+ }
389
+
390
+ isRunning() {
391
+ return this._running
392
+ }
393
+
394
+ getStatus() {
395
+ return {
396
+ running: this._running,
397
+ tickCount: this._tickCount,
398
+ lastTick: this._lastActionTime ? new Date(this._lastActionTime) : null,
399
+ recentActivities: this._recentActivities.slice(-10),
400
+ activeGoals: this._goalManager.getActiveGoals().length,
401
+ pendingGoals: this._goalManager.getPendingGoals().length
402
+ }
403
+ }
404
+
405
+ _scheduleNext() {
406
+ if (!this._running) return
407
+ this._timer = setTimeout(() => this._tick(), this._config.tickInterval)
408
+ }
409
+
410
+ async _tick() {
411
+ if (!this._running) return
412
+
413
+ // Skip tick if in nested execution context
414
+ try {
415
+ if (this._framework.getExecutionContext) {
416
+ const ctx = this._framework.getExecutionContext()
417
+ if (ctx && ctx.id) {
418
+ // Skip this tick, reschedule
419
+ this._scheduleNext()
420
+ return
421
+ }
422
+ }
423
+ } catch (e) {
424
+ // getExecutionContext might not exist, continue
425
+ }
426
+
427
+ this._tickCount++
428
+ this._lastActionTime = Date.now()
429
+
430
+ try {
431
+ await this._processGoals()
432
+ } catch (err) {
433
+ console.error('[Ambient] Tick error:', err.message)
434
+ this._addActivity('error', { message: err.message })
435
+ }
436
+
437
+ // Schedule next tick
438
+ this._scheduleNext()
439
+ }
440
+
441
+ async _processGoals() {
442
+ const activeGoals = this._goalManager.getActiveGoals()
443
+
444
+ for (const goal of activeGoals) {
445
+ // Check cooldown
446
+ if (Date.now() - this._lastActionTime < this._config.cooldownPeriod) {
447
+ continue
448
+ }
449
+
450
+ // Check if goal has unprocessed events
451
+ const hasNewEvents = goal.eventsReceived && goal.eventsReceived.length > 0
452
+
453
+ // Get next action
454
+ if (goal.actions && goal.actions.length > 0) {
455
+ const action = goal.actions[0] // Simple: take first action
456
+
457
+ // Loop detection (only for non-event-driven actions)
458
+ if (!hasNewEvents && this._reflector.checkLoopDetection(goal, action.id)) {
459
+ this._addActivity('goal_failed', { goalId: goal.id, reason: 'Loop detected' })
460
+ continue
461
+ }
462
+
463
+ // Execute action
464
+ const result = await this._executeAction(action, hasNewEvents ? goal.eventsReceived[0] : null)
465
+
466
+ // Evaluate outcome
467
+ const outcome = this._reflector.evaluateOutcome(goal, result)
468
+
469
+ if (outcome.goalComplete) {
470
+ this._goalManager.completeGoal(goal.id)
471
+ this._addActivity('goal_completed', { goalId: goal.id, attempts: goal.attempts })
472
+ } else if (!outcome.shouldContinue) {
473
+ this._goalManager.failGoal(goal.id, 'Max attempts reached')
474
+ this._addActivity('goal_failed', { goalId: goal.id, reason: 'Max attempts' })
475
+ } else if (outcome.actionTaken) {
476
+ // Remove completed action from queue
477
+ goal.actions.shift()
478
+ this._addActivity('action_executed', { goalId: goal.id, action: action.id, hasEvents: hasNewEvents })
479
+ }
480
+
481
+ // Clear processed events after action is taken
482
+ if (hasNewEvents && outcome.actionTaken) {
483
+ goal.eventsReceived = []
484
+ this._goalManager.updateGoal(goal.id, { eventsReceived: [] })
485
+ }
486
+ }
487
+ }
488
+
489
+ // Activate pending goals that have matching events
490
+ const pendingGoals = this._goalManager.getPendingGoals()
491
+ for (const goal of pendingGoals) {
492
+ // Auto-activate goals without conditions, or with satisfied conditions
493
+ if (!goal.conditions || Object.keys(goal.conditions).length === 0) {
494
+ this._goalManager.activateGoal(goal.id)
495
+ this._addActivity('goal_activated', { goalId: goal.id })
496
+ }
497
+ }
498
+ }
499
+
500
+ async _executeAction(action, eventData = null) {
501
+ if (!this._framework) {
502
+ return { success: false, error: 'Framework not available' }
503
+ }
504
+
505
+ try {
506
+ if (action.type === 'tool') {
507
+ // Use getTools() which returns array, find by name
508
+ const tools = this._framework.getTools()
509
+ const tool = tools.find(t => t.name === action.name)
510
+ if (!tool) {
511
+ return { success: false, error: `Tool not found: ${action.name}` }
512
+ }
513
+ // Merge event data into args if available
514
+ const args = { ...action.args }
515
+ if (eventData) {
516
+ args._event = eventData
517
+ }
518
+ const result = await tool.execute(args, this._framework)
519
+ return result
520
+ } else if (action.type === 'message') {
521
+ const agent = this._getActiveAgent()
522
+ if (!agent) {
523
+ return { success: false, error: 'No active agent' }
524
+ }
525
+ // Include event context in message if available
526
+ let content = action.content
527
+ if (eventData) {
528
+ content = `${action.content}\n\n[Event Context: ${JSON.stringify(eventData)}]`
529
+ }
530
+ const result = await agent.pushMessage(content)
531
+ return { success: true, result }
532
+ } else if (action.type === 'think') {
533
+ // Trigger thinking
534
+ const thinkPlugin = this._framework.pluginManager.get('think')
535
+ if (thinkPlugin) {
536
+ // Include event context in topic if available
537
+ let topic = action.topic || 'Ambient agent reflection'
538
+ if (eventData) {
539
+ topic = `${topic}\n\n[Event Context: ${JSON.stringify(eventData)}]`
540
+ }
541
+ const result = await thinkPlugin._triggerThinking({
542
+ topic,
543
+ mode: action.mode || 'reflect',
544
+ depth: action.depth || 2
545
+ })
546
+ return result
547
+ }
548
+ return { success: false, error: 'Think plugin not available' }
549
+ }
550
+
551
+ return { success: false, error: `Unknown action type: ${action.type}` }
552
+ } catch (err) {
553
+ return { success: false, error: err.message }
554
+ }
555
+ }
556
+
557
+ _getActiveAgent() {
558
+ if (this._framework._mainAgent) {
559
+ return this._framework._mainAgent
560
+ }
561
+ const agents = this._framework._agents || []
562
+ return agents.length > 0 ? agents[agents.length - 1] : null
563
+ }
564
+
565
+ _addActivity(type, data) {
566
+ this._recentActivities.push({
567
+ type,
568
+ data,
569
+ timestamp: new Date()
570
+ })
571
+ // Keep only last 50 activities
572
+ if (this._recentActivities.length > 50) {
573
+ this._recentActivities = this._recentActivities.slice(-50)
574
+ }
575
+ }
576
+ }
577
+
578
+ // ============================================================================
579
+ // Main Plugin Class
580
+ // ============================================================================
581
+ class AmbientAgentPlugin extends Plugin {
582
+ constructor(config = {}) {
583
+ super()
584
+ this.name = 'ambient'
585
+ this.version = '1.0.0'
586
+ this.description = 'Ambient Agent - continuous background agent for autonomous monitoring and actions'
587
+ this.priority = 18
588
+ this.system = true
589
+
590
+ this.config = {
591
+ enabled: config.enabled !== false,
592
+ tickInterval: config.tickInterval || 5000,
593
+ cooldownPeriod: config.cooldownPeriod || 3000,
594
+ persistencePath: config.persistencePath || '.agent/data/ambient',
595
+ defaultGoals: config.defaultGoals || []
596
+ }
597
+
598
+ this._framework = null
599
+ this._stateStore = null
600
+ this._goalManager = null
601
+ this._eventWatcher = null
602
+ this._reflector = null
603
+ this._explorerLoop = null
604
+ this._memories = []
605
+ }
606
+
607
+ install(framework) {
608
+ this._framework = framework
609
+ this._stateStore = new StateStore(this.config.persistencePath)
610
+ this._goalManager = new GoalManager(this._stateStore)
611
+ this._reflector = new Reflector(this._goalManager)
612
+
613
+ // Load persisted memories
614
+ this._memories = this._stateStore.loadMemories()
615
+
616
+ return this
617
+ }
618
+
619
+ start(framework) {
620
+ if (!this.config.enabled) {
621
+ console.log('[Ambient] Plugin disabled, skipping start')
622
+ return this
623
+ }
624
+
625
+ // Create default goals if none exist
626
+ if (this._goalManager.getAllGoals().length === 0 && this.config.defaultGoals.length > 0) {
627
+ for (const goalDef of this.config.defaultGoals) {
628
+ this._goalManager.createGoal(goalDef)
629
+ }
630
+ }
631
+
632
+ // Start event watcher
633
+ this._eventWatcher = new EventWatcher(this._goalManager, this._framework)
634
+ this._eventWatcher.start()
635
+
636
+ // Start explorer loop
637
+ this._explorerLoop = new ExplorerLoop(this._goalManager, this._reflector, this._framework, {
638
+ tickInterval: this.config.tickInterval,
639
+ cooldownPeriod: this.config.cooldownPeriod
640
+ })
641
+ this._explorerLoop.start()
642
+
643
+ // Register tools
644
+ this._registerTools(framework)
645
+
646
+ console.log('[Ambient] Plugin started')
647
+ return this
648
+ }
649
+
650
+ _registerTools(framework) {
651
+ // ambient_goals - List/create/update/delete goals
652
+ framework.registerTool({
653
+ name: 'ambient_goals',
654
+ description: 'Manage ambient agent goals - list, create, update, delete goals',
655
+ inputSchema: z.object({
656
+ action: z.enum(['list', 'create', 'update', 'delete', 'activate']).describe('Operation to perform'),
657
+ goalId: z.string().optional().describe('Goal ID (required for update/delete/activate)'),
658
+ title: z.string().optional().describe('Goal title (for create/update)'),
659
+ description: z.string().optional().describe('Goal description (for create/update)'),
660
+ priority: z.number().optional().describe('Priority 1-10, higher = more important (for create/update)'),
661
+ actions: z.array(z.object({
662
+ id: z.string().optional().describe('Action identifier'),
663
+ type: z.enum(['tool', 'message', 'think']).describe('Action type'),
664
+ name: z.string().optional().describe('Tool name (for tool type)'),
665
+ args: z.record(z.any()).optional().describe('Tool arguments (for tool type)'),
666
+ content: z.string().optional().describe('Message content (for message type)'),
667
+ topic: z.string().optional().describe('Think topic (for think type)'),
668
+ mode: z.enum(['reflect', 'brainstorm', 'analyze', 'plan']).optional().describe('Think mode (for think type)')
669
+ })).optional().describe('Actions to execute (for create/update)'),
670
+ conditions: z.object({
671
+ events: z.union([z.string(), z.array(z.string())]).optional().describe('Event types to listen for'),
672
+ toolNames: z.union([z.string(), z.array(z.string())]).optional().describe('Filter by tool names')
673
+ }).optional().describe('Activation conditions (for create/update)')
674
+ }),
675
+ execute: async (args) => {
676
+ return this._handleGoalsTool(args)
677
+ }
678
+ })
679
+
680
+ // ambient_status - Get current loop state
681
+ framework.registerTool({
682
+ name: 'ambient_status',
683
+ description: 'Get current ambient agent status - loop state, active goals, recent activity',
684
+ inputSchema: z.object({}),
685
+ execute: async () => {
686
+ return this._handleStatusTool()
687
+ }
688
+ })
689
+
690
+ // ambient_think - Trigger proactive LLM thinking
691
+ framework.registerTool({
692
+ name: 'ambient_think',
693
+ description: 'Trigger proactive LLM thinking for a goal - modes: reflect, brainstorm, plan, analyze',
694
+ inputSchema: z.object({
695
+ goalId: z.string().optional().describe('Goal ID to think about'),
696
+ mode: z.enum(['reflect', 'brainstorm', 'plan', 'analyze']).describe('Thinking mode'),
697
+ topic: z.string().optional().describe('Topic to think about'),
698
+ depth: z.number().optional().describe('Thinking depth 1-5')
699
+ }),
700
+ execute: async (args) => {
701
+ return this._handleThinkTool(args)
702
+ }
703
+ })
704
+
705
+ // ambient_remember - Store/retrieve/search persistent memories
706
+ framework.registerTool({
707
+ name: 'ambient_remember',
708
+ description: 'Store or retrieve persistent memories for the ambient agent',
709
+ inputSchema: z.object({
710
+ action: z.enum(['store', 'retrieve', 'search']).describe('Action: store a memory, retrieve recent, or search'),
711
+ content: z.string().optional().describe('Memory content (for store)'),
712
+ key: z.string().optional().describe('Memory key (for store/retrieve)'),
713
+ query: z.string().optional().describe('Search query (for search)'),
714
+ limit: z.number().optional().describe('Max results (for retrieve/search)')
715
+ }),
716
+ execute: async (args) => {
717
+ return this._handleRememberTool(args)
718
+ }
719
+ })
720
+
721
+ // ambient_control - Pause/resume/adjust the explorer loop
722
+ framework.registerTool({
723
+ name: 'ambient_control',
724
+ description: 'Control the ambient agent explorer loop - pause, resume, or adjust settings',
725
+ inputSchema: z.object({
726
+ action: z.enum(['pause', 'resume', 'status', 'adjust']).describe('Control action'),
727
+ tickInterval: z.number().optional().describe('New tick interval in ms (for adjust)'),
728
+ cooldownPeriod: z.number().optional().describe('New cooldown period in ms (for adjust)')
729
+ }),
730
+ execute: async (args) => {
731
+ return this._handleControlTool(args)
732
+ }
733
+ })
734
+ }
735
+
736
+ _handleGoalsTool(args) {
737
+ const { action, goalId, title, description, priority, actions, conditions } = args
738
+
739
+ switch (action) {
740
+ case 'list': {
741
+ const allGoals = this._goalManager.getAllGoals()
742
+ return {
743
+ success: true,
744
+ goals: allGoals.map(g => ({
745
+ id: g.id,
746
+ title: g.title,
747
+ state: g.state,
748
+ priority: g.priority,
749
+ actionsCount: g.actions ? g.actions.length : 0,
750
+ attempts: g.attempts,
751
+ createdAt: g.createdAt,
752
+ activatedAt: g.activatedAt,
753
+ completedAt: g.completedAt,
754
+ failedAt: g.failedAt
755
+ })),
756
+ total: allGoals.length,
757
+ active: this._goalManager.getActiveGoals().length,
758
+ pending: this._goalManager.getPendingGoals().length
759
+ }
760
+ }
761
+
762
+ case 'create': {
763
+ if (!title) {
764
+ return { success: false, error: 'Title is required for creating a goal' }
765
+ }
766
+ const goal = this._goalManager.createGoal({ title, description, priority, actions, conditions })
767
+ return { success: true, goal }
768
+ }
769
+
770
+ case 'update': {
771
+ if (!goalId) {
772
+ return { success: false, error: 'Goal ID is required for update' }
773
+ }
774
+ const updates = {}
775
+ if (title !== undefined) updates.title = title
776
+ if (description !== undefined) updates.description = description
777
+ if (priority !== undefined) updates.priority = priority
778
+ if (actions !== undefined) updates.actions = actions
779
+ if (conditions !== undefined) updates.conditions = conditions
780
+ const goal = this._goalManager.updateGoal(goalId, updates)
781
+ return goal ? { success: true, goal } : { success: false, error: 'Goal not found' }
782
+ }
783
+
784
+ case 'delete': {
785
+ if (!goalId) {
786
+ return { success: false, error: 'Goal ID is required for delete' }
787
+ }
788
+ const deleted = this._goalManager.deleteGoal(goalId)
789
+ return { success: deleted }
790
+ }
791
+
792
+ case 'activate': {
793
+ if (!goalId) {
794
+ return { success: false, error: 'Goal ID is required for activate' }
795
+ }
796
+ const goal = this._goalManager.activateGoal(goalId)
797
+ return goal ? { success: true, goal } : { success: false, error: 'Goal not found or not pending' }
798
+ }
799
+
800
+ default:
801
+ return { success: false, error: `Unknown action: ${action}` }
802
+ }
803
+ }
804
+
805
+ _handleStatusTool() {
806
+ if (!this._explorerLoop) {
807
+ return { success: false, error: 'Explorer loop not initialized' }
808
+ }
809
+
810
+ const loopStatus = this._explorerLoop.getStatus()
811
+ const activeGoals = this._goalManager.getActiveGoals().map(g => ({
812
+ id: g.id,
813
+ title: g.title,
814
+ priority: g.priority,
815
+ attempts: g.attempts,
816
+ lastActionId: g.lastActionId
817
+ }))
818
+ const pendingGoals = this._goalManager.getPendingGoals().map(g => ({
819
+ id: g.id,
820
+ title: g.title,
821
+ priority: g.priority
822
+ }))
823
+
824
+ return {
825
+ success: true,
826
+ loop: {
827
+ running: loopStatus.running,
828
+ tickCount: loopStatus.tickCount,
829
+ lastTick: loopStatus.lastTick,
830
+ tickInterval: this.config.tickInterval,
831
+ cooldownPeriod: this.config.cooldownPeriod
832
+ },
833
+ activeGoals,
834
+ pendingGoals,
835
+ recentActivities: loopStatus.recentActivities
836
+ }
837
+ }
838
+
839
+ async _handleThinkTool(args) {
840
+ const { goalId, mode, topic, depth } = args
841
+
842
+ // Get active agent
843
+ const agent = this._getActiveAgent()
844
+ if (!agent) {
845
+ return { success: false, error: 'No active agent available' }
846
+ }
847
+
848
+ // Build thinking prompt
849
+ let prompt = ''
850
+ if (goalId) {
851
+ const goal = this._goalManager.getGoal(goalId)
852
+ if (goal) {
853
+ prompt = this._buildGoalThinkPrompt(goal, mode, topic)
854
+ } else {
855
+ return { success: false, error: 'Goal not found' }
856
+ }
857
+ } else {
858
+ prompt = this._buildFreeThinkPrompt(mode, topic)
859
+ }
860
+
861
+ // Wait for agent to be available if busy
862
+ let attempts = 0
863
+ const maxWaitAttempts = 10
864
+ const waitInterval = 500
865
+
866
+ while (agent._status === 'busy' && attempts < maxWaitAttempts) {
867
+ console.log('[Ambient] Agent busy, waiting...')
868
+ await new Promise(resolve => setTimeout(resolve, waitInterval))
869
+ attempts++
870
+ }
871
+
872
+ if (agent._status === 'busy') {
873
+ return {
874
+ success: false,
875
+ error: 'Agent is busy and could not become available in time. Please try again later.',
876
+ queued: true
877
+ }
878
+ }
879
+
880
+ // Execute thinking via agent
881
+ const thinkPlugin = this._framework.pluginManager.get('think')
882
+ if (thinkPlugin) {
883
+ const result = await thinkPlugin._triggerThinking({ topic: prompt, mode, depth: depth || 2 })
884
+ // If the result indicates busy, return a more helpful message
885
+ if (result && result.message && result.message.includes && result.message.includes('busy')) {
886
+ return {
887
+ success: true,
888
+ queued: true,
889
+ message: 'Thinking request was queued and will be processed when the agent is free.'
890
+ }
891
+ }
892
+ return result
893
+ }
894
+
895
+ // Fallback: push message directly
896
+ return agent.pushMessage(prompt).then(result => ({
897
+ success: true,
898
+ result: typeof result === 'string' ? result : JSON.stringify(result)
899
+ })).catch(err => ({
900
+ success: false,
901
+ error: err.message
902
+ }))
903
+ }
904
+
905
+ _buildGoalThinkPrompt(goal, mode, topic) {
906
+ const modePrompts = {
907
+ reflect: `As the Ambient Agent, reflect on the current goal:
908
+ Title: ${goal.title}
909
+ Description: ${goal.description || 'None'}
910
+ Priority: ${goal.priority}
911
+ State: ${goal.state}
912
+ Attempts: ${goal.attempts}
913
+
914
+ ${topic ? `Additional focus: ${topic}` : ''}
915
+
916
+ Reflect on:
917
+ 1. Is this goal still relevant and achievable?
918
+ 2. Are the actions appropriate for the goal?
919
+ 3. What could be improved?`,
920
+ brainstorm: `Brainstorm new approaches for the ambient agent goal:
921
+ Title: ${goal.title}
922
+ ${goal.description ? `Description: ${goal.description}` : ''}
923
+ Actions planned: ${goal.actions ? goal.actions.length : 0}
924
+
925
+ ${topic ? `Focus: ${topic}` : 'Generate new ideas for how this goal could be pursued more effectively.'}`,
926
+ plan: `Create a refined plan for the ambient agent goal:
927
+ Title: ${goal.title}
928
+ ${goal.description ? `Description: ${goal.description}` : ''}
929
+ ${topic ? `Additional context: ${topic}` : ''}
930
+
931
+ Develop a clear, actionable plan.`,
932
+ analyze: `Analyze the ambient agent goal:
933
+ Title: ${goal.title}
934
+ State: ${goal.state}
935
+ Priority: ${goal.priority}
936
+ Actions: ${goal.actions ? JSON.stringify(goal.actions) : 'None'}
937
+
938
+ ${topic ? `Analysis focus: ${topic}` : 'What are the strengths, weaknesses, and potential issues with this goal?'}`
939
+ }
940
+ return modePrompts[mode] || modePrompts.reflect
941
+ }
942
+
943
+ _buildFreeThinkPrompt(mode, topic) {
944
+ const modePrompts = {
945
+ reflect: `Ambient Agent reflection: ${topic || 'Consider the current state of the system and goals. What could be improved?'}`,
946
+ brainstorm: `Ambient Agent brainstorming: ${topic || 'Generate creative ideas for new goals or approaches.'}`,
947
+ plan: `Ambient Agent planning: ${topic || 'Develop plans for pending objectives.'}`,
948
+ analyze: `Ambient Agent analysis: ${topic || 'Analyze current goals and their progress.'}`
949
+ }
950
+ return modePrompts[mode] || modePrompts.reflect
951
+ }
952
+
953
+ _handleRememberTool(args) {
954
+ const { action, content, key, query, limit } = args
955
+ const maxResults = limit || 10
956
+
957
+ switch (action) {
958
+ case 'store': {
959
+ if (!content) {
960
+ return { success: false, error: 'Content is required for store' }
961
+ }
962
+ const memoryKey = key || `memory_${Date.now()}`
963
+ const memory = {
964
+ key: memoryKey,
965
+ content,
966
+ timestamp: new Date()
967
+ }
968
+ this._memories.push(memory)
969
+ this._stateStore.saveMemories(this._memories)
970
+ return { success: true, memory }
971
+ }
972
+
973
+ case 'retrieve': {
974
+ const memories = this._memories.slice(-maxResults).reverse()
975
+ return {
976
+ success: true,
977
+ memories,
978
+ total: this._memories.length
979
+ }
980
+ }
981
+
982
+ case 'search': {
983
+ if (!query) {
984
+ return { success: false, error: 'Query is required for search' }
985
+ }
986
+ const queryLower = query.toLowerCase()
987
+ const results = this._memories
988
+ .filter(m => m.content.toLowerCase().includes(queryLower))
989
+ .slice(-maxResults)
990
+ .reverse()
991
+ return {
992
+ success: true,
993
+ results,
994
+ total: results.length
995
+ }
996
+ }
997
+
998
+ default:
999
+ return { success: false, error: `Unknown action: ${action}` }
1000
+ }
1001
+ }
1002
+
1003
+ _handleControlTool(args) {
1004
+ const { action, tickInterval, cooldownPeriod } = args
1005
+
1006
+ switch (action) {
1007
+ case 'pause': {
1008
+ if (!this._explorerLoop) {
1009
+ return { success: false, error: 'Explorer loop not initialized' }
1010
+ }
1011
+ this._explorerLoop.pause()
1012
+ return { success: true, message: 'Explorer loop paused' }
1013
+ }
1014
+
1015
+ case 'resume': {
1016
+ if (!this._explorerLoop) {
1017
+ return { success: false, error: 'Explorer loop not initialized' }
1018
+ }
1019
+ this._explorerLoop.resume()
1020
+ return { success: true, message: 'Explorer loop resumed' }
1021
+ }
1022
+
1023
+ case 'status': {
1024
+ if (!this._explorerLoop) {
1025
+ return { success: false, error: 'Explorer loop not initialized' }
1026
+ }
1027
+ const status = this._explorerLoop.getStatus()
1028
+ return {
1029
+ success: true,
1030
+ running: status.running,
1031
+ tickCount: status.tickCount,
1032
+ lastTick: status.lastTick,
1033
+ tickInterval: this.config.tickInterval,
1034
+ cooldownPeriod: this.config.cooldownPeriod
1035
+ }
1036
+ }
1037
+
1038
+ case 'adjust': {
1039
+ if (tickInterval) {
1040
+ this.config.tickInterval = tickInterval
1041
+ }
1042
+ if (cooldownPeriod) {
1043
+ this.config.cooldownPeriod = cooldownPeriod
1044
+ }
1045
+ return {
1046
+ success: true,
1047
+ message: 'Settings adjusted',
1048
+ tickInterval: this.config.tickInterval,
1049
+ cooldownPeriod: this.config.cooldownPeriod
1050
+ }
1051
+ }
1052
+
1053
+ default:
1054
+ return { success: false, error: `Unknown action: ${action}` }
1055
+ }
1056
+ }
1057
+
1058
+ _getActiveAgent() {
1059
+ if (this._framework._mainAgent) {
1060
+ return this._framework._mainAgent
1061
+ }
1062
+ const agents = this._framework._agents || []
1063
+ return agents.length > 0 ? agents[agents.length - 1] : null
1064
+ }
1065
+
1066
+ reload(framework) {
1067
+ this._framework = framework
1068
+ if (this._explorerLoop && this._explorerLoop.isRunning()) {
1069
+ // Restart with new framework reference
1070
+ this._eventWatcher = new EventWatcher(this._goalManager, this._framework)
1071
+ this._eventWatcher.start()
1072
+ }
1073
+ }
1074
+
1075
+ uninstall(framework) {
1076
+ // Stop explorer loop
1077
+ if (this._explorerLoop) {
1078
+ this._explorerLoop.stop()
1079
+ this._explorerLoop = null
1080
+ }
1081
+
1082
+ // Stop event watcher
1083
+ if (this._eventWatcher) {
1084
+ this._eventWatcher.stop()
1085
+ this._eventWatcher = null
1086
+ }
1087
+
1088
+ // Clear references
1089
+ this._framework = null
1090
+ this._goalManager = null
1091
+ this._reflector = null
1092
+ this._stateStore = null
1093
+ this._memories = []
1094
+ }
1095
+ }
1096
+
1097
+ module.exports = { AmbientAgentPlugin, GoalState }