@swarmclawai/swarmclaw 0.6.7 → 0.6.8
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/README.md +24 -6
- package/package.json +1 -1
- package/src/app/api/agents/route.ts +1 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/route.ts +5 -0
- package/src/app/api/tasks/route.ts +2 -0
- package/src/app/api/usage/route.ts +9 -2
- package/src/cli/index.js +24 -0
- package/src/components/agents/agent-sheet.tsx +27 -6
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/message-list.tsx +19 -3
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/connectors/connector-sheet.tsx +8 -1
- package/src/components/home/home-view.tsx +39 -15
- package/src/components/layout/app-layout.tsx +18 -2
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +9 -2
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -4
- package/src/components/tasks/approvals-panel.tsx +120 -0
- package/src/components/usage/metrics-dashboard.tsx +25 -3
- package/src/lib/server/chat-execution.ts +96 -12
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/daemon-state.ts +70 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/main-agent-loop.ts +114 -15
- package/src/lib/server/memory-db.ts +18 -7
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +3 -0
- package/src/lib/server/plugins.ts +44 -22
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +27 -0
- package/src/lib/server/session-run-manager.ts +21 -1
- package/src/lib/server/session-tools/http.ts +19 -9
- package/src/lib/server/session-tools/index.ts +34 -0
- package/src/lib/server/session-tools/memory.ts +39 -11
- package/src/lib/server/session-tools/schedule.ts +43 -0
- package/src/lib/server/session-tools/web.ts +35 -11
- package/src/lib/server/storage.ts +12 -0
- package/src/lib/server/stream-agent-chat.ts +57 -8
- package/src/lib/server/tool-capability-policy.ts +1 -0
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/types/index.ts +34 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OpenClaw Guardian — Auto-Rollback capability.
|
|
7
|
+
* If an agent fails a task critically and has autoRecovery enabled,
|
|
8
|
+
* we attempt to roll back the workspace to the last known good state.
|
|
9
|
+
*/
|
|
10
|
+
export function performGuardianRollback(cwd: string): { ok: boolean; reason?: string } {
|
|
11
|
+
try {
|
|
12
|
+
const gitDir = path.join(cwd, '.git')
|
|
13
|
+
if (!fs.existsSync(gitDir)) {
|
|
14
|
+
return { ok: false, reason: 'Workspace is not a git repository. Cannot rollback.' }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check if dirty
|
|
18
|
+
const status = execSync('git status --porcelain', { cwd, encoding: 'utf8' })
|
|
19
|
+
if (!status.trim()) {
|
|
20
|
+
return { ok: false, reason: 'Workspace is clean. Nothing to rollback.' }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log(`[guardian] Auto-recovery triggered in ${cwd}. Rolling back changes...`)
|
|
24
|
+
|
|
25
|
+
// Perform rollback
|
|
26
|
+
execSync('git reset --hard HEAD', { cwd, encoding: 'utf8' })
|
|
27
|
+
execSync('git clean -fd', { cwd, encoding: 'utf8' })
|
|
28
|
+
|
|
29
|
+
return { ok: true }
|
|
30
|
+
} catch (err: unknown) {
|
|
31
|
+
console.error('[guardian] Auto-rollback failed:', err)
|
|
32
|
+
return { ok: false, reason: `Git operation failed: ${err instanceof Error ? err.message : String(err)}` }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -132,6 +132,45 @@ function readHeartbeatFile(session: any): string {
|
|
|
132
132
|
return ''
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
function readIdentityFile(session: Record<string, unknown>): Record<string, string> {
|
|
136
|
+
try {
|
|
137
|
+
const filePath = path.join(typeof session.cwd === 'string' ? session.cwd : WORKSPACE_DIR, 'IDENTITY.md')
|
|
138
|
+
if (fs.existsSync(filePath)) {
|
|
139
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
140
|
+
const identity: Record<string, string> = {}
|
|
141
|
+
for (const line of content.split('\n')) {
|
|
142
|
+
const cleaned = line.trim().replace(/^\s*-\s*/, '')
|
|
143
|
+
const colonIndex = cleaned.indexOf(':')
|
|
144
|
+
if (colonIndex === -1) continue
|
|
145
|
+
const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, '').trim().toLowerCase()
|
|
146
|
+
const value = cleaned.slice(colonIndex + 1).replace(/^[*_]+|[*_]+$/g, '').trim()
|
|
147
|
+
if (value) identity[label] = value
|
|
148
|
+
}
|
|
149
|
+
return identity
|
|
150
|
+
}
|
|
151
|
+
} catch { /* ignore */ }
|
|
152
|
+
return {}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function buildIdentityContext(session: Record<string, unknown> | undefined | null, agent: Record<string, unknown> | undefined | null): string {
|
|
156
|
+
const fileId = session ? readIdentityFile(session) : {}
|
|
157
|
+
const name = fileId.name || agent?.name || ''
|
|
158
|
+
const emoji = fileId.emoji || agent?.emoji || ''
|
|
159
|
+
const creature = fileId.creature || agent?.creature || ''
|
|
160
|
+
const vibe = fileId.vibe || agent?.vibe || ''
|
|
161
|
+
const theme = fileId.theme || agent?.theme || ''
|
|
162
|
+
|
|
163
|
+
const lines = []
|
|
164
|
+
if (name) lines.push(`Name: ${name}`)
|
|
165
|
+
if (emoji) lines.push(`Emoji: ${emoji}`)
|
|
166
|
+
if (creature) lines.push(`Creature: ${creature}`)
|
|
167
|
+
if (vibe) lines.push(`Vibe: ${vibe}`)
|
|
168
|
+
if (theme) lines.push(`Theme: ${theme}`)
|
|
169
|
+
|
|
170
|
+
if (lines.length === 0) return ''
|
|
171
|
+
return `## Your Identity\n${lines.join('\n')}`
|
|
172
|
+
}
|
|
173
|
+
|
|
135
174
|
/** Detect HEARTBEAT.md files that contain only skeleton structure (headers, empty list items) but no real content. */
|
|
136
175
|
export function isHeartbeatContentEffectivelyEmpty(content: string | undefined | null): boolean {
|
|
137
176
|
if (!content || typeof content !== 'string') return true
|
|
@@ -148,6 +187,7 @@ export function isHeartbeatContentEffectivelyEmpty(content: string | undefined |
|
|
|
148
187
|
function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: string, heartbeatFileContent: string): string {
|
|
149
188
|
if (!agent) return fallbackPrompt
|
|
150
189
|
|
|
190
|
+
const identityContext = buildIdentityContext(session, agent)
|
|
151
191
|
// Drain system events accumulated since last heartbeat
|
|
152
192
|
const events = drainSystemEvents(session.id)
|
|
153
193
|
const eventBlock = events.length > 0
|
|
@@ -178,7 +218,7 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
|
|
|
178
218
|
return [
|
|
179
219
|
'AGENT_HEARTBEAT_TICK',
|
|
180
220
|
`Time: ${new Date().toISOString()}`,
|
|
181
|
-
|
|
221
|
+
identityContext,
|
|
182
222
|
description ? `Description: ${description}` : '',
|
|
183
223
|
eventBlock ? `Events since last heartbeat:\n${eventBlock}` : '',
|
|
184
224
|
dynamicGoal
|
|
@@ -202,6 +242,14 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
|
|
|
202
242
|
].filter(Boolean).join('\n')
|
|
203
243
|
}
|
|
204
244
|
|
|
245
|
+
function applyMomentumMultiplier(intervalSec: number, momentumScore: number): number {
|
|
246
|
+
let multiplier = 1.0
|
|
247
|
+
if (momentumScore >= 80) multiplier = 0.5
|
|
248
|
+
else if (momentumScore < 40) multiplier = 2.0
|
|
249
|
+
const adjusted = Math.round(intervalSec * multiplier)
|
|
250
|
+
return Math.max(30, Math.min(7200, adjusted))
|
|
251
|
+
}
|
|
252
|
+
|
|
205
253
|
function resolveInterval(obj: Record<string, any>, currentSec: number): number {
|
|
206
254
|
// Prefer heartbeatInterval (duration string) over heartbeatIntervalSec (raw number)
|
|
207
255
|
if (obj.heartbeatInterval !== undefined && obj.heartbeatInterval !== null) {
|
|
@@ -347,6 +395,10 @@ async function tickHeartbeats() {
|
|
|
347
395
|
const cfg = heartbeatConfigForSession(session, settings, agents)
|
|
348
396
|
if (!cfg.enabled) continue
|
|
349
397
|
|
|
398
|
+
// Apply momentum-based multiplier to heartbeat interval
|
|
399
|
+
const momentumScore = session.mainLoopState?.momentumScore ?? 40
|
|
400
|
+
cfg.intervalSec = applyMomentumMultiplier(cfg.intervalSec, momentumScore)
|
|
401
|
+
|
|
350
402
|
// For sessions with explicit opt-in, use a shorter idle threshold (just intervalSec * 2).
|
|
351
403
|
// For inherited/global heartbeats, keep the 180s minimum to avoid noisy auto-fire.
|
|
352
404
|
const defaultIdleSec = explicitOptIn
|
|
@@ -266,6 +266,16 @@ export class SqliteCheckpointSaver extends BaseCheckpointSaver {
|
|
|
266
266
|
this.db.prepare(`DELETE FROM langgraph_checkpoints WHERE thread_id = ?`).run(threadId)
|
|
267
267
|
this.db.prepare(`DELETE FROM langgraph_writes WHERE thread_id = ?`).run(threadId)
|
|
268
268
|
}
|
|
269
|
+
|
|
270
|
+
async deleteCheckpoint(threadId: string, checkpointId: string): Promise<void> {
|
|
271
|
+
this.db.prepare(`DELETE FROM langgraph_checkpoints WHERE thread_id = ? AND checkpoint_id = ?`).run(threadId, checkpointId)
|
|
272
|
+
this.db.prepare(`DELETE FROM langgraph_writes WHERE thread_id = ? AND checkpoint_id = ?`).run(threadId, checkpointId)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async deleteCheckpointsAfter(threadId: string, timestamp: number): Promise<void> {
|
|
276
|
+
this.db.prepare(`DELETE FROM langgraph_checkpoints WHERE thread_id = ? AND created_at > ?`).run(threadId, timestamp)
|
|
277
|
+
this.db.prepare(`DELETE FROM langgraph_writes WHERE thread_id = ? AND checkpoint_id NOT IN (SELECT checkpoint_id FROM langgraph_checkpoints WHERE thread_id = ?)`).run(threadId, threadId)
|
|
278
|
+
}
|
|
269
279
|
}
|
|
270
280
|
|
|
271
281
|
let _saver: SqliteCheckpointSaver | undefined
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as cheerio from 'cheerio'
|
|
2
|
+
import { truncate } from './session-tools/context'
|
|
3
|
+
|
|
4
|
+
const BARE_LINK_RE = /https?:\/\/\S+/gi
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Automatically fetch and summarize links found in user messages.
|
|
8
|
+
* This aligns SwarmClaw with OpenClaw's proactive link-understanding feature.
|
|
9
|
+
*/
|
|
10
|
+
export async function runLinkUnderstanding(message: string): Promise<string[]> {
|
|
11
|
+
const links = message.match(BARE_LINK_RE)
|
|
12
|
+
if (!links || links.length === 0) return []
|
|
13
|
+
|
|
14
|
+
const uniqueLinks = Array.from(new Set(links)).slice(0, 3) // Limit to first 3 links
|
|
15
|
+
const results: string[] = []
|
|
16
|
+
|
|
17
|
+
for (const url of uniqueLinks) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(url, {
|
|
20
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
|
|
21
|
+
signal: AbortSignal.timeout(8000),
|
|
22
|
+
})
|
|
23
|
+
if (!res.ok) continue
|
|
24
|
+
|
|
25
|
+
const contentType = res.headers.get('content-type') || ''
|
|
26
|
+
if (contentType.includes('text/html')) {
|
|
27
|
+
const html = await res.text()
|
|
28
|
+
const $ = cheerio.load(html)
|
|
29
|
+
|
|
30
|
+
// Handle YouTube specifically (OpenClaw favorite)
|
|
31
|
+
if (url.includes('youtube.com/') || url.includes('youtu.be/')) {
|
|
32
|
+
const title = $('meta[property="og:title"]').attr('content') || $('title').text()
|
|
33
|
+
const desc = $('meta[property="og:description"]').attr('content') || ''
|
|
34
|
+
results.push(`[Link Analysis: YouTube] ${url}\nTitle: ${title}\nDescription: ${desc}`)
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// General web page extraction
|
|
39
|
+
$('script, style, noscript, nav, footer, header').remove()
|
|
40
|
+
const title = $('title').text().trim()
|
|
41
|
+
const main = $('article, main, [role="main"]').first()
|
|
42
|
+
const bodyText = (main.length ? main.text() : $('body').text())
|
|
43
|
+
.replace(/\s+/g, ' ')
|
|
44
|
+
.trim()
|
|
45
|
+
|
|
46
|
+
results.push(`[Link Analysis] ${url}\nTitle: ${title}\nContent: ${truncate(bodyText, 1000)}`)
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// Fail silently for link understanding — don't block the main run
|
|
50
|
+
console.error(`Link understanding failed for ${url}:`, err)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return results
|
|
55
|
+
}
|
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import type { GoalContract, MessageToolEvent } from '@/types'
|
|
4
|
-
import { loadSessions, saveSessions, loadAgents, saveAgents, loadTasks, saveTasks } from './storage'
|
|
4
|
+
import { loadSessions, saveSessions, loadAgents, saveAgents, loadTasks, saveTasks, loadSettings } from './storage'
|
|
5
5
|
import { log } from './logger'
|
|
6
6
|
import { getMemoryDb } from './memory-db'
|
|
7
7
|
import { isProtectedMainSession } from './main-session'
|
|
8
|
+
import { logExecution } from './execution-log'
|
|
8
9
|
import {
|
|
9
10
|
mergeGoalContracts,
|
|
10
11
|
parseGoalContractFromText,
|
|
11
12
|
parseMainLoopPlan,
|
|
12
13
|
parseMainLoopReview,
|
|
13
14
|
} from './autonomy-contract'
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
import { buildIdentityContext } from './heartbeat-service'
|
|
16
|
+
|
|
17
|
+
const MAX_PENDING_EVENTS = 60
|
|
18
|
+
const MAX_TIMELINE_EVENTS = 120
|
|
16
19
|
const EVENT_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
|
17
|
-
const MEMORY_NOTE_MIN_INTERVAL_MS =
|
|
20
|
+
const MEMORY_NOTE_MIN_INTERVAL_MS = 30 * 60 * 1000
|
|
18
21
|
const DEFAULT_FOLLOWUP_DELAY_SEC = 45
|
|
19
|
-
const
|
|
22
|
+
const DEFAULT_MAX_FOLLOWUP_CHAIN = 20
|
|
23
|
+
function getMaxFollowupChain(agentId: string | undefined): number {
|
|
24
|
+
if (agentId) {
|
|
25
|
+
const agents = loadAgents()
|
|
26
|
+
const agent = agents[agentId]
|
|
27
|
+
if (typeof agent?.maxFollowupChain === 'number' && agent.maxFollowupChain > 0) {
|
|
28
|
+
return Math.min(agent.maxFollowupChain, 100)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const settings = loadSettings()
|
|
32
|
+
if (typeof settings?.maxFollowupChain === 'number' && settings.maxFollowupChain > 0) {
|
|
33
|
+
return Math.min(settings.maxFollowupChain, 100)
|
|
34
|
+
}
|
|
35
|
+
return DEFAULT_MAX_FOLLOWUP_CHAIN
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
const META_LINE_RE = /\[MAIN_LOOP_META\]\s*(\{[^\n]*\})/i
|
|
21
39
|
const AGENT_HEARTBEAT_META_RE = /\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i
|
|
22
40
|
const SCREENSHOT_GOAL_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i
|
|
@@ -25,6 +43,14 @@ const SCHEDULE_GOAL_HINT = /\b(schedule|scheduled|every\s+\w+|interval|cron|recu
|
|
|
25
43
|
const UPLOAD_ARTIFACT_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf)\b/i
|
|
26
44
|
const SENT_ARTIFACT_HINT = /\b(sent|shared|uploaded|returned)\b[^.]*\b(screenshot|snapshot|image|file)\b/i
|
|
27
45
|
|
|
46
|
+
const COMPANION_GOAL_PROMPT = `
|
|
47
|
+
## Identity & Vibe
|
|
48
|
+
You are a persistent companion.
|
|
49
|
+
1. **Identity**: Embody your creature, theme, and vibe. Your emoji is your signature.
|
|
50
|
+
2. **Workspace Context**: Respect the current workspace. Read IDENTITY.md and HEARTBEAT.md if they exist.
|
|
51
|
+
3. **Continuity**: Maintain awareness of the user's long-term journey. Proactively help with open-ended goals without being asked for every step.
|
|
52
|
+
`.trim()
|
|
53
|
+
|
|
28
54
|
interface MainLoopSessionMessageLike {
|
|
29
55
|
text?: string
|
|
30
56
|
}
|
|
@@ -45,13 +71,12 @@ export interface MainLoopTimelineEntry {
|
|
|
45
71
|
at: number
|
|
46
72
|
source: string
|
|
47
73
|
note: string
|
|
48
|
-
status?: 'idle' | 'progress' | 'blocked' | 'ok'
|
|
74
|
+
status?: 'idle' | 'progress' | 'blocked' | 'ok' | 'reflection'
|
|
49
75
|
}
|
|
50
76
|
|
|
51
77
|
export interface MainLoopState {
|
|
52
78
|
goal: string | null
|
|
53
79
|
goalContract: GoalContract | null
|
|
54
|
-
status: 'idle' | 'progress' | 'blocked' | 'ok'
|
|
55
80
|
summary: string | null
|
|
56
81
|
nextAction: string | null
|
|
57
82
|
planSteps: string[]
|
|
@@ -61,9 +86,12 @@ export interface MainLoopState {
|
|
|
61
86
|
missionTaskId: string | null
|
|
62
87
|
momentumScore: number
|
|
63
88
|
paused: boolean
|
|
89
|
+
status: 'idle' | 'progress' | 'blocked' | 'ok'
|
|
64
90
|
autonomyMode: 'assist' | 'autonomous'
|
|
65
91
|
pendingEvents: MainLoopEvent[]
|
|
66
92
|
timeline: MainLoopTimelineEntry[]
|
|
93
|
+
missionTokens: number
|
|
94
|
+
missionCostUsd: number
|
|
67
95
|
followupChainCount: number
|
|
68
96
|
metaMissCount: number
|
|
69
97
|
workingMemoryNotes: string[]
|
|
@@ -104,6 +132,9 @@ export interface HandleMainLoopRunResultInput {
|
|
|
104
132
|
resultText: string
|
|
105
133
|
error?: string
|
|
106
134
|
toolEvents?: MessageToolEvent[]
|
|
135
|
+
inputTokens?: number
|
|
136
|
+
outputTokens?: number
|
|
137
|
+
estimatedCost?: number
|
|
107
138
|
}
|
|
108
139
|
|
|
109
140
|
function toOneLine(value: string, max = 240): string {
|
|
@@ -143,7 +174,7 @@ function appendTimeline(
|
|
|
143
174
|
source: string,
|
|
144
175
|
note: string,
|
|
145
176
|
now = Date.now(),
|
|
146
|
-
status?: 'idle' | 'progress' | 'blocked' | 'ok',
|
|
177
|
+
status?: 'idle' | 'progress' | 'blocked' | 'ok' | 'reflection',
|
|
147
178
|
) {
|
|
148
179
|
const normalizedNote = toOneLine(note, 400)
|
|
149
180
|
if (!normalizedNote) return
|
|
@@ -278,6 +309,8 @@ function normalizeState(raw: any, now = Date.now()): MainLoopState {
|
|
|
278
309
|
autonomyMode: raw?.autonomyMode === 'assist' ? 'assist' : 'autonomous',
|
|
279
310
|
pendingEvents,
|
|
280
311
|
timeline,
|
|
312
|
+
missionTokens: typeof raw?.missionTokens === 'number' && Number.isFinite(raw.missionTokens) ? raw.missionTokens : 0,
|
|
313
|
+
missionCostUsd: typeof raw?.missionCostUsd === 'number' && Number.isFinite(raw.missionCostUsd) ? raw.missionCostUsd : 0,
|
|
281
314
|
followupChainCount: clampInt(raw?.followupChainCount, 0, 0, 100),
|
|
282
315
|
metaMissCount: clampInt(raw?.metaMissCount, 0, 0, 100),
|
|
283
316
|
workingMemoryNotes: normalizeStringList(raw?.workingMemoryNotes, 24, 260),
|
|
@@ -489,6 +522,7 @@ function upsertMissionTask(session: any, state: MainLoopState, now: number): str
|
|
|
489
522
|
const statusMap = {
|
|
490
523
|
idle: 'backlog',
|
|
491
524
|
progress: 'running',
|
|
525
|
+
reflection: 'running',
|
|
492
526
|
blocked: 'failed',
|
|
493
527
|
ok: 'completed',
|
|
494
528
|
} as const
|
|
@@ -603,6 +637,9 @@ function maybeStoreMissionMemoryNote(
|
|
|
603
637
|
state.reviewNote ? `review: ${state.reviewNote}` : '',
|
|
604
638
|
typeof state.reviewConfidence === 'number' ? `review_confidence: ${state.reviewConfidence}` : '',
|
|
605
639
|
state.missionTaskId ? `mission_task_id: ${state.missionTaskId}` : '',
|
|
640
|
+
typeof state.missionTokens === 'number' ? `mission_tokens: ${state.missionTokens}` : '',
|
|
641
|
+
typeof state.missionCostUsd === 'number' ? `mission_cost_usd: $${state.missionCostUsd.toFixed(4)}` : '',
|
|
642
|
+
state.workingMemoryNotes?.length ? `working_notes: ${state.workingMemoryNotes.slice(-5).join('; ')}` : '',
|
|
606
643
|
].filter(Boolean).join('\n')
|
|
607
644
|
|
|
608
645
|
try {
|
|
@@ -622,20 +659,27 @@ function maybeStoreMissionMemoryNote(
|
|
|
622
659
|
category: 'mission',
|
|
623
660
|
title,
|
|
624
661
|
content,
|
|
625
|
-
}
|
|
662
|
+
})
|
|
626
663
|
state.lastMemoryNoteAt = now
|
|
627
|
-
|
|
628
|
-
|
|
664
|
+
logExecution(session.id, 'mission_checkpoint', `Checkpoint: ${toOneLine(state.goal || '', 120)}`, {
|
|
665
|
+
agentId: session.agentId,
|
|
666
|
+
detail: { momentumScore: state.momentumScore, followupChainCount: state.followupChainCount, missionTokens: state.missionTokens, missionCostUsd: state.missionCostUsd },
|
|
667
|
+
})
|
|
668
|
+
} catch (err: unknown) {
|
|
669
|
+
appendEvent(state, 'memory_note_error', `Failed to store mission memory note: ${toOneLine(err instanceof Error ? err.message : String(err), 240)}`, now)
|
|
629
670
|
}
|
|
630
671
|
}
|
|
631
672
|
|
|
632
|
-
function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: boolean }): string {
|
|
673
|
+
function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: boolean; agent?: Record<string, unknown> | null; session?: Record<string, unknown> | null }): string {
|
|
633
674
|
const hasMemoryTool = opts?.hasMemoryTool === true
|
|
675
|
+
const identityContext = buildIdentityContext(opts?.session, opts?.agent)
|
|
634
676
|
const goal = state.goal || 'No explicit goal yet. Continue with the strongest actionable objective from recent context.'
|
|
635
677
|
const nextAction = state.nextAction || 'Determine the next highest-impact action and execute it.'
|
|
636
678
|
const contractLines = buildGoalContractLines(state)
|
|
637
679
|
return [
|
|
638
680
|
'SWARM_MAIN_AUTO_FOLLOWUP',
|
|
681
|
+
identityContext,
|
|
682
|
+
COMPANION_GOAL_PROMPT,
|
|
639
683
|
`Mission goal: ${goal}`,
|
|
640
684
|
`Next action to execute now: ${nextAction}`,
|
|
641
685
|
`Current status: ${state.status}`,
|
|
@@ -647,6 +691,9 @@ function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: bool
|
|
|
647
691
|
state.reviewNote ? `Last review: ${state.reviewNote}` : '',
|
|
648
692
|
buildPendingEventLines(state),
|
|
649
693
|
buildTimelineLines(state),
|
|
694
|
+
state.planSteps.length === 0 && state.followupChainCount === 0
|
|
695
|
+
? 'Before executing, break the mission goal into 3-7 concrete subtasks. Output a [MAIN_LOOP_PLAN] JSON line with your plan, then execute the first step immediately.'
|
|
696
|
+
: '',
|
|
650
697
|
'Act autonomously. Use available tools to execute work, verify results, and keep momentum.',
|
|
651
698
|
state.autonomyMode === 'assist'
|
|
652
699
|
? 'Assist mode: execute safe internal analysis by default, and ask before irreversible external side effects (sending messages, purchases, account mutations).'
|
|
@@ -673,6 +720,9 @@ export function isMainSession(session: any): boolean {
|
|
|
673
720
|
|
|
674
721
|
export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: string): string {
|
|
675
722
|
const now = Date.now()
|
|
723
|
+
const agents = loadAgents()
|
|
724
|
+
const agent = session.agentId ? agents[session.agentId] : null
|
|
725
|
+
const identityContext = buildIdentityContext(session, agent)
|
|
676
726
|
const state = normalizeState(session?.mainLoopState, now)
|
|
677
727
|
const goal = state.goal || inferGoalFromSessionMessages(session) || null
|
|
678
728
|
const hasMemoryTool = Array.isArray(session?.tools) && session.tools.includes('memory')
|
|
@@ -684,6 +734,8 @@ export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: strin
|
|
|
684
734
|
|
|
685
735
|
return [
|
|
686
736
|
'SWARM_MAIN_MISSION_TICK',
|
|
737
|
+
identityContext,
|
|
738
|
+
COMPANION_GOAL_PROMPT,
|
|
687
739
|
`Time: ${new Date(now).toISOString()}`,
|
|
688
740
|
`Mission goal: ${promptGoal}`,
|
|
689
741
|
`Current status: ${state.status}`,
|
|
@@ -902,6 +954,10 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
902
954
|
appendTimeline(state, 'user_goal', `Goal updated: ${userGoal}`, now, state.status)
|
|
903
955
|
appendWorkingMemoryNote(state, `goal:${userGoal}`)
|
|
904
956
|
forceMemoryNote = true
|
|
957
|
+
logExecution(input.sessionId, 'mission_start', `New goal: ${toOneLine(userGoal, 200)}`, {
|
|
958
|
+
agentId: session.agentId,
|
|
959
|
+
detail: { goal: userGoal, planSteps: state.planSteps },
|
|
960
|
+
})
|
|
905
961
|
} else if (userGoalContract?.objective) {
|
|
906
962
|
state.goal = userGoalContract.objective
|
|
907
963
|
state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
|
|
@@ -909,8 +965,22 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
909
965
|
appendTimeline(state, 'user_goal_contract', `Goal contract updated: ${userGoalContract.objective}`, now, state.status)
|
|
910
966
|
appendWorkingMemoryNote(state, `contract:${userGoalContract.objective}`)
|
|
911
967
|
forceMemoryNote = true
|
|
968
|
+
logExecution(input.sessionId, 'mission_start', `New goal contract: ${toOneLine(userGoalContract.objective, 200)}`, {
|
|
969
|
+
agentId: session.agentId,
|
|
970
|
+
detail: { goal: userGoalContract.objective, contract: userGoalContract, planSteps: state.planSteps },
|
|
971
|
+
})
|
|
912
972
|
}
|
|
913
973
|
state.followupChainCount = 0
|
|
974
|
+
state.missionTokens = 0
|
|
975
|
+
state.missionCostUsd = 0
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Accumulate per-mission token/cost tracking
|
|
979
|
+
if (typeof input.inputTokens === 'number') {
|
|
980
|
+
state.missionTokens = (state.missionTokens || 0) + input.inputTokens + (input.outputTokens || 0)
|
|
981
|
+
}
|
|
982
|
+
if (typeof input.estimatedCost === 'number') {
|
|
983
|
+
state.missionCostUsd = (state.missionCostUsd || 0) + input.estimatedCost
|
|
914
984
|
}
|
|
915
985
|
|
|
916
986
|
if (state.paused && input.internal) {
|
|
@@ -958,7 +1028,7 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
958
1028
|
|
|
959
1029
|
if (shouldAutoKickFromUserGoal) {
|
|
960
1030
|
followup = {
|
|
961
|
-
message: buildFollowupPrompt(state, { hasMemoryTool }),
|
|
1031
|
+
message: buildFollowupPrompt(state, { hasMemoryTool, agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
|
|
962
1032
|
delayMs: 1500,
|
|
963
1033
|
dedupeKey: `main-loop-user-kickoff:${input.sessionId}`,
|
|
964
1034
|
}
|
|
@@ -1020,11 +1090,33 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1020
1090
|
)
|
|
1021
1091
|
consumeEvents(state, meta.consume_event_ids)
|
|
1022
1092
|
|
|
1023
|
-
|
|
1093
|
+
// Budget enforcement: check mission cost against goalContract.budgetUsd
|
|
1094
|
+
const budgetUsd = state.goalContract?.budgetUsd
|
|
1095
|
+
if (typeof budgetUsd === 'number' && budgetUsd > 0 && typeof state.missionCostUsd === 'number') {
|
|
1096
|
+
const usageRatio = state.missionCostUsd / budgetUsd
|
|
1097
|
+
if (usageRatio >= 1.0 && !state.paused) {
|
|
1098
|
+
state.paused = true
|
|
1099
|
+
state.status = 'blocked'
|
|
1100
|
+
appendTimeline(state, 'budget_exceeded', `Mission budget exceeded ($${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}). Mission paused.`, now, 'blocked')
|
|
1101
|
+
appendEvent(state, 'budget_exceeded', `Budget limit reached: $${state.missionCostUsd.toFixed(4)} of $${budgetUsd.toFixed(4)}`, now)
|
|
1102
|
+
logExecution(input.sessionId, 'budget_warning', `Budget exceeded: $${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}`, {
|
|
1103
|
+
agentId: session.agentId,
|
|
1104
|
+
detail: { missionCostUsd: state.missionCostUsd, budgetUsd, missionTokens: state.missionTokens },
|
|
1105
|
+
})
|
|
1106
|
+
} else if (usageRatio >= 0.8) {
|
|
1107
|
+
appendEvent(state, 'budget_warning', `Mission approaching budget limit: $${state.missionCostUsd.toFixed(4)} of $${budgetUsd.toFixed(4)} (${Math.round(usageRatio * 100)}%)`, now)
|
|
1108
|
+
logExecution(input.sessionId, 'budget_warning', `Budget at ${Math.round(usageRatio * 100)}%: $${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}`, {
|
|
1109
|
+
agentId: session.agentId,
|
|
1110
|
+
detail: { missionCostUsd: state.missionCostUsd, budgetUsd, usagePercent: Math.round(usageRatio * 100) },
|
|
1111
|
+
})
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (meta.follow_up === true && !input.error && !isHeartbeatOk && !state.paused && state.followupChainCount < getMaxFollowupChain(session.agentId)) {
|
|
1024
1116
|
state.followupChainCount += 1
|
|
1025
1117
|
const delaySec = clampInt(meta.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900)
|
|
1026
1118
|
followup = {
|
|
1027
|
-
message: buildFollowupPrompt(state, { hasMemoryTool }),
|
|
1119
|
+
message: buildFollowupPrompt(state, { hasMemoryTool, agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
|
|
1028
1120
|
delayMs: delaySec * 1000,
|
|
1029
1121
|
dedupeKey: `main-loop-followup:${input.sessionId}`,
|
|
1030
1122
|
}
|
|
@@ -1034,6 +1126,12 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1034
1126
|
}
|
|
1035
1127
|
if (state.status === 'ok' || state.status === 'blocked') {
|
|
1036
1128
|
forceMemoryNote = true
|
|
1129
|
+
if (state.status === 'ok') {
|
|
1130
|
+
logExecution(input.sessionId, 'mission_complete', `Mission completed: ${toOneLine(state.goal || 'unknown', 200)}`, {
|
|
1131
|
+
agentId: session.agentId,
|
|
1132
|
+
detail: { momentumScore: state.momentumScore, followupChainCount: state.followupChainCount, missionTokens: state.missionTokens, missionCostUsd: state.missionCostUsd },
|
|
1133
|
+
})
|
|
1134
|
+
}
|
|
1037
1135
|
}
|
|
1038
1136
|
} else if (!isHeartbeatOk && trimmedText) {
|
|
1039
1137
|
state.metaMissCount = Math.min(100, state.metaMissCount + 1)
|
|
@@ -1064,6 +1162,7 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1064
1162
|
state.missionTaskId = upsertMissionTask(session, state, now)
|
|
1065
1163
|
const shouldWritePeriodicMemory = !!state.summary && state.status === 'progress'
|
|
1066
1164
|
maybeStoreMissionMemoryNote(session, state, now, input.source, forceMemoryNote || shouldWritePeriodicMemory)
|
|
1165
|
+
|
|
1067
1166
|
state.momentumScore = computeMomentumScore(state)
|
|
1068
1167
|
|
|
1069
1168
|
state.updatedAt = now
|
|
@@ -5,6 +5,7 @@ import { createHash } from 'crypto'
|
|
|
5
5
|
import { genId } from '@/lib/id'
|
|
6
6
|
import type { MemoryEntry, FileReference, MemoryImage, MemoryReference } from '@/types'
|
|
7
7
|
import { getEmbedding, cosineSimilarity, serializeEmbedding, deserializeEmbedding } from './embeddings'
|
|
8
|
+
import { applyMMR } from './mmr'
|
|
8
9
|
import { loadSettings } from './storage'
|
|
9
10
|
import {
|
|
10
11
|
normalizeLinkedMemoryIds,
|
|
@@ -23,8 +24,8 @@ const MAX_IMAGE_INPUT_BYTES = 10 * 1024 * 1024 // 10MB
|
|
|
23
24
|
const IMAGE_EXT_WHITELIST = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff'])
|
|
24
25
|
export const MAX_FTS_QUERY_TERMS = 6
|
|
25
26
|
export const MAX_FTS_TERM_LENGTH = 48
|
|
26
|
-
const MAX_FTS_RESULT_ROWS =
|
|
27
|
-
const MAX_MERGED_RESULTS =
|
|
27
|
+
const MAX_FTS_RESULT_ROWS = 50
|
|
28
|
+
const MAX_MERGED_RESULTS = 80
|
|
28
29
|
|
|
29
30
|
export const MEMORY_FTS_STOP_WORDS = new Set([
|
|
30
31
|
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'how',
|
|
@@ -865,9 +866,12 @@ function initDb() {
|
|
|
865
866
|
|
|
866
867
|
// Attempt vector search (synchronous — uses cached embedding if available)
|
|
867
868
|
const vectorSimilarityScores = new Map<string, number>()
|
|
869
|
+
const rawEmbeddings = new Map<string, number[]>()
|
|
868
870
|
let vectorResults: MemoryEntry[] = []
|
|
871
|
+
let queryEmbeddingResult: number[] | undefined
|
|
869
872
|
try {
|
|
870
873
|
const queryEmbedding = getEmbeddingSync(query)
|
|
874
|
+
queryEmbeddingResult = queryEmbedding || undefined
|
|
871
875
|
if (queryEmbedding) {
|
|
872
876
|
const rows = agentId
|
|
873
877
|
? getAllWithEmbeddingsByAgent.all(agentId) as any[]
|
|
@@ -877,7 +881,7 @@ function initDb() {
|
|
|
877
881
|
.map((row) => {
|
|
878
882
|
const emb = deserializeEmbedding(row.embedding)
|
|
879
883
|
const score = cosineSimilarity(queryEmbedding, emb)
|
|
880
|
-
return { row, score }
|
|
884
|
+
return { row, score, emb }
|
|
881
885
|
})
|
|
882
886
|
.filter((s) => s.score > 0.3) // relevance threshold
|
|
883
887
|
.sort((a, b) => b.score - a.score)
|
|
@@ -886,6 +890,7 @@ function initDb() {
|
|
|
886
890
|
vectorResults = scored.map((s) => {
|
|
887
891
|
const entry = rowToEntry(s.row)
|
|
888
892
|
vectorSimilarityScores.set(entry.id, s.score)
|
|
893
|
+
rawEmbeddings.set(entry.id, s.emb)
|
|
889
894
|
return entry
|
|
890
895
|
})
|
|
891
896
|
}
|
|
@@ -913,11 +918,17 @@ function initDb() {
|
|
|
913
918
|
const reinforcement = Math.log((entry.reinforcementCount || 0) + 1) + 1
|
|
914
919
|
const pinnedBoost = entry.pinned ? 1.5 : 1.0
|
|
915
920
|
const salience = similarity * recencyDecay * reinforcement * pinnedBoost
|
|
916
|
-
return { entry, salience }
|
|
921
|
+
return { entry, salience, embedding: rawEmbeddings.get(entry.id) }
|
|
917
922
|
})
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
923
|
+
|
|
924
|
+
// Apply MMR if we have a query embedding, otherwise standard sort
|
|
925
|
+
let out: MemoryEntry[] = []
|
|
926
|
+
if (queryEmbeddingResult) {
|
|
927
|
+
out = applyMMR(queryEmbeddingResult, salienceScored, MAX_MERGED_RESULTS, 0.6) // Lambda 0.6 = favor relevance slightly over diversity
|
|
928
|
+
} else {
|
|
929
|
+
salienceScored.sort((a, b) => b.salience - a.salience)
|
|
930
|
+
out = salienceScored.slice(0, MAX_MERGED_RESULTS).map((s) => s.entry)
|
|
931
|
+
}
|
|
921
932
|
|
|
922
933
|
// Bump access counts for returned results (non-blocking)
|
|
923
934
|
if (out.length) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { cosineSimilarity } from './embeddings'
|
|
2
|
+
import type { MemoryEntry } from '@/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Applies Maximal Marginal Relevance (MMR) to diversify search results.
|
|
6
|
+
* It balances relevance to the query (salience/similarity) against novelty
|
|
7
|
+
* compared to already-selected documents.
|
|
8
|
+
*/
|
|
9
|
+
export function applyMMR(
|
|
10
|
+
queryEmbedding: number[],
|
|
11
|
+
candidates: Array<{ entry: MemoryEntry; salience: number; embedding?: number[] }>,
|
|
12
|
+
limit: number,
|
|
13
|
+
lambda: number = 0.5
|
|
14
|
+
): MemoryEntry[] {
|
|
15
|
+
if (candidates.length === 0) return []
|
|
16
|
+
|
|
17
|
+
// Normalize salience to [0, 1] range
|
|
18
|
+
const maxSalience = Math.max(...candidates.map(c => c.salience))
|
|
19
|
+
const minSalience = Math.min(...candidates.map(c => c.salience))
|
|
20
|
+
const salienceRange = maxSalience - minSalience || 1
|
|
21
|
+
|
|
22
|
+
const candidatesWithNormalizedSalience = candidates.map(c => ({
|
|
23
|
+
...c,
|
|
24
|
+
normSalience: (c.salience - minSalience) / salienceRange
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
const selected: typeof candidatesWithNormalizedSalience = []
|
|
28
|
+
const remaining = [...candidatesWithNormalizedSalience]
|
|
29
|
+
|
|
30
|
+
// Debug: uncomment for troubleshooting
|
|
31
|
+
// console.log(`[mmr] Starting MMR for ${remaining.length} candidates, limit=${limit}, lambda=${lambda}`)
|
|
32
|
+
|
|
33
|
+
while (selected.length < limit && remaining.length > 0) {
|
|
34
|
+
let bestMmrScore = -Infinity
|
|
35
|
+
let bestIndex = -1
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
38
|
+
const candidate = remaining[i]
|
|
39
|
+
|
|
40
|
+
let maxSimToSelected = 0
|
|
41
|
+
if (selected.length > 0 && candidate.embedding) {
|
|
42
|
+
for (const sel of selected) {
|
|
43
|
+
if (sel.embedding) {
|
|
44
|
+
const sim = cosineSimilarity(candidate.embedding, sel.embedding)
|
|
45
|
+
if (sim > maxSimToSelected) maxSimToSelected = sim
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// MMR Score = Lambda * Relevance - (1 - Lambda) * Diversity (max similarity to selected)
|
|
51
|
+
const mmrScore = (lambda * candidate.normSalience) - ((1 - lambda) * maxSimToSelected)
|
|
52
|
+
|
|
53
|
+
// DEBUG LOG
|
|
54
|
+
// console.log(` Candidate ${candidate.entry.id}: rel=${candidate.normSalience.toFixed(3)}, div_penalty=${maxSimToSelected.toFixed(3)}, mmr=${mmrScore.toFixed(3)}`)
|
|
55
|
+
|
|
56
|
+
if (mmrScore > bestMmrScore) {
|
|
57
|
+
bestMmrScore = mmrScore
|
|
58
|
+
bestIndex = i
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (bestIndex !== -1) {
|
|
63
|
+
const picked = remaining[bestIndex]
|
|
64
|
+
// console.log(`[mmr] Picked ${picked.entry.id} with score ${bestMmrScore.toFixed(3)}`)
|
|
65
|
+
selected.push(picked)
|
|
66
|
+
remaining.splice(bestIndex, 1)
|
|
67
|
+
} else {
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return selected.map(s => s.entry)
|
|
73
|
+
}
|
|
@@ -12,6 +12,7 @@ import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
|
12
12
|
import { notify } from './ws-hub'
|
|
13
13
|
import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
14
14
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
15
|
+
import { getPluginManager } from './plugins'
|
|
15
16
|
import { genId } from '@/lib/id'
|
|
16
17
|
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
17
18
|
import type { Agent, TaskComment, MessageToolEvent } from '@/types'
|
|
@@ -176,6 +177,7 @@ export async function executeLangGraphOrchestrator(
|
|
|
176
177
|
return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
|
|
177
178
|
}
|
|
178
179
|
console.log(`[orchestrator-lg] Delegating to ${agent.name}: ${agentTask.slice(0, 80)}`)
|
|
180
|
+
getPluginManager().runHook('onAgentDelegation', { sourceAgentId: orchestrator.id, targetAgentId: agent.id, task: agentTask })
|
|
179
181
|
const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
|
|
180
182
|
saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
|
|
181
183
|
name: 'delegate_to_agent',
|
|
@@ -623,6 +625,7 @@ export async function resumeLangGraphOrchestrator(
|
|
|
623
625
|
async ({ agentName, task: agentTask }) => {
|
|
624
626
|
const agent = agents.find((a) => a.name.toLowerCase() === agentName.toLowerCase())
|
|
625
627
|
if (!agent) return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
|
|
628
|
+
getPluginManager().runHook('onAgentDelegation', { sourceAgentId: orchestrator.id, targetAgentId: agent.id, task: agentTask })
|
|
626
629
|
const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
|
|
627
630
|
saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
|
|
628
631
|
name: 'delegate_to_agent',
|