@swarmclawai/swarmclaw 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -45
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +18 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +11 -3
- package/src/app/api/tasks/route.ts +8 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +13 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +86 -29
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +30 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +29 -6
- package/src/components/home/home-view.tsx +20 -14
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +73 -21
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +213 -59
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +19 -7
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +170 -66
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +66 -64
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +223 -62
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +42 -0
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +180 -17
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/orchestrator-lg.ts +4 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +650 -142
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/queue.ts +253 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +11 -1
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +85 -33
- package/src/lib/server/session-tools/index.ts +205 -160
- package/src/lib/server/session-tools/memory.ts +152 -265
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +66 -31
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +179 -349
- package/src/lib/server/storage.ts +24 -0
- package/src/lib/server/stream-agent-chat.ts +301 -244
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +23 -5
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +23 -23
- package/src/lib/validation/schemas.ts +12 -0
- package/src/lib/view-routes.ts +2 -24
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +121 -7
|
@@ -5,6 +5,7 @@ const MAX_LOG_CHARS = 200_000
|
|
|
5
5
|
const DEFAULT_BACKGROUND_YIELD_MS = 10_000
|
|
6
6
|
const DEFAULT_TIMEOUT_MS = 30 * 60_000
|
|
7
7
|
const DEFAULT_TTL_MS = 30 * 60_000
|
|
8
|
+
const BACKGROUND_STARTUP_GRACE_MS = 500
|
|
8
9
|
|
|
9
10
|
export type ProcessStatus = 'running' | 'exited' | 'killed' | 'failed' | 'timeout'
|
|
10
11
|
|
|
@@ -170,6 +171,23 @@ export async function startManagedProcess(opts: StartProcessOptions): Promise<St
|
|
|
170
171
|
state.exitWaiters.set(id, exitPromise)
|
|
171
172
|
|
|
172
173
|
if (opts.background) {
|
|
174
|
+
// Give background processes a brief grace window so immediate crashes
|
|
175
|
+
// (e.g., bind/permission errors) are surfaced instead of misreported as running.
|
|
176
|
+
const startupWaitMs = Math.min(
|
|
177
|
+
Math.max(100, BACKGROUND_STARTUP_GRACE_MS),
|
|
178
|
+
Math.max(200, timeoutMs),
|
|
179
|
+
)
|
|
180
|
+
await wait(startupWaitMs)
|
|
181
|
+
const rec = state.records.get(id)
|
|
182
|
+
if (rec && rec.status !== 'running') {
|
|
183
|
+
return {
|
|
184
|
+
status: 'completed',
|
|
185
|
+
processId: id,
|
|
186
|
+
output: rec.log,
|
|
187
|
+
exitCode: rec.exitCode,
|
|
188
|
+
signal: rec.signal,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
173
191
|
return {
|
|
174
192
|
status: 'running',
|
|
175
193
|
processId: id,
|
package/src/lib/server/queue.ts
CHANGED
|
@@ -23,12 +23,14 @@ interface SessionMessageLike {
|
|
|
23
23
|
role?: string
|
|
24
24
|
text?: string
|
|
25
25
|
time?: number
|
|
26
|
-
kind?:
|
|
26
|
+
kind?: string
|
|
27
27
|
source?: {
|
|
28
28
|
connectorId?: string
|
|
29
29
|
channelId?: string
|
|
30
30
|
}
|
|
31
31
|
toolEvents?: Array<{ name?: string; output?: string }>
|
|
32
|
+
streaming?: boolean
|
|
33
|
+
imageUrl?: string
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
interface SessionLike {
|
|
@@ -97,6 +99,181 @@ function applyTaskPolicyDefaults(task: BoardTask): void {
|
|
|
97
99
|
if (task.deadLetteredAt === undefined) task.deadLetteredAt = null
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
const DEV_TASK_HINT = /\b(dev(?:\s+server)?|start(?:ing)?\s+(?:the\s+)?server|run(?:ning)?\s+(?:the\s+)?(?:app|project|site)|serve|localhost|http\s+server|web\s+server|npm\b|pnpm\b|yarn\b|bun\b|vite|next(?:\.js)?|react|build|compile)\b/i
|
|
103
|
+
const TASK_CWD_NOISE_DIRS = new Set([
|
|
104
|
+
'uploads',
|
|
105
|
+
'data',
|
|
106
|
+
'projects',
|
|
107
|
+
'tasks',
|
|
108
|
+
'.swarm-data-test',
|
|
109
|
+
'.git',
|
|
110
|
+
'.next',
|
|
111
|
+
'node_modules',
|
|
112
|
+
])
|
|
113
|
+
const PROJECT_MARKER_FILES = ['package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', '.git']
|
|
114
|
+
const SOURCE_MARKER_DIRS = ['src', 'app', 'public', 'pages']
|
|
115
|
+
const WORKSPACE_PROJECTS_DIR = path.join(WORKSPACE_DIR, 'projects')
|
|
116
|
+
|
|
117
|
+
interface WorkspaceDirCandidate {
|
|
118
|
+
dir: string
|
|
119
|
+
name: string
|
|
120
|
+
hasProjectMarker: boolean
|
|
121
|
+
hasSourceMarker: boolean
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let workspaceDirCache: { expiresAt: number; candidates: WorkspaceDirCandidate[] } | null = null
|
|
125
|
+
|
|
126
|
+
function isExistingDirectory(dirPath: string): boolean {
|
|
127
|
+
try {
|
|
128
|
+
return fs.statSync(dirPath).isDirectory()
|
|
129
|
+
} catch {
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isWithinDirectory(parent: string, child: string): boolean {
|
|
135
|
+
const parentResolved = path.resolve(parent)
|
|
136
|
+
const childResolved = path.resolve(child)
|
|
137
|
+
const rel = path.relative(parentResolved, childResolved)
|
|
138
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeForMatch(value: string): string {
|
|
142
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function hasAnyMarker(dirPath: string, markers: string[]): boolean {
|
|
146
|
+
return markers.some((marker) => fs.existsSync(path.join(dirPath, marker)))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeDirCandidate(raw: unknown, baseDir: string): string | null {
|
|
150
|
+
if (typeof raw !== 'string') return null
|
|
151
|
+
const trimmed = raw.trim()
|
|
152
|
+
if (!trimmed) return null
|
|
153
|
+
const homeDir = process.env.HOME || ''
|
|
154
|
+
const expanded = trimmed === '~'
|
|
155
|
+
? homeDir
|
|
156
|
+
: trimmed.startsWith('~/')
|
|
157
|
+
? path.join(homeDir, trimmed.slice(2))
|
|
158
|
+
: trimmed
|
|
159
|
+
const resolved = path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(baseDir, expanded)
|
|
160
|
+
return isExistingDirectory(resolved) ? resolved : null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function looksLikeDevTask(task: Pick<BoardTask, 'title' | 'description'>): boolean {
|
|
164
|
+
const text = `${task.title || ''} ${task.description || ''}`.trim()
|
|
165
|
+
return DEV_TASK_HINT.test(text)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function listWorkspaceDirCandidates(): WorkspaceDirCandidate[] {
|
|
169
|
+
const now = Date.now()
|
|
170
|
+
if (workspaceDirCache && workspaceDirCache.expiresAt > now) return workspaceDirCache.candidates
|
|
171
|
+
|
|
172
|
+
const candidates: WorkspaceDirCandidate[] = []
|
|
173
|
+
const seen = new Set<string>()
|
|
174
|
+
const roots = [WORKSPACE_DIR, WORKSPACE_PROJECTS_DIR]
|
|
175
|
+
|
|
176
|
+
for (const root of roots) {
|
|
177
|
+
if (!isExistingDirectory(root)) continue
|
|
178
|
+
let entries: fs.Dirent[] = []
|
|
179
|
+
try {
|
|
180
|
+
entries = fs.readdirSync(root, { withFileTypes: true })
|
|
181
|
+
} catch {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (!entry.isDirectory()) continue
|
|
186
|
+
const name = entry.name
|
|
187
|
+
if (!name || name.startsWith('.')) continue
|
|
188
|
+
if (TASK_CWD_NOISE_DIRS.has(name)) continue
|
|
189
|
+
const dir = path.join(root, name)
|
|
190
|
+
const key = path.resolve(dir)
|
|
191
|
+
if (seen.has(key)) continue
|
|
192
|
+
seen.add(key)
|
|
193
|
+
candidates.push({
|
|
194
|
+
dir: key,
|
|
195
|
+
name,
|
|
196
|
+
hasProjectMarker: hasAnyMarker(key, PROJECT_MARKER_FILES),
|
|
197
|
+
hasSourceMarker: hasAnyMarker(key, SOURCE_MARKER_DIRS),
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
candidates.sort((a, b) => a.name.localeCompare(b.name))
|
|
203
|
+
workspaceDirCache = {
|
|
204
|
+
expiresAt: now + 15_000,
|
|
205
|
+
candidates,
|
|
206
|
+
}
|
|
207
|
+
return candidates
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function inferWorkspaceProjectCwd(task: Pick<BoardTask, 'title' | 'description' | 'file'>): string | null {
|
|
211
|
+
const candidates = listWorkspaceDirCandidates()
|
|
212
|
+
if (!candidates.length) return null
|
|
213
|
+
|
|
214
|
+
const taskText = normalizeForMatch(`${task.title || ''} ${task.description || ''} ${task.file || ''}`)
|
|
215
|
+
const devTask = looksLikeDevTask(task)
|
|
216
|
+
const markerCandidates = candidates.filter((candidate) => candidate.hasProjectMarker)
|
|
217
|
+
|
|
218
|
+
let best: { dir: string; score: number } | null = null
|
|
219
|
+
for (const candidate of candidates) {
|
|
220
|
+
const nameNorm = normalizeForMatch(candidate.name)
|
|
221
|
+
if (!nameNorm) continue
|
|
222
|
+
let score = 0
|
|
223
|
+
if (taskText.includes(nameNorm)) score += 8
|
|
224
|
+
for (const token of nameNorm.split(' ')) {
|
|
225
|
+
if (token.length < 3) continue
|
|
226
|
+
if (taskText.includes(token)) score += 1
|
|
227
|
+
}
|
|
228
|
+
if (candidate.hasProjectMarker) score += devTask ? 3 : 1
|
|
229
|
+
if (candidate.hasSourceMarker) score += 1
|
|
230
|
+
if (!best || score > best.score) best = { dir: candidate.dir, score }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (best && best.score >= 4) return best.dir
|
|
234
|
+
if (devTask && markerCandidates.length === 1) return markerCandidates[0].dir
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveTaskExecutionCwd(task: ScheduleTaskMeta, sessions: Record<string, SessionLike>): string {
|
|
239
|
+
const workspaceRoot = path.resolve(WORKSPACE_DIR)
|
|
240
|
+
|
|
241
|
+
const explicitCwd = normalizeDirCandidate(task.cwd, workspaceRoot)
|
|
242
|
+
if (explicitCwd) return explicitCwd
|
|
243
|
+
|
|
244
|
+
const projectId = typeof task.projectId === 'string' ? task.projectId.trim() : ''
|
|
245
|
+
if (projectId) {
|
|
246
|
+
const projectDir = path.join(WORKSPACE_PROJECTS_DIR, projectId)
|
|
247
|
+
if (isExistingDirectory(projectDir)) return projectDir
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const fileRef = typeof task.file === 'string' ? task.file.trim() : ''
|
|
251
|
+
if (fileRef) {
|
|
252
|
+
const filePath = path.isAbsolute(fileRef) ? fileRef : path.resolve(workspaceRoot, fileRef)
|
|
253
|
+
const fileDir = isExistingDirectory(filePath) ? filePath : path.dirname(filePath)
|
|
254
|
+
if (isExistingDirectory(fileDir) && isWithinDirectory(workspaceRoot, fileDir)) return fileDir
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const inferredCwd = inferWorkspaceProjectCwd(task)
|
|
258
|
+
if (inferredCwd) return inferredCwd
|
|
259
|
+
|
|
260
|
+
const sourceSessionId = typeof task.createdInSessionId === 'string' ? task.createdInSessionId.trim() : ''
|
|
261
|
+
const sourceSessionCwd = sourceSessionId
|
|
262
|
+
? normalizeDirCandidate(sessions[sourceSessionId]?.cwd, workspaceRoot)
|
|
263
|
+
: null
|
|
264
|
+
if (sourceSessionCwd && path.resolve(sourceSessionCwd) !== workspaceRoot) return sourceSessionCwd
|
|
265
|
+
|
|
266
|
+
const runSessionId = typeof task.sessionId === 'string' ? task.sessionId.trim() : ''
|
|
267
|
+
const runSessionCwd = runSessionId
|
|
268
|
+
? normalizeDirCandidate(sessions[runSessionId]?.cwd, workspaceRoot)
|
|
269
|
+
: null
|
|
270
|
+
if (runSessionCwd && path.resolve(runSessionCwd) !== workspaceRoot) return runSessionCwd
|
|
271
|
+
|
|
272
|
+
const sandboxDir = path.join(workspaceRoot, 'tasks', task.id)
|
|
273
|
+
fs.mkdirSync(sandboxDir, { recursive: true })
|
|
274
|
+
return sandboxDir
|
|
275
|
+
}
|
|
276
|
+
|
|
100
277
|
function queueContains(queue: string[], id: string): boolean {
|
|
101
278
|
return queue.includes(id)
|
|
102
279
|
}
|
|
@@ -316,12 +493,34 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
316
493
|
// Task result extraction now uses Zod-validated structured data
|
|
317
494
|
// from ./task-result.ts (extractTaskResult, formatResultBody)
|
|
318
495
|
|
|
496
|
+
/** Check if a task result looks incomplete (agent stopped mid-objective). */
|
|
497
|
+
function looksIncomplete(text: string): boolean {
|
|
498
|
+
if (!text) return false
|
|
499
|
+
const trimmed = text.trim()
|
|
500
|
+
// Ends with ellipsis or continuation signal
|
|
501
|
+
if (trimmed.endsWith('...') || trimmed.endsWith('…')) return true
|
|
502
|
+
// Ends with a step/phase header (agent was listing next steps)
|
|
503
|
+
if (/(?:^|\n)#{1,3}\s+(?:Step|Phase|Next)\s+\d/i.test(trimmed.slice(-200))) return true
|
|
504
|
+
// Contains forward-looking language at the end
|
|
505
|
+
const lastChunk = trimmed.slice(-300).toLowerCase()
|
|
506
|
+
if (/\b(?:next i(?:'ll| will)|now i(?:'ll| will)|let me (?:now|next)|moving on to|proceeding to)\b/.test(lastChunk)) return true
|
|
507
|
+
return false
|
|
508
|
+
}
|
|
509
|
+
|
|
319
510
|
async function executeTaskRun(
|
|
320
511
|
task: BoardTask,
|
|
321
512
|
agent: Agent,
|
|
322
513
|
sessionId: string,
|
|
323
514
|
): Promise<string> {
|
|
324
|
-
const
|
|
515
|
+
const basePrompt = task.description || task.title
|
|
516
|
+
const prompt = [
|
|
517
|
+
basePrompt,
|
|
518
|
+
'',
|
|
519
|
+
'Completion requirements:',
|
|
520
|
+
'- Execute the task before replying; do not reply with only a plan.',
|
|
521
|
+
'- Include concrete evidence in your final summary: changed file paths, commands run, and verification results.',
|
|
522
|
+
'- If blocked, state the blocker explicitly and what input or permission is missing.',
|
|
523
|
+
].join('\n')
|
|
325
524
|
if (agent?.isOrchestrator) {
|
|
326
525
|
return executeOrchestrator(agent, prompt, sessionId, task.id)
|
|
327
526
|
}
|
|
@@ -331,11 +530,24 @@ async function executeTaskRun(
|
|
|
331
530
|
message: prompt,
|
|
332
531
|
internal: false,
|
|
333
532
|
source: 'task',
|
|
533
|
+
runId: task.id,
|
|
334
534
|
})
|
|
335
|
-
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
535
|
+
let text = typeof run.text === 'string' ? run.text.trim() : ''
|
|
536
|
+
if (run.error) return text ? text : `Error: ${run.error}`
|
|
537
|
+
|
|
538
|
+
// Auto-continue if the result looks incomplete
|
|
539
|
+
if (text && looksIncomplete(text)) {
|
|
540
|
+
const followUp = await executeSessionChatTurn({
|
|
541
|
+
sessionId,
|
|
542
|
+
message: 'Continue and complete the remaining steps. Provide a final summary when done.',
|
|
543
|
+
internal: false,
|
|
544
|
+
source: 'task',
|
|
545
|
+
})
|
|
546
|
+
const contText = typeof followUp.text === 'string' ? followUp.text.trim() : ''
|
|
547
|
+
if (contText) text = contText
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return text
|
|
339
551
|
}
|
|
340
552
|
|
|
341
553
|
function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
@@ -378,8 +590,8 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
|
378
590
|
const now = Date.now()
|
|
379
591
|
let changed = false
|
|
380
592
|
|
|
381
|
-
const buildMsg = ():
|
|
382
|
-
const msg:
|
|
593
|
+
const buildMsg = (): SessionMessageLike => {
|
|
594
|
+
const msg: SessionMessageLike = { role: 'assistant', text: body, time: now, kind: 'system' }
|
|
383
595
|
if (firstImage) msg.imageUrl = firstImage.url
|
|
384
596
|
return msg
|
|
385
597
|
}
|
|
@@ -702,6 +914,7 @@ export function enqueueTask(taskId: string) {
|
|
|
702
914
|
export function validateCompletedTasksQueue() {
|
|
703
915
|
const tasks = loadTasks()
|
|
704
916
|
const sessions = loadSessions()
|
|
917
|
+
const settings = loadSettings()
|
|
705
918
|
const now = Date.now()
|
|
706
919
|
let checked = 0
|
|
707
920
|
let demoted = 0
|
|
@@ -718,7 +931,7 @@ export function validateCompletedTasksQueue() {
|
|
|
718
931
|
tasksDirty = true
|
|
719
932
|
}
|
|
720
933
|
|
|
721
|
-
const validation = validateTaskCompletion(task, { report })
|
|
934
|
+
const validation = validateTaskCompletion(task, { report, settings })
|
|
722
935
|
const prevValidation = task.validation || null
|
|
723
936
|
const validationChanged = !prevValidation
|
|
724
937
|
|| prevValidation.ok !== validation.ok
|
|
@@ -930,7 +1143,9 @@ export async function processNext() {
|
|
|
930
1143
|
task.validation = null
|
|
931
1144
|
task.updatedAt = Date.now()
|
|
932
1145
|
|
|
933
|
-
const
|
|
1146
|
+
const sessionsForCwd = loadSessions() as Record<string, SessionLike>
|
|
1147
|
+
const taskCwd = resolveTaskExecutionCwd(task as ScheduleTaskMeta, sessionsForCwd)
|
|
1148
|
+
task.cwd = taskCwd
|
|
934
1149
|
let sessionId = ''
|
|
935
1150
|
const scheduleTask = task as ScheduleTaskMeta
|
|
936
1151
|
const isScheduleTask = scheduleTask.sourceType === 'schedule'
|
|
@@ -1029,6 +1244,7 @@ export async function processNext() {
|
|
|
1029
1244
|
try {
|
|
1030
1245
|
const result = await executeTaskRun(task, agent, sessionId)
|
|
1031
1246
|
const t2 = loadTasks()
|
|
1247
|
+
const settings = loadSettings()
|
|
1032
1248
|
if (t2[taskId]) {
|
|
1033
1249
|
applyTaskPolicyDefaults(t2[taskId])
|
|
1034
1250
|
// Structured extraction: Zod-validated result with typed artifacts
|
|
@@ -1045,7 +1261,7 @@ export async function processNext() {
|
|
|
1045
1261
|
t2[taskId].updatedAt = Date.now()
|
|
1046
1262
|
const report = ensureTaskCompletionReport(t2[taskId])
|
|
1047
1263
|
if (report?.relativePath) t2[taskId].completionReportPath = report.relativePath
|
|
1048
|
-
const validation = validateTaskCompletion(t2[taskId], { report })
|
|
1264
|
+
const validation = validateTaskCompletion(t2[taskId], { report, settings })
|
|
1049
1265
|
t2[taskId].validation = validation
|
|
1050
1266
|
|
|
1051
1267
|
const now = Date.now()
|
|
@@ -1260,6 +1476,29 @@ export function recoverStalledRunningTasks(): { recovered: number; deadLettered:
|
|
|
1260
1476
|
|
|
1261
1477
|
for (const task of Object.values(tasks) as BoardTask[]) {
|
|
1262
1478
|
if (task.status !== 'running') continue
|
|
1479
|
+
if (!task.startedAt) {
|
|
1480
|
+
const recoveredAt = Date.now()
|
|
1481
|
+
task.status = 'queued'
|
|
1482
|
+
task.queuedAt = task.queuedAt || recoveredAt
|
|
1483
|
+
task.retryScheduledAt = null
|
|
1484
|
+
task.updatedAt = recoveredAt
|
|
1485
|
+
task.error = 'Recovered inconsistent running state (missing startedAt); requeued.'
|
|
1486
|
+
if (!task.comments) task.comments = []
|
|
1487
|
+
task.comments.push({
|
|
1488
|
+
id: genId(),
|
|
1489
|
+
author: 'System',
|
|
1490
|
+
text: 'Recovered inconsistent running state (missing startedAt). Task requeued.',
|
|
1491
|
+
createdAt: recoveredAt,
|
|
1492
|
+
})
|
|
1493
|
+
pushQueueUnique(queue, task.id)
|
|
1494
|
+
recovered++
|
|
1495
|
+
changed = true
|
|
1496
|
+
pushMainLoopEventToMainSessions({
|
|
1497
|
+
type: 'task_stall_recovered',
|
|
1498
|
+
text: `Recovered inconsistent running task "${task.title}" (${task.id}) and requeued it.`,
|
|
1499
|
+
})
|
|
1500
|
+
continue
|
|
1501
|
+
}
|
|
1263
1502
|
const since = Math.max(task.updatedAt || 0, task.startedAt || 0)
|
|
1264
1503
|
if (!since || (now - since) < staleMs) continue
|
|
1265
1504
|
|
|
@@ -1286,6 +1525,9 @@ export function recoverStalledRunningTasks(): { recovered: number; deadLettered:
|
|
|
1286
1525
|
if (changed) {
|
|
1287
1526
|
saveTasks(tasks)
|
|
1288
1527
|
saveQueue(queue)
|
|
1528
|
+
if (recovered > 0) {
|
|
1529
|
+
setTimeout(() => processNext(), 250)
|
|
1530
|
+
}
|
|
1289
1531
|
}
|
|
1290
1532
|
|
|
1291
1533
|
return { recovered, deadLettered }
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_AGENT_LOOP_RECURSION_LIMIT,
|
|
4
4
|
DEFAULT_CLAUDE_CODE_TIMEOUT_SEC,
|
|
5
5
|
DEFAULT_CLI_PROCESS_TIMEOUT_SEC,
|
|
6
|
+
DEFAULT_DELEGATION_MAX_DEPTH,
|
|
6
7
|
DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS,
|
|
7
8
|
DEFAULT_LOOP_MODE,
|
|
8
9
|
DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
|
|
@@ -17,6 +18,7 @@ export interface RuntimeSettings {
|
|
|
17
18
|
agentLoopRecursionLimit: number
|
|
18
19
|
orchestratorLoopRecursionLimit: number
|
|
19
20
|
legacyOrchestratorMaxTurns: number
|
|
21
|
+
delegationMaxDepth: number
|
|
20
22
|
ongoingLoopMaxIterations: number
|
|
21
23
|
ongoingLoopMaxRuntimeMs: number | null
|
|
22
24
|
shellCommandTimeoutMs: number
|
|
@@ -61,6 +63,12 @@ export function loadRuntimeSettings(): RuntimeSettings {
|
|
|
61
63
|
1,
|
|
62
64
|
300,
|
|
63
65
|
)
|
|
66
|
+
const delegationMaxDepth = parseIntSetting(
|
|
67
|
+
settings.delegationMaxDepth,
|
|
68
|
+
DEFAULT_DELEGATION_MAX_DEPTH,
|
|
69
|
+
1,
|
|
70
|
+
12,
|
|
71
|
+
)
|
|
64
72
|
const ongoingLoopMaxIterations = parseIntSetting(
|
|
65
73
|
settings.ongoingLoopMaxIterations,
|
|
66
74
|
DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
|
|
@@ -98,6 +106,7 @@ export function loadRuntimeSettings(): RuntimeSettings {
|
|
|
98
106
|
agentLoopRecursionLimit,
|
|
99
107
|
orchestratorLoopRecursionLimit,
|
|
100
108
|
legacyOrchestratorMaxTurns,
|
|
109
|
+
delegationMaxDepth,
|
|
101
110
|
ongoingLoopMaxIterations,
|
|
102
111
|
ongoingLoopMaxRuntimeMs: ongoingLoopMaxRuntimeMinutes > 0 ? ongoingLoopMaxRuntimeMinutes * 60_000 : null,
|
|
103
112
|
shellCommandTimeoutMs: shellCommandTimeoutSec * 1000,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { isMainMissionSession } from './session-run-manager'
|
|
4
|
+
|
|
5
|
+
describe('isMainMissionSession', () => {
|
|
6
|
+
it('accepts explicit main sessions', () => {
|
|
7
|
+
assert.equal(isMainMissionSession({ id: 'main-user', name: '__main__' }), true)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('rejects human agent-thread sessions', () => {
|
|
11
|
+
assert.equal(
|
|
12
|
+
isMainMissionSession({ id: 'agent-thread-agent_coder-123', name: 'agent-thread:agent_coder', sessionType: 'human' }),
|
|
13
|
+
false,
|
|
14
|
+
)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('accepts orchestrated sessions', () => {
|
|
18
|
+
assert.equal(
|
|
19
|
+
isMainMissionSession({ id: 'agent-thread-worker-1', name: 'agent-thread:worker', sessionType: 'orchestrated' }),
|
|
20
|
+
true,
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -203,7 +203,7 @@ function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupR
|
|
|
203
203
|
try {
|
|
204
204
|
const sessions = loadSessions()
|
|
205
205
|
const session = sessions[sessionId]
|
|
206
|
-
if (!session || session
|
|
206
|
+
if (!session || !isMainMissionSession(session)) return
|
|
207
207
|
enqueueSessionRun({
|
|
208
208
|
sessionId,
|
|
209
209
|
message: followup.message,
|
|
@@ -218,6 +218,16 @@ function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupR
|
|
|
218
218
|
}, delayMs)
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
export function isMainMissionSession(session: Record<string, unknown>): boolean {
|
|
222
|
+
const id = typeof session.id === 'string' ? session.id.trim() : ''
|
|
223
|
+
const name = typeof session.name === 'string' ? session.name.trim() : ''
|
|
224
|
+
const sessionType = typeof session.sessionType === 'string' ? session.sessionType : ''
|
|
225
|
+
if (id.startsWith('main-') || name === '__main__') return true
|
|
226
|
+
// Only orchestrated thread sessions should receive autonomous main-loop followups.
|
|
227
|
+
if (sessionType === 'orchestrated') return true
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
|
|
221
231
|
async function drainExecution(executionKey: string): Promise<void> {
|
|
222
232
|
if (state.runningByExecution.has(executionKey)) return
|
|
223
233
|
const q = queueForExecution(executionKey)
|
|
@@ -3,65 +3,100 @@ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
|
3
3
|
import { loadSessions, saveSessions } from '../storage'
|
|
4
4
|
import { notify } from '../ws-hub'
|
|
5
5
|
import type { ToolBuildContext } from './context'
|
|
6
|
+
import type { Plugin, PluginHooks } from '@/types'
|
|
7
|
+
import { getPluginManager } from '../plugins'
|
|
8
|
+
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Core Canvas Execution Logic
|
|
12
|
+
*/
|
|
13
|
+
async function executeCanvasAction(args: Record<string, unknown>, context: { sessionId?: string }) {
|
|
14
|
+
const normalized = normalizeToolInputArgs(args)
|
|
15
|
+
const action = normalized.action as string
|
|
16
|
+
const content = normalized.content as string | undefined
|
|
17
|
+
try {
|
|
18
|
+
const sessionId = context.sessionId
|
|
19
|
+
if (!sessionId) return 'Error: no active session for canvas.'
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const sessionId = ctx?.sessionId
|
|
16
|
-
if (!sessionId) return 'Error: no active session for canvas.'
|
|
21
|
+
const sessions = loadSessions()
|
|
22
|
+
const session = sessions[sessionId]
|
|
23
|
+
if (!session) return 'Error: session not found.'
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
if (action === 'present') {
|
|
26
|
+
if (!content) return 'Error: content is required for present action.'
|
|
27
|
+
;(session as Record<string, unknown>).canvasContent = content
|
|
28
|
+
session.lastActiveAt = Date.now()
|
|
29
|
+
sessions[sessionId] = session
|
|
30
|
+
saveSessions(sessions)
|
|
31
|
+
notify(`canvas:${sessionId}`)
|
|
32
|
+
return JSON.stringify({ ok: true, action: 'present', contentLength: content.length })
|
|
33
|
+
}
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
35
|
+
if (action === 'hide') {
|
|
36
|
+
;(session as Record<string, unknown>).canvasContent = null
|
|
37
|
+
session.lastActiveAt = Date.now()
|
|
38
|
+
sessions[sessionId] = session
|
|
39
|
+
saveSessions(sessions)
|
|
40
|
+
notify(`canvas:${sessionId}`)
|
|
41
|
+
return JSON.stringify({ ok: true, action: 'hide' })
|
|
42
|
+
}
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
if (action === 'snapshot') {
|
|
45
|
+
const current = (session as Record<string, unknown>).canvasContent
|
|
46
|
+
return JSON.stringify({
|
|
47
|
+
ok: true,
|
|
48
|
+
action: 'snapshot',
|
|
49
|
+
hasContent: !!current,
|
|
50
|
+
contentLength: typeof current === 'string' ? current.length : 0,
|
|
51
|
+
preview: typeof current === 'string' ? current.slice(0, 500) : null,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
40
54
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
hasContent: !!current,
|
|
47
|
-
contentLength: typeof current === 'string' ? current.length : 0,
|
|
48
|
-
preview: typeof current === 'string' ? current.slice(0, 500) : null,
|
|
49
|
-
})
|
|
50
|
-
}
|
|
55
|
+
return `Unknown canvas action "${action}".`
|
|
56
|
+
} catch (err: unknown) {
|
|
57
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
58
|
+
}
|
|
59
|
+
}
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Register as a Built-in Plugin
|
|
63
|
+
*/
|
|
64
|
+
const CanvasPlugin: Plugin = {
|
|
65
|
+
name: 'Core Canvas',
|
|
66
|
+
description: 'Present live HTML/CSS/JS content to the user in an interactive canvas panel.',
|
|
67
|
+
hooks: {} as PluginHooks,
|
|
68
|
+
tools: [
|
|
69
|
+
{
|
|
70
|
+
name: 'canvas',
|
|
71
|
+
description: 'Interact with the live canvas panel.',
|
|
72
|
+
parameters: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
action: { type: 'string', enum: ['present', 'hide', 'snapshot'] },
|
|
76
|
+
content: { type: 'string' }
|
|
77
|
+
},
|
|
78
|
+
required: ['action']
|
|
56
79
|
},
|
|
80
|
+
execute: async (args, context) => executeCanvasAction(args, { sessionId: context.session.id })
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getPluginManager().registerBuiltin('canvas', CanvasPlugin)
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Legacy Bridge
|
|
89
|
+
*/
|
|
90
|
+
export function buildCanvasTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
91
|
+
if (!bctx.hasTool('canvas')) return []
|
|
92
|
+
return [
|
|
93
|
+
tool(
|
|
94
|
+
async (args) => executeCanvasAction(args, { sessionId: bctx.ctx?.sessionId || undefined }),
|
|
57
95
|
{
|
|
58
96
|
name: 'canvas',
|
|
59
|
-
description:
|
|
60
|
-
schema: z.object({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}),
|
|
64
|
-
},
|
|
65
|
-
),
|
|
97
|
+
description: CanvasPlugin.tools![0].description,
|
|
98
|
+
schema: z.object({}).passthrough()
|
|
99
|
+
}
|
|
100
|
+
)
|
|
66
101
|
]
|
|
67
102
|
}
|