crawd 0.8.7 → 0.9.1
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/dist/types.d.ts +5 -23
- package/openclaw.plugin.json +8 -40
- package/package.json +13 -11
- package/skills/crawd/SKILL.md +39 -0
- package/src/backend/coordinator.test.ts +393 -0
- package/src/backend/coordinator.ts +372 -17
- package/src/backend/index.ts +29 -208
- package/src/backend/server.ts +71 -219
- package/src/plugin.ts +122 -33
- package/src/types.ts +4 -23
- package/src/lib/tts/tiktok.ts +0 -91
|
@@ -4,29 +4,54 @@ import type { ChatMessage } from '../lib/chat/types'
|
|
|
4
4
|
|
|
5
5
|
const SESSION_KEY = process.env.CRAWD_CHANNEL_ID || 'agent:main:crawd:live'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Plan types
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export type PlanStep = {
|
|
12
|
+
description: string
|
|
13
|
+
status: 'pending' | 'done'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Plan = {
|
|
17
|
+
id: string
|
|
18
|
+
goal: string
|
|
19
|
+
steps: PlanStep[]
|
|
20
|
+
createdAt: number
|
|
21
|
+
status: 'active' | 'completed' | 'abandoned'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AutonomyMode = 'vibe' | 'plan' | 'none'
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Coordinator configuration
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
8
30
|
export type CoordinatorConfig = {
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
/** How often to send vibe prompt when active (ms). Default:
|
|
31
|
+
/** Autonomy mode: 'vibe' (periodic prompts), 'plan' (goal-driven loop), 'none' (disabled). */
|
|
32
|
+
autonomyMode: AutonomyMode
|
|
33
|
+
/** How often to send vibe prompt when active (ms). Default: 30000 (30 sec). Only used in vibe mode. */
|
|
12
34
|
vibeIntervalMs: number
|
|
13
|
-
/** Go idle after this much inactivity while active (ms). Default:
|
|
35
|
+
/** Go idle after this much inactivity while active (ms). Default: 180000 (3 min) */
|
|
14
36
|
idleAfterMs: number
|
|
15
|
-
/** Go sleep after this much inactivity while idle (ms). Default:
|
|
37
|
+
/** Go sleep after this much inactivity while idle (ms). Default: 180000 (3 min) */
|
|
16
38
|
sleepAfterIdleMs: number
|
|
17
39
|
/** Chat batch throttle window (ms). Default: 20000 (20 sec) */
|
|
18
40
|
batchWindowMs: number
|
|
19
|
-
/** The autonomous "vibe" prompt sent periodically */
|
|
41
|
+
/** The autonomous "vibe" prompt sent periodically. Only used in vibe mode. */
|
|
20
42
|
vibePrompt: string
|
|
43
|
+
/** Delay between plan nudges (ms). Default: 2000 (2 sec). Only used in plan mode. */
|
|
44
|
+
planNudgeDelayMs: number
|
|
21
45
|
}
|
|
22
46
|
|
|
23
47
|
export const DEFAULT_CONFIG: CoordinatorConfig = {
|
|
24
|
-
|
|
48
|
+
autonomyMode: 'vibe',
|
|
25
49
|
vibeIntervalMs: 30_000,
|
|
26
50
|
idleAfterMs: 180_000,
|
|
27
51
|
sleepAfterIdleMs: 180_000,
|
|
28
52
|
batchWindowMs: 20_000,
|
|
29
53
|
vibePrompt: `[CRAWD:VIBE] You are on a livestream. Make sure the crawd skill is loaded. Do one thing on the internet or ask the chat something. Respond with LIVESTREAM_REPLIED after using a tool, or NO_REPLY if you have nothing to say.`,
|
|
54
|
+
planNudgeDelayMs: 2_000,
|
|
30
55
|
}
|
|
31
56
|
|
|
32
57
|
export type CoordinatorState = 'sleep' | 'idle' | 'active'
|
|
@@ -481,6 +506,102 @@ export class OneShotGateway {
|
|
|
481
506
|
})
|
|
482
507
|
})
|
|
483
508
|
}
|
|
509
|
+
|
|
510
|
+
/** Compact a session via the gateway's sessions.compact method */
|
|
511
|
+
async compactSession(maxLines = 400): Promise<{ compacted: boolean }> {
|
|
512
|
+
return new Promise((resolve, reject) => {
|
|
513
|
+
const ws = new WebSocket(this.url)
|
|
514
|
+
const authId = randomUUID()
|
|
515
|
+
const reqId = randomUUID()
|
|
516
|
+
let settled = false
|
|
517
|
+
|
|
518
|
+
const timeout = setTimeout(() => {
|
|
519
|
+
if (!settled) {
|
|
520
|
+
settled = true
|
|
521
|
+
ws.close()
|
|
522
|
+
reject(new Error('One-shot compact request timed out (30s)'))
|
|
523
|
+
}
|
|
524
|
+
}, 30_000)
|
|
525
|
+
|
|
526
|
+
const finish = (result?: { compacted: boolean }, error?: Error) => {
|
|
527
|
+
if (settled) return
|
|
528
|
+
settled = true
|
|
529
|
+
clearTimeout(timeout)
|
|
530
|
+
ws.close()
|
|
531
|
+
if (error) {
|
|
532
|
+
console.error(`[OneShotGateway] compact error: ${error.message}`)
|
|
533
|
+
reject(error)
|
|
534
|
+
} else {
|
|
535
|
+
resolve(result ?? { compacted: false })
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const sendConnect = () => {
|
|
540
|
+
ws.send(JSON.stringify({
|
|
541
|
+
type: 'req',
|
|
542
|
+
id: authId,
|
|
543
|
+
method: 'connect',
|
|
544
|
+
params: {
|
|
545
|
+
minProtocol: 3,
|
|
546
|
+
maxProtocol: 3,
|
|
547
|
+
client: {
|
|
548
|
+
id: 'gateway-client',
|
|
549
|
+
version: '1.0.0',
|
|
550
|
+
platform: 'node',
|
|
551
|
+
mode: 'backend',
|
|
552
|
+
},
|
|
553
|
+
auth: this.token ? { token: this.token } : {},
|
|
554
|
+
},
|
|
555
|
+
}))
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
ws.on('message', (data) => {
|
|
559
|
+
try {
|
|
560
|
+
const frame = JSON.parse(data.toString()) as GatewayFrame
|
|
561
|
+
|
|
562
|
+
if (frame.type === 'event' && (frame as any).event === 'connect.challenge') {
|
|
563
|
+
sendConnect()
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (frame.type === 'res' && frame.id === authId) {
|
|
568
|
+
if (frame.error) {
|
|
569
|
+
finish(undefined, new Error(`Gateway auth failed: ${frame.error.message}`))
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
ws.send(JSON.stringify({
|
|
573
|
+
type: 'req',
|
|
574
|
+
id: reqId,
|
|
575
|
+
method: 'sessions.compact',
|
|
576
|
+
params: {
|
|
577
|
+
key: this.sessionKey,
|
|
578
|
+
maxLines,
|
|
579
|
+
},
|
|
580
|
+
}))
|
|
581
|
+
} else if (frame.type === 'res' && frame.id === reqId) {
|
|
582
|
+
if (frame.error) {
|
|
583
|
+
finish(undefined, new Error(`sessions.compact failed: ${frame.error.message}`))
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
const payload = frame.payload as any
|
|
587
|
+
finish({ compacted: payload?.compacted === true })
|
|
588
|
+
}
|
|
589
|
+
} catch {
|
|
590
|
+
// Parse error — ignore
|
|
591
|
+
}
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
ws.on('error', (err) => {
|
|
595
|
+
finish(undefined, err instanceof Error ? err : new Error(String(err)))
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
ws.on('close', () => {
|
|
599
|
+
if (!settled) {
|
|
600
|
+
finish(undefined, new Error('WebSocket closed unexpectedly'))
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
})
|
|
604
|
+
}
|
|
484
605
|
}
|
|
485
606
|
|
|
486
607
|
/**
|
|
@@ -504,11 +625,19 @@ export type CoordinatorEvent =
|
|
|
504
625
|
| { type: 'vibeExecuted'; skipped: boolean; reason?: string }
|
|
505
626
|
| { type: 'sleepCheck'; inactiveForMs: number; willSleep: boolean }
|
|
506
627
|
| { type: 'chatProcessed'; count: number }
|
|
628
|
+
// Plan events
|
|
629
|
+
| { type: 'planCreated'; planId: string; goal: string; stepCount: number }
|
|
630
|
+
| { type: 'planStepDone'; planId: string; step: number }
|
|
631
|
+
| { type: 'planCompleted'; planId: string }
|
|
632
|
+
| { type: 'planAbandoned'; planId: string }
|
|
633
|
+
| { type: 'planNudgeScheduled'; nextNudgeAt: number }
|
|
634
|
+
| { type: 'planNudgeExecuted'; skipped: boolean; reason?: string }
|
|
507
635
|
|
|
508
636
|
export class Coordinator {
|
|
509
637
|
private buffer: ChatMessage[] = []
|
|
510
638
|
private timer: NodeJS.Timeout | null = null
|
|
511
639
|
private triggerFn: TriggerAgentFn
|
|
640
|
+
private compactFn?: () => Promise<void>
|
|
512
641
|
private onEvent?: (event: CoordinatorEvent) => void
|
|
513
642
|
/** Timestamp when coordinator was created (used to filter old messages on restart) */
|
|
514
643
|
private readonly startedAt: number
|
|
@@ -520,11 +649,15 @@ export class Coordinator {
|
|
|
520
649
|
private idleSince = 0
|
|
521
650
|
private vibeTimer: NodeJS.Timeout | null = null
|
|
522
651
|
private sleepCheckTimer: NodeJS.Timeout | null = null
|
|
523
|
-
/** True while a flush or talk is being processed — vibes should wait */
|
|
652
|
+
/** True while a flush or talk is being processed — vibes/nudges should wait */
|
|
524
653
|
private _busy = false
|
|
525
654
|
/** Serializes all triggerAgent calls to prevent concurrent runs */
|
|
526
655
|
private _gatewayQueue: Promise<void> = Promise.resolve()
|
|
527
656
|
|
|
657
|
+
// === Plan State ===
|
|
658
|
+
private currentPlan: Plan | null = null
|
|
659
|
+
private planNudgeTimer: NodeJS.Timeout | null = null
|
|
660
|
+
|
|
528
661
|
// === Injected dependencies ===
|
|
529
662
|
private readonly clock: IClock
|
|
530
663
|
private readonly logger: Pick<Console, 'log' | 'error' | 'warn'>
|
|
@@ -555,6 +688,7 @@ export class Coordinator {
|
|
|
555
688
|
updateConfig(config: Partial<CoordinatorConfig>): void {
|
|
556
689
|
this.config = { ...this.config, ...config }
|
|
557
690
|
this.logger.log('[Coordinator] Config updated:', {
|
|
691
|
+
autonomyMode: this.config.autonomyMode,
|
|
558
692
|
vibeIntervalMs: this.config.vibeIntervalMs,
|
|
559
693
|
idleAfterMs: this.config.idleAfterMs,
|
|
560
694
|
sleepAfterIdleMs: this.config.sleepAfterIdleMs,
|
|
@@ -563,11 +697,13 @@ export class Coordinator {
|
|
|
563
697
|
}
|
|
564
698
|
|
|
565
699
|
/** Get current state and config */
|
|
566
|
-
getState(): { state: CoordinatorState; lastActivityAt: number; config: CoordinatorConfig } {
|
|
700
|
+
getState(): { state: CoordinatorState; lastActivityAt: number; config: CoordinatorConfig; plan: Plan | null; autonomyMode: AutonomyMode } {
|
|
567
701
|
return {
|
|
568
702
|
state: this._state,
|
|
569
703
|
lastActivityAt: this.lastActivityAt,
|
|
570
704
|
config: this.config,
|
|
705
|
+
plan: this.currentPlan,
|
|
706
|
+
autonomyMode: this.config.autonomyMode,
|
|
571
707
|
}
|
|
572
708
|
}
|
|
573
709
|
|
|
@@ -581,6 +717,11 @@ export class Coordinator {
|
|
|
581
717
|
this.onEvent = callback
|
|
582
718
|
}
|
|
583
719
|
|
|
720
|
+
/** Set the compact function (uses gateway sessions.compact instead of sending /compact as agent message) */
|
|
721
|
+
setCompactFn(fn: () => Promise<void>): void {
|
|
722
|
+
this.compactFn = fn
|
|
723
|
+
}
|
|
724
|
+
|
|
584
725
|
private emit(event: CoordinatorEvent): void {
|
|
585
726
|
this.onEvent?.(event)
|
|
586
727
|
}
|
|
@@ -597,6 +738,7 @@ export class Coordinator {
|
|
|
597
738
|
|
|
598
739
|
stop(): void {
|
|
599
740
|
this.stopVibeLoop()
|
|
741
|
+
this.cancelPlanNudge()
|
|
600
742
|
if (this.timer) {
|
|
601
743
|
this.clock.clearTimeout(this.timer)
|
|
602
744
|
this.timer = null
|
|
@@ -607,7 +749,7 @@ export class Coordinator {
|
|
|
607
749
|
|
|
608
750
|
// === State Machine Methods ===
|
|
609
751
|
|
|
610
|
-
/** Wake up from sleep/idle state and start the
|
|
752
|
+
/** Wake up from sleep/idle state and start the autonomy loop */
|
|
611
753
|
wake(): void {
|
|
612
754
|
if (this._state === 'active') return
|
|
613
755
|
|
|
@@ -617,7 +759,12 @@ export class Coordinator {
|
|
|
617
759
|
this.logger.log('[Coordinator] WAKE - transitioning to ACTIVE state')
|
|
618
760
|
this.emit({ type: 'stateChange', from, to: 'active' })
|
|
619
761
|
|
|
620
|
-
this.startVibeLoop()
|
|
762
|
+
this.startVibeLoop() // starts sleep/idle check timers + vibe loop (if mode=vibe)
|
|
763
|
+
|
|
764
|
+
// In plan mode, also start plan nudges if there are pending steps
|
|
765
|
+
if (this.config.autonomyMode === 'plan' && this.hasPendingPlanSteps()) {
|
|
766
|
+
this.schedulePlanNudge()
|
|
767
|
+
}
|
|
621
768
|
}
|
|
622
769
|
|
|
623
770
|
/** Go to idle state (between activities, eyes open) */
|
|
@@ -644,14 +791,16 @@ export class Coordinator {
|
|
|
644
791
|
this.emit({ type: 'stateChange', from, to: 'sleep' })
|
|
645
792
|
|
|
646
793
|
this.stopVibeLoop()
|
|
794
|
+
this.cancelPlanNudge()
|
|
647
795
|
this.compactSession()
|
|
648
796
|
}
|
|
649
797
|
|
|
650
798
|
/** Compact the agent's session context before sleeping to free stale history */
|
|
651
799
|
private compactSession(): void {
|
|
800
|
+
if (!this.compactFn) return
|
|
652
801
|
this._gatewayQueue = this._gatewayQueue.then(async () => {
|
|
653
802
|
try {
|
|
654
|
-
await this.
|
|
803
|
+
await this.compactFn!()
|
|
655
804
|
this.logger.log('[Coordinator] Session compacted before sleep')
|
|
656
805
|
} catch (err) {
|
|
657
806
|
this.logger.error('[Coordinator] Failed to compact session:', err)
|
|
@@ -675,6 +824,203 @@ export class Coordinator {
|
|
|
675
824
|
return this.clock.now() - this.lastActivityAt
|
|
676
825
|
}
|
|
677
826
|
|
|
827
|
+
// === Plan Methods ===
|
|
828
|
+
|
|
829
|
+
/** Create or replace the current plan */
|
|
830
|
+
setPlan(goal: string, steps: string[]): Plan {
|
|
831
|
+
if (this.currentPlan?.status === 'active') {
|
|
832
|
+
this.currentPlan.status = 'abandoned'
|
|
833
|
+
this.emit({ type: 'planAbandoned', planId: this.currentPlan.id })
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const plan: Plan = {
|
|
837
|
+
id: randomUUID(),
|
|
838
|
+
goal,
|
|
839
|
+
steps: steps.map(s => ({ description: s, status: 'pending' as const })),
|
|
840
|
+
createdAt: this.clock.now(),
|
|
841
|
+
status: 'active',
|
|
842
|
+
}
|
|
843
|
+
this.currentPlan = plan
|
|
844
|
+
this.emit({ type: 'planCreated', planId: plan.id, goal, stepCount: steps.length })
|
|
845
|
+
|
|
846
|
+
if (this._state !== 'active') this.wake()
|
|
847
|
+
|
|
848
|
+
return plan
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Mark a step as done (0-indexed). Returns updated plan or null. */
|
|
852
|
+
markStepDone(stepIndex: number): Plan | null {
|
|
853
|
+
if (!this.currentPlan || this.currentPlan.status !== 'active') return null
|
|
854
|
+
if (stepIndex < 0 || stepIndex >= this.currentPlan.steps.length) return null
|
|
855
|
+
|
|
856
|
+
this.currentPlan.steps[stepIndex].status = 'done'
|
|
857
|
+
this.emit({ type: 'planStepDone', planId: this.currentPlan.id, step: stepIndex })
|
|
858
|
+
|
|
859
|
+
const allDone = this.currentPlan.steps.every(s => s.status === 'done')
|
|
860
|
+
if (allDone) {
|
|
861
|
+
this.currentPlan.status = 'completed'
|
|
862
|
+
this.emit({ type: 'planCompleted', planId: this.currentPlan.id })
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return this.currentPlan
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/** Abandon the current plan */
|
|
869
|
+
abandonPlan(): Plan | null {
|
|
870
|
+
if (!this.currentPlan || this.currentPlan.status !== 'active') return null
|
|
871
|
+
this.currentPlan.status = 'abandoned'
|
|
872
|
+
this.emit({ type: 'planAbandoned', planId: this.currentPlan.id })
|
|
873
|
+
this.cancelPlanNudge()
|
|
874
|
+
return this.currentPlan
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** Get the current plan */
|
|
878
|
+
getPlan(): Plan | null {
|
|
879
|
+
return this.currentPlan
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Check if there is an active plan with pending steps */
|
|
883
|
+
private hasPendingPlanSteps(): boolean {
|
|
884
|
+
if (!this.currentPlan || this.currentPlan.status !== 'active') return false
|
|
885
|
+
return this.currentPlan.steps.some(s => s.status === 'pending')
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// === Plan Nudge Loop ===
|
|
889
|
+
|
|
890
|
+
/** Schedule next plan nudge (short delay to avoid spinning) */
|
|
891
|
+
private schedulePlanNudge(): void {
|
|
892
|
+
this.cancelPlanNudge()
|
|
893
|
+
if (this._state === 'sleep') return
|
|
894
|
+
if (this.config.autonomyMode !== 'plan') return
|
|
895
|
+
if (!this.hasPendingPlanSteps()) return
|
|
896
|
+
|
|
897
|
+
const delay = this.config.planNudgeDelayMs
|
|
898
|
+
const nextNudgeAt = this.clock.now() + delay
|
|
899
|
+
this.emit({ type: 'planNudgeScheduled', nextNudgeAt })
|
|
900
|
+
this.planNudgeTimer = this.clock.setTimeout(() => this.planNudge(), delay)
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Cancel pending plan nudge */
|
|
904
|
+
private cancelPlanNudge(): void {
|
|
905
|
+
if (this.planNudgeTimer) {
|
|
906
|
+
this.clock.clearTimeout(this.planNudgeTimer)
|
|
907
|
+
this.planNudgeTimer = null
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/** Check plan state and schedule next nudge if needed */
|
|
912
|
+
private checkPlanProgress(): void {
|
|
913
|
+
if (this.config.autonomyMode !== 'plan') return
|
|
914
|
+
if (this._state === 'sleep') return
|
|
915
|
+
if (this.hasPendingPlanSteps()) {
|
|
916
|
+
this.schedulePlanNudge()
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Execute a plan nudge — send [CRAWD:PLAN] prompt to agent */
|
|
921
|
+
private async planNudge(): Promise<void> {
|
|
922
|
+
if (this._state === 'sleep') {
|
|
923
|
+
this.emit({ type: 'planNudgeExecuted', skipped: true, reason: 'sleeping' })
|
|
924
|
+
return
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (this._busy) {
|
|
928
|
+
this.logger.log('[Coordinator] Plan nudge skipped - busy')
|
|
929
|
+
this.emit({ type: 'planNudgeExecuted', skipped: true, reason: 'busy' })
|
|
930
|
+
this.schedulePlanNudge()
|
|
931
|
+
return
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (!this.hasPendingPlanSteps()) {
|
|
935
|
+
this.logger.log('[Coordinator] Plan nudge skipped - no pending steps')
|
|
936
|
+
this.emit({ type: 'planNudgeExecuted', skipped: true, reason: 'no pending steps' })
|
|
937
|
+
return
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (this._state === 'idle') {
|
|
941
|
+
const from = this._state
|
|
942
|
+
this._state = 'active'
|
|
943
|
+
this.emit({ type: 'stateChange', from, to: 'active' })
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
this.logger.log('[Coordinator] Plan nudge - sending plan prompt')
|
|
947
|
+
this.emit({ type: 'planNudgeExecuted', skipped: false })
|
|
948
|
+
this.resetActivity()
|
|
949
|
+
|
|
950
|
+
const prompt = this.buildPlanNudgePrompt()
|
|
951
|
+
|
|
952
|
+
this._busy = true
|
|
953
|
+
let noReply = false
|
|
954
|
+
const nudgeOp = this._gatewayQueue.then(async () => {
|
|
955
|
+
this._busy = true
|
|
956
|
+
try {
|
|
957
|
+
const replies = await this.triggerFn(prompt)
|
|
958
|
+
const agentReplies = replies.filter(r => !this.isApiError(r))
|
|
959
|
+
|
|
960
|
+
// Treat empty replies as NO_REPLY — gateway returns empty payloads
|
|
961
|
+
// when the agent responds with text-only (no tool calls)
|
|
962
|
+
if (agentReplies.length === 0 || agentReplies.some(r => r.trim().toUpperCase() === 'NO_REPLY')) {
|
|
963
|
+
noReply = true
|
|
964
|
+
} else if (!this.isCompliantReply(agentReplies)) {
|
|
965
|
+
const misaligned = agentReplies.filter(r => {
|
|
966
|
+
const t = r.trim().toUpperCase()
|
|
967
|
+
return t !== 'NO_REPLY' && t !== 'LIVESTREAM_REPLIED'
|
|
968
|
+
})
|
|
969
|
+
if (misaligned.length > 0) {
|
|
970
|
+
await this.sendMisalignment(misaligned)
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
} catch (err) {
|
|
974
|
+
this.logger.error('[Coordinator] Plan nudge failed:', err)
|
|
975
|
+
} finally {
|
|
976
|
+
this._busy = false
|
|
977
|
+
}
|
|
978
|
+
})
|
|
979
|
+
this._gatewayQueue = nudgeOp.catch(() => {})
|
|
980
|
+
|
|
981
|
+
try {
|
|
982
|
+
await nudgeOp
|
|
983
|
+
} catch {}
|
|
984
|
+
|
|
985
|
+
if (noReply) {
|
|
986
|
+
this.logger.log('[Coordinator] Agent sent NO_REPLY during plan nudge, going to sleep')
|
|
987
|
+
this.goSleep()
|
|
988
|
+
return
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
this.checkPlanProgress()
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** Build the [CRAWD:PLAN] prompt with current plan progress */
|
|
995
|
+
private buildPlanNudgePrompt(): string {
|
|
996
|
+
if (!this.currentPlan || this.currentPlan.status !== 'active') {
|
|
997
|
+
return '[CRAWD:PLAN] No active plan. Create one using plan_set or respond with NO_REPLY to idle.'
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const lines: string[] = ['[CRAWD:PLAN] Continue your plan.', '']
|
|
1001
|
+
lines.push(`Target: ${this.currentPlan.goal}`)
|
|
1002
|
+
|
|
1003
|
+
let nextStepIdx = -1
|
|
1004
|
+
for (let i = 0; i < this.currentPlan.steps.length; i++) {
|
|
1005
|
+
const step = this.currentPlan.steps[i]
|
|
1006
|
+
const isDone = step.status === 'done'
|
|
1007
|
+
if (!isDone && nextStepIdx === -1) nextStepIdx = i
|
|
1008
|
+
const marker = isDone ? '[x]' : (nextStepIdx === i ? '[-]' : '[ ]')
|
|
1009
|
+
const arrow = nextStepIdx === i ? ' <-- next' : ''
|
|
1010
|
+
lines.push(`${marker} ${i}. ${step.description}${arrow}`)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
lines.push('')
|
|
1014
|
+
if (nextStepIdx >= 0) {
|
|
1015
|
+
lines.push(`Work on step ${nextStepIdx}. Use plan_step_done when complete.`)
|
|
1016
|
+
}
|
|
1017
|
+
lines.push('You can use plan_abandon to drop this plan, or plan_set to replace it.')
|
|
1018
|
+
lines.push('IMPORTANT: Never mention plans, steps, or progress on stream. Just do the thing naturally.')
|
|
1019
|
+
lines.push('Respond with LIVESTREAM_REPLIED after speaking, or NO_REPLY if you have nothing to say.')
|
|
1020
|
+
|
|
1021
|
+
return lines.join('\n')
|
|
1022
|
+
}
|
|
1023
|
+
|
|
678
1024
|
/** Start the periodic vibe loop */
|
|
679
1025
|
private startVibeLoop(): void {
|
|
680
1026
|
this.stopVibeLoop() // Clear any existing timer
|
|
@@ -723,9 +1069,9 @@ export class Coordinator {
|
|
|
723
1069
|
|
|
724
1070
|
/** Schedule the next vibe action */
|
|
725
1071
|
scheduleNextVibe(): void {
|
|
726
|
-
// Vibe while active or idle (not while sleeping)
|
|
727
1072
|
if (this._state === 'sleep') return
|
|
728
|
-
|
|
1073
|
+
// Only schedule vibes in vibe mode
|
|
1074
|
+
if (this.config.autonomyMode !== 'vibe') return
|
|
729
1075
|
|
|
730
1076
|
const nextVibeAt = this.clock.now() + this.config.vibeIntervalMs
|
|
731
1077
|
this.emit({ type: 'vibeScheduled', nextVibeAt })
|
|
@@ -771,7 +1117,9 @@ export class Coordinator {
|
|
|
771
1117
|
const replies = await this.triggerFn(this.config.vibePrompt)
|
|
772
1118
|
// Filter out API errors (429s, rate limits) — not agent responses
|
|
773
1119
|
const agentReplies = replies.filter(r => !this.isApiError(r))
|
|
774
|
-
|
|
1120
|
+
// Treat empty replies as NO_REPLY — gateway returns empty payloads
|
|
1121
|
+
// when the agent responds with text-only (no tool calls)
|
|
1122
|
+
if (agentReplies.length === 0 || agentReplies.some(r => r.trim().toUpperCase() === 'NO_REPLY')) {
|
|
775
1123
|
noReply = true
|
|
776
1124
|
} else if (!this.isCompliantReply(agentReplies)) {
|
|
777
1125
|
misaligned = agentReplies.filter(r => {
|
|
@@ -919,6 +1267,7 @@ export class Coordinator {
|
|
|
919
1267
|
this.logger.error('[Coordinator] Failed to trigger agent:', err)
|
|
920
1268
|
} finally {
|
|
921
1269
|
this._busy = false
|
|
1270
|
+
this.checkPlanProgress()
|
|
922
1271
|
}
|
|
923
1272
|
}).catch(() => {})
|
|
924
1273
|
}
|
|
@@ -938,6 +1287,12 @@ export class Coordinator {
|
|
|
938
1287
|
? '\n(To reply to a specific message, prefix with its ID: [msgId] your reply)'
|
|
939
1288
|
: ''
|
|
940
1289
|
|
|
941
|
-
|
|
1290
|
+
// In plan mode with no active plan, instruct agent to create one
|
|
1291
|
+
let planInstruction = ''
|
|
1292
|
+
if (this.config.autonomyMode === 'plan' && !this.hasPendingPlanSteps()) {
|
|
1293
|
+
planInstruction = '\n\nYou are in plan mode. Create a plan using plan_set based on these messages or your own ideas, then start working on it. Never mention plans or steps on stream — just do things naturally.'
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
return `${header}\n${lines.join('\n')}${instruction}${planInstruction}`
|
|
942
1297
|
}
|
|
943
1298
|
}
|