@swarmclawai/swarmclaw 0.3.0 → 0.4.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 +20 -11
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +2 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +3 -0
- package/src/app/api/agents/[id]/thread/route.ts +2 -1
- package/src/app/api/agents/route.ts +5 -1
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/connectors/[id]/route.ts +4 -0
- package/src/app/api/connectors/route.ts +6 -1
- package/src/app/api/credentials/route.ts +3 -1
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/mcp-servers/route.ts +3 -1
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/providers/[id]/route.ts +3 -0
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +5 -1
- package/src/app/api/schedules/[id]/route.ts +3 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/route.ts +3 -1
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/route.ts +9 -2
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/route.ts +3 -1
- package/src/app/api/tasks/[id]/approve/route.ts +73 -0
- package/src/app/api/tasks/[id]/route.ts +3 -0
- package/src/app/api/tasks/route.ts +3 -0
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.ts +3 -1
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +8 -2
- package/src/cli/index.js +1 -9
- package/src/cli/index.ts +51 -1
- package/src/cli/spec.js +0 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-sheet.tsx +63 -80
- package/src/components/chat/chat-area.tsx +44 -30
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +41 -3
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/connectors/connector-list.tsx +3 -8
- package/src/components/connectors/connector-sheet.tsx +24 -29
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +92 -71
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +6 -3
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +8 -9
- package/src/components/shared/connector-platform-icon.tsx +22 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +7 -39
- package/src/components/shared/settings/section-orchestrator.tsx +8 -9
- package/src/components/skills/skill-list.tsx +260 -34
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +3 -5
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/providers/anthropic.ts +1 -1
- package/src/lib/providers/index.ts +2 -0
- package/src/lib/providers/ollama.ts +1 -1
- package/src/lib/providers/openai.ts +33 -12
- package/src/lib/server/chat-execution.ts +19 -4
- package/src/lib/server/connectors/manager.ts +9 -3
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +61 -2
- package/src/lib/server/orchestrator-lg.ts +394 -13
- package/src/lib/server/orchestrator.ts +25 -5
- package/src/lib/server/queue.ts +17 -3
- package/src/lib/server/session-run-manager.ts +6 -1
- package/src/lib/server/session-tools/delegate.ts +2 -2
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/sandbox.ts +164 -0
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +24 -7
- package/src/lib/server/stream-agent-chat.ts +77 -22
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +42 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/ws-client.ts +124 -0
- package/src/stores/use-chat-store.ts +33 -13
- package/src/types/index.ts +8 -1
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- package/src/components/shared/ai-gen-block.tsx +0 -77
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool } from '@langchain/core/tools'
|
|
3
|
-
import {
|
|
3
|
+
import { StateGraph, MessagesAnnotation, START, END } from '@langchain/langgraph'
|
|
4
|
+
import { ToolNode } from '@langchain/langgraph/prebuilt'
|
|
5
|
+
import { AIMessage } from '@langchain/core/messages'
|
|
4
6
|
import { loadSessions, saveSessions, loadAgents, loadCredentials, loadSettings, loadSecrets, loadTasks, saveTasks, decryptKey, loadSkills } from './storage'
|
|
7
|
+
import { WORKSPACE_DIR } from './data-dir'
|
|
5
8
|
import { loadRuntimeSettings, getOrchestratorLoopRecursionLimit } from './runtime-settings'
|
|
6
9
|
import { getMemoryDb } from './memory-db'
|
|
7
10
|
import { buildChatModel } from './build-llm'
|
|
11
|
+
import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
12
|
+
import { notify } from './ws-hub'
|
|
13
|
+
import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
8
14
|
import crypto from 'crypto'
|
|
9
|
-
import type { Agent, TaskComment } from '@/types'
|
|
15
|
+
import type { Agent, TaskComment, MessageToolEvent } from '@/types'
|
|
10
16
|
|
|
11
17
|
const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
|
|
12
18
|
|
|
@@ -73,11 +79,11 @@ function getSecretsForOrchestrator(orchestratorId: string): { name: string; serv
|
|
|
73
79
|
return result
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
function saveMessage(sessionId: string, role: 'user' | 'assistant', text: string) {
|
|
82
|
+
function saveMessage(sessionId: string, role: 'user' | 'assistant', text: string, toolEvents?: MessageToolEvent[]) {
|
|
77
83
|
const sessions = loadSessions()
|
|
78
84
|
const session = sessions[sessionId]
|
|
79
85
|
if (!session) return
|
|
80
|
-
session.messages.push({ role, text, time: Date.now() })
|
|
86
|
+
session.messages.push({ role, text, time: Date.now(), ...(toolEvents?.length ? { toolEvents } : {}) })
|
|
81
87
|
session.lastActiveAt = Date.now()
|
|
82
88
|
saveSessions(sessions)
|
|
83
89
|
}
|
|
@@ -95,7 +101,7 @@ async function executeSubTaskViaCli(agent: Agent, task: string, parentSessionId:
|
|
|
95
101
|
sessions[childId] = {
|
|
96
102
|
id: childId,
|
|
97
103
|
name: `[Agent] ${agent.name}: ${task.slice(0, 40)}`,
|
|
98
|
-
cwd: parentSession?.cwd ||
|
|
104
|
+
cwd: parentSession?.cwd || WORKSPACE_DIR,
|
|
99
105
|
user: 'system',
|
|
100
106
|
provider: agent.provider,
|
|
101
107
|
model: agent.model,
|
|
@@ -136,6 +142,7 @@ export async function executeLangGraphOrchestrator(
|
|
|
136
142
|
orchestrator: Agent,
|
|
137
143
|
task: string,
|
|
138
144
|
sessionId: string,
|
|
145
|
+
taskId?: string,
|
|
139
146
|
): Promise<string> {
|
|
140
147
|
const allAgents = loadAgents()
|
|
141
148
|
|
|
@@ -165,9 +172,12 @@ export async function executeLangGraphOrchestrator(
|
|
|
165
172
|
return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
|
|
166
173
|
}
|
|
167
174
|
console.log(`[orchestrator-lg] Delegating to ${agent.name}: ${agentTask.slice(0, 80)}`)
|
|
168
|
-
saveMessage(sessionId, 'assistant', `[Delegating to ${agent.name}]: ${agentTask}`)
|
|
169
175
|
const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
|
|
170
|
-
saveMessage(sessionId, '
|
|
176
|
+
saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
|
|
177
|
+
name: 'delegate_to_agent',
|
|
178
|
+
input: JSON.stringify({ agentName: agent.name, agentId: agent.id, task: agentTask }),
|
|
179
|
+
output: result.slice(0, 2000),
|
|
180
|
+
}])
|
|
171
181
|
return result
|
|
172
182
|
},
|
|
173
183
|
{
|
|
@@ -369,12 +379,102 @@ export async function executeLangGraphOrchestrator(
|
|
|
369
379
|
taskContext,
|
|
370
380
|
].join('\n')
|
|
371
381
|
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
382
|
+
const checkpointSaver = getCheckpointSaver()
|
|
383
|
+
const isStrictMode = settings.capabilityPolicyMode === 'strict'
|
|
384
|
+
const allTools = [delegateTool, storeMemoryTool, searchMemoryTool, getSecretTool, commentOnTaskTool, createTaskTool, markCompleteTool]
|
|
385
|
+
const llmWithTools = llm.bindTools(allTools)
|
|
386
|
+
const toolNode = new ToolNode(allTools)
|
|
387
|
+
|
|
388
|
+
// Track fallback attempts for delegate_to_agent failures
|
|
389
|
+
let fallbackAttempts = 0
|
|
390
|
+
const MAX_FALLBACK_ATTEMPTS = 2
|
|
391
|
+
|
|
392
|
+
// Agent node: calls LLM with tools
|
|
393
|
+
async function agentNode(state: typeof MessagesAnnotation.State) {
|
|
394
|
+
const response = await llmWithTools.invoke([
|
|
395
|
+
{ role: 'system' as const, content: systemMessage },
|
|
396
|
+
...state.messages,
|
|
397
|
+
])
|
|
398
|
+
return { messages: [response] }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Router node: inspects tool results, decides next step
|
|
402
|
+
function routerNode(state: typeof MessagesAnnotation.State) {
|
|
403
|
+
const messages = state.messages
|
|
404
|
+
const lastMsg = messages[messages.length - 1]
|
|
405
|
+
|
|
406
|
+
// Check if the last tool message contains an error from delegate_to_agent
|
|
407
|
+
if (lastMsg && typeof (lastMsg as any).content === 'string') {
|
|
408
|
+
const content = (lastMsg as any).content as string
|
|
409
|
+
const isError = content.startsWith('Error:') || content.startsWith('Agent "') && content.includes('not found')
|
|
410
|
+
|
|
411
|
+
if (isError && fallbackAttempts < MAX_FALLBACK_ATTEMPTS) {
|
|
412
|
+
fallbackAttempts++
|
|
413
|
+
// Look for a delegate tool call in recent messages and try to find alternative agent
|
|
414
|
+
const failedToolCall = [...messages].reverse().find(
|
|
415
|
+
(m) => (m as any).tool_calls?.some((tc: any) => tc.name === 'delegate_to_agent')
|
|
416
|
+
)
|
|
417
|
+
if (failedToolCall) {
|
|
418
|
+
const tc = (failedToolCall as any).tool_calls?.find((tc: any) => tc.name === 'delegate_to_agent')
|
|
419
|
+
const failedAgentName = tc?.args?.agentName || 'unknown'
|
|
420
|
+
const fallbackHint = `The agent "${failedAgentName}" failed. Try delegating to a different agent with matching capabilities, or re-plan your approach. Fallback attempt ${fallbackAttempts}/${MAX_FALLBACK_ATTEMPTS}.`
|
|
421
|
+
return { messages: [new AIMessage({ content: fallbackHint })] }
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// No fallback needed — pass through
|
|
427
|
+
return { messages: [] }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Routing function: after agent node, check if there are tool calls
|
|
431
|
+
function shouldContinue(state: typeof MessagesAnnotation.State) {
|
|
432
|
+
const lastMsg = state.messages[state.messages.length - 1]
|
|
433
|
+
const toolCalls = (lastMsg as any)?.tool_calls
|
|
434
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
435
|
+
return 'tools'
|
|
436
|
+
}
|
|
437
|
+
return END
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// After router, decide whether to go back to agent or end
|
|
441
|
+
function afterRouter(state: typeof MessagesAnnotation.State) {
|
|
442
|
+
const messages = state.messages
|
|
443
|
+
// If router added a fallback hint, route back to agent
|
|
444
|
+
const lastMsg = messages[messages.length - 1]
|
|
445
|
+
if (lastMsg && typeof (lastMsg as any).content === 'string' && (lastMsg as any).content.includes('Fallback attempt')) {
|
|
446
|
+
return 'agent'
|
|
447
|
+
}
|
|
448
|
+
return 'agent'
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Build the StateGraph
|
|
452
|
+
const graph = new StateGraph(MessagesAnnotation)
|
|
453
|
+
.addNode('agent', agentNode)
|
|
454
|
+
.addNode('tools', toolNode)
|
|
455
|
+
.addNode('router', routerNode)
|
|
456
|
+
.addEdge(START, 'agent')
|
|
457
|
+
.addConditionalEdges('agent', shouldContinue, { tools: 'tools', [END]: END })
|
|
458
|
+
.addEdge('tools', 'router')
|
|
459
|
+
.addConditionalEdges('router', afterRouter, { agent: 'agent' })
|
|
460
|
+
|
|
461
|
+
const compiledGraph = graph.compile({
|
|
462
|
+
checkpointer: checkpointSaver,
|
|
463
|
+
...(isStrictMode ? { interruptBefore: ['tools'] } : {}),
|
|
376
464
|
})
|
|
377
465
|
|
|
466
|
+
// Export graph structure for introspection
|
|
467
|
+
;(compiledGraph as any).__graphStructure = {
|
|
468
|
+
nodes: ['agent', 'tools', 'router'],
|
|
469
|
+
edges: [
|
|
470
|
+
{ from: START, to: 'agent' },
|
|
471
|
+
{ from: 'agent', to: 'tools', condition: 'has_tool_calls' },
|
|
472
|
+
{ from: 'agent', to: END, condition: 'no_tool_calls' },
|
|
473
|
+
{ from: 'tools', to: 'router' },
|
|
474
|
+
{ from: 'router', to: 'agent', condition: 'fallback_or_continue' },
|
|
475
|
+
],
|
|
476
|
+
}
|
|
477
|
+
|
|
378
478
|
// Save initial user message
|
|
379
479
|
saveMessage(sessionId, 'user', task)
|
|
380
480
|
|
|
@@ -391,9 +491,15 @@ export async function executeLangGraphOrchestrator(
|
|
|
391
491
|
: null
|
|
392
492
|
|
|
393
493
|
try {
|
|
394
|
-
const
|
|
494
|
+
const threadId = taskId || sessionId
|
|
495
|
+
const streamConfig = {
|
|
496
|
+
recursionLimit,
|
|
497
|
+
signal: abortController.signal,
|
|
498
|
+
configurable: { thread_id: threadId },
|
|
499
|
+
}
|
|
500
|
+
const stream = await compiledGraph.stream(
|
|
395
501
|
{ messages: [{ role: 'user' as const, content: task }] },
|
|
396
|
-
|
|
502
|
+
streamConfig,
|
|
397
503
|
)
|
|
398
504
|
|
|
399
505
|
for await (const chunk of stream) {
|
|
@@ -414,6 +520,42 @@ export async function executeLangGraphOrchestrator(
|
|
|
414
520
|
}
|
|
415
521
|
}
|
|
416
522
|
}
|
|
523
|
+
|
|
524
|
+
// Check for interrupt (paused before tool execution in strict mode)
|
|
525
|
+
if (isStrictMode && taskId) {
|
|
526
|
+
const state = await compiledGraph.getState({ configurable: { thread_id: threadId } })
|
|
527
|
+
const nextNodes = state?.next || []
|
|
528
|
+
if (nextNodes.includes('tools')) {
|
|
529
|
+
// Graph is paused before tool execution — extract pending tool call
|
|
530
|
+
const messages = state.values?.messages || []
|
|
531
|
+
const lastMsg = messages[messages.length - 1]
|
|
532
|
+
const toolCalls = lastMsg?.tool_calls || lastMsg?.additional_kwargs?.tool_calls || []
|
|
533
|
+
const pendingCall = toolCalls[0]
|
|
534
|
+
if (pendingCall) {
|
|
535
|
+
const tasks = loadTasks()
|
|
536
|
+
const t = tasks[taskId]
|
|
537
|
+
if (t) {
|
|
538
|
+
t.pendingApproval = {
|
|
539
|
+
toolName: pendingCall.name || pendingCall.function?.name || 'unknown',
|
|
540
|
+
args: pendingCall.args || (pendingCall.function?.arguments ? JSON.parse(pendingCall.function.arguments) : {}),
|
|
541
|
+
threadId,
|
|
542
|
+
}
|
|
543
|
+
t.updatedAt = Date.now()
|
|
544
|
+
saveTasks(tasks)
|
|
545
|
+
notify('tasks')
|
|
546
|
+
const approvalMsg = `[Awaiting approval] Tool: ${t.pendingApproval.toolName}\nArgs: ${JSON.stringify(t.pendingApproval.args).slice(0, 300)}\n\nApprove or reject in the task board.`
|
|
547
|
+
saveMessage(sessionId, 'assistant', approvalMsg)
|
|
548
|
+
notify(`messages:${sessionId}`)
|
|
549
|
+
pushMainLoopEventToMainSessions({
|
|
550
|
+
type: 'pending_approval',
|
|
551
|
+
text: `Task "${t.title}" needs approval: ${t.pendingApproval.toolName}(${JSON.stringify(t.pendingApproval.args).slice(0, 100)})`,
|
|
552
|
+
})
|
|
553
|
+
console.log(`[orchestrator-lg] Interrupt: waiting for approval of tool "${t.pendingApproval.toolName}" on task ${taskId}`)
|
|
554
|
+
return approvalMsg
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
417
559
|
} catch (err: any) {
|
|
418
560
|
const errMsg = timedOut
|
|
419
561
|
? 'Ongoing loop stopped after reaching the configured runtime limit.'
|
|
@@ -429,3 +571,242 @@ export async function executeLangGraphOrchestrator(
|
|
|
429
571
|
const completeMatch = finalResult.match(/ORCHESTRATION_COMPLETE:\s*([\s\S]+)/)
|
|
430
572
|
return completeMatch ? completeMatch[1].trim() : finalResult
|
|
431
573
|
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Resume a paused orchestrator run after human approval.
|
|
577
|
+
* Re-creates the same agent graph and streams from the saved checkpoint.
|
|
578
|
+
*/
|
|
579
|
+
export async function resumeLangGraphOrchestrator(
|
|
580
|
+
orchestrator: Agent,
|
|
581
|
+
sessionId: string,
|
|
582
|
+
threadId: string,
|
|
583
|
+
): Promise<string> {
|
|
584
|
+
const allAgents = loadAgents()
|
|
585
|
+
const agentIds = orchestrator.subAgentIds || []
|
|
586
|
+
const agents = agentIds.map((id) => allAgents[id]).filter(Boolean) as Agent[]
|
|
587
|
+
|
|
588
|
+
// Recreate the same tools
|
|
589
|
+
const delegateTool = tool(
|
|
590
|
+
async ({ agentName, task: agentTask }) => {
|
|
591
|
+
const agent = agents.find((a) => a.name.toLowerCase() === agentName.toLowerCase())
|
|
592
|
+
if (!agent) return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
|
|
593
|
+
const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
|
|
594
|
+
saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
|
|
595
|
+
name: 'delegate_to_agent',
|
|
596
|
+
input: JSON.stringify({ agentName: agent.name, agentId: agent.id, task: agentTask }),
|
|
597
|
+
output: result.slice(0, 2000),
|
|
598
|
+
}])
|
|
599
|
+
return result
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: 'delegate_to_agent',
|
|
603
|
+
description: 'Delegate a task to one of the available agents.',
|
|
604
|
+
schema: z.object({
|
|
605
|
+
agentName: z.string().describe('Name of the agent to delegate to'),
|
|
606
|
+
task: z.string().describe('The task description for the agent'),
|
|
607
|
+
}),
|
|
608
|
+
},
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
const db = getMemoryDb()
|
|
612
|
+
const storeMemoryTool = tool(
|
|
613
|
+
async ({ category, title, content }) => {
|
|
614
|
+
db.add({ agentId: orchestrator.id, sessionId, category, title, content })
|
|
615
|
+
return 'Memory stored successfully.'
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: 'store_memory',
|
|
619
|
+
description: 'Store information in long-term memory.',
|
|
620
|
+
schema: z.object({
|
|
621
|
+
category: z.string(), title: z.string(), content: z.string(),
|
|
622
|
+
}),
|
|
623
|
+
},
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
const searchMemoryTool = tool(
|
|
627
|
+
async ({ query }) => {
|
|
628
|
+
const results = db.search(query, orchestrator.id)
|
|
629
|
+
if (!results.length) return 'No matching memories found.'
|
|
630
|
+
return results.map((m) => `[${m.category}] ${m.title}: ${m.content.slice(0, 300)}`).join('\n')
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: 'search_memory',
|
|
634
|
+
description: 'Search long-term memory.',
|
|
635
|
+
schema: z.object({ query: z.string() }),
|
|
636
|
+
},
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
const markCompleteTool = tool(
|
|
640
|
+
async ({ summary }) => `ORCHESTRATION_COMPLETE: ${summary}`,
|
|
641
|
+
{
|
|
642
|
+
name: 'mark_complete',
|
|
643
|
+
description: 'Signal orchestration is done.',
|
|
644
|
+
schema: z.object({ summary: z.string() }),
|
|
645
|
+
},
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
const availableSecrets = getSecretsForOrchestrator(orchestrator.id)
|
|
649
|
+
const getSecretTool = tool(
|
|
650
|
+
async ({ serviceName }) => {
|
|
651
|
+
const match = availableSecrets.find(
|
|
652
|
+
(s) => s.service.toLowerCase() === serviceName.toLowerCase() || s.name.toLowerCase() === serviceName.toLowerCase(),
|
|
653
|
+
)
|
|
654
|
+
if (!match) return `No secret found for "${serviceName}".`
|
|
655
|
+
return JSON.stringify({ name: match.name, service: match.service, value: match.value })
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
name: 'get_secret',
|
|
659
|
+
description: 'Retrieve a stored credential/secret by service name.',
|
|
660
|
+
schema: z.object({ serviceName: z.string() }),
|
|
661
|
+
},
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
const commentOnTaskTool = tool(
|
|
665
|
+
async ({ taskId, comment }) => {
|
|
666
|
+
const tasks = loadTasks()
|
|
667
|
+
const t = tasks[taskId]
|
|
668
|
+
if (!t) return `Task "${taskId}" not found.`
|
|
669
|
+
if (!t.comments) t.comments = []
|
|
670
|
+
t.comments.push({
|
|
671
|
+
id: crypto.randomBytes(4).toString('hex'),
|
|
672
|
+
author: orchestrator.name,
|
|
673
|
+
agentId: orchestrator.id,
|
|
674
|
+
text: comment,
|
|
675
|
+
createdAt: Date.now(),
|
|
676
|
+
})
|
|
677
|
+
t.updatedAt = Date.now()
|
|
678
|
+
saveTasks(tasks)
|
|
679
|
+
return `Comment added to task "${t.title}".`
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
name: 'comment_on_task',
|
|
683
|
+
description: 'Add a comment to a task.',
|
|
684
|
+
schema: z.object({ taskId: z.string(), comment: z.string() }),
|
|
685
|
+
},
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
const createTaskTool = tool(
|
|
689
|
+
async ({ title, description: desc }) => {
|
|
690
|
+
const tasks = loadTasks()
|
|
691
|
+
const id = crypto.randomBytes(4).toString('hex')
|
|
692
|
+
tasks[id] = {
|
|
693
|
+
id, title, description: desc, status: 'backlog',
|
|
694
|
+
agentId: orchestrator.id, sessionId: null, result: null, error: null,
|
|
695
|
+
comments: [], createdAt: Date.now(), updatedAt: Date.now(),
|
|
696
|
+
queuedAt: null, startedAt: null, completedAt: null,
|
|
697
|
+
}
|
|
698
|
+
saveTasks(tasks)
|
|
699
|
+
return `Task "${title}" created in backlog (id: ${id}).`
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
name: 'create_task',
|
|
703
|
+
description: 'Create a new task in the backlog.',
|
|
704
|
+
schema: z.object({ title: z.string(), description: z.string() }),
|
|
705
|
+
},
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
const engine = getOrchestrationEngineConfig(orchestrator)
|
|
709
|
+
const llm = buildChatModel({
|
|
710
|
+
provider: engine.provider, model: engine.model,
|
|
711
|
+
apiKey: engine.apiKey, apiEndpoint: engine.apiEndpoint,
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
const checkpointSaver = getCheckpointSaver()
|
|
715
|
+
const settings = loadSettings()
|
|
716
|
+
const isStrictMode = settings.capabilityPolicyMode === 'strict'
|
|
717
|
+
|
|
718
|
+
const allTools = [delegateTool, storeMemoryTool, searchMemoryTool, getSecretTool, commentOnTaskTool, createTaskTool, markCompleteTool]
|
|
719
|
+
const llmWithTools = llm.bindTools(allTools)
|
|
720
|
+
const toolNode = new ToolNode(allTools)
|
|
721
|
+
|
|
722
|
+
async function agentNode(state: typeof MessagesAnnotation.State) {
|
|
723
|
+
const response = await llmWithTools.invoke(state.messages)
|
|
724
|
+
return { messages: [response] }
|
|
725
|
+
}
|
|
726
|
+
function routerNode(state: typeof MessagesAnnotation.State) {
|
|
727
|
+
return { messages: [] }
|
|
728
|
+
}
|
|
729
|
+
function shouldContinue(state: typeof MessagesAnnotation.State) {
|
|
730
|
+
const lastMsg = state.messages[state.messages.length - 1]
|
|
731
|
+
const toolCalls = (lastMsg as any)?.tool_calls
|
|
732
|
+
if (toolCalls && toolCalls.length > 0) return 'tools'
|
|
733
|
+
return END
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const graphAgent = new StateGraph(MessagesAnnotation)
|
|
737
|
+
.addNode('agent', agentNode)
|
|
738
|
+
.addNode('tools', toolNode)
|
|
739
|
+
.addNode('router', routerNode)
|
|
740
|
+
.addEdge(START, 'agent')
|
|
741
|
+
.addConditionalEdges('agent', shouldContinue, { tools: 'tools', [END]: END })
|
|
742
|
+
.addEdge('tools', 'router')
|
|
743
|
+
.addEdge('router', 'agent')
|
|
744
|
+
.compile({
|
|
745
|
+
checkpointer: checkpointSaver,
|
|
746
|
+
...(isStrictMode ? { interruptBefore: ['tools'] } : {}),
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
let finalResult = ''
|
|
750
|
+
const runtime = loadRuntimeSettings()
|
|
751
|
+
const recursionLimit = getOrchestratorLoopRecursionLimit(runtime)
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
// Resume from checkpoint with null input — the checkpoint has the full state
|
|
755
|
+
const stream = await graphAgent.stream(null, {
|
|
756
|
+
recursionLimit,
|
|
757
|
+
configurable: { thread_id: threadId },
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
for await (const chunk of stream) {
|
|
761
|
+
const agentChunk = (chunk as any).agent
|
|
762
|
+
if (agentChunk?.messages) {
|
|
763
|
+
const msgs = Array.isArray(agentChunk.messages) ? agentChunk.messages : [agentChunk.messages]
|
|
764
|
+
for (const msg of msgs) {
|
|
765
|
+
const text = typeof msg.content === 'string'
|
|
766
|
+
? msg.content
|
|
767
|
+
: Array.isArray(msg.content)
|
|
768
|
+
? msg.content.map((c: any) => c.text || '').join('')
|
|
769
|
+
: ''
|
|
770
|
+
if (text) {
|
|
771
|
+
finalResult = text
|
|
772
|
+
saveMessage(sessionId, 'assistant', text)
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Check for another interrupt
|
|
779
|
+
const state = await graphAgent.getState({ configurable: { thread_id: threadId } })
|
|
780
|
+
const nextNodes = state?.next || []
|
|
781
|
+
if (nextNodes.includes('tools')) {
|
|
782
|
+
const messages = state.values?.messages || []
|
|
783
|
+
const lastMsg = messages[messages.length - 1]
|
|
784
|
+
const toolCalls = lastMsg?.tool_calls || lastMsg?.additional_kwargs?.tool_calls || []
|
|
785
|
+
const pendingCall = toolCalls[0]
|
|
786
|
+
if (pendingCall) {
|
|
787
|
+
// Find the task by threadId
|
|
788
|
+
const tasks = loadTasks()
|
|
789
|
+
for (const t of Object.values(tasks)) {
|
|
790
|
+
if (t.pendingApproval?.threadId === threadId || t.id === threadId) {
|
|
791
|
+
t.pendingApproval = {
|
|
792
|
+
toolName: pendingCall.name || pendingCall.function?.name || 'unknown',
|
|
793
|
+
args: pendingCall.args || {},
|
|
794
|
+
threadId,
|
|
795
|
+
}
|
|
796
|
+
t.updatedAt = Date.now()
|
|
797
|
+
saveTasks(tasks)
|
|
798
|
+
notify('tasks')
|
|
799
|
+
return `Waiting for approval: ${t.pendingApproval.toolName}`
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
} catch (err: any) {
|
|
805
|
+
console.error(`[orchestrator-lg] Resume error:`, err.message || String(err))
|
|
806
|
+
saveMessage(sessionId, 'assistant', `[Error] ${err.message || String(err)}`)
|
|
807
|
+
throw err
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const completeMatch = finalResult.match(/ORCHESTRATION_COMPLETE:\s*([\s\S]+)/)
|
|
811
|
+
return completeMatch ? completeMatch[1].trim() : finalResult
|
|
812
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
loadSessions, saveSessions, loadAgents,
|
|
4
4
|
loadCredentials, decryptKey, loadSettings, loadSkills,
|
|
5
5
|
} from './storage'
|
|
6
|
+
import { WORKSPACE_DIR } from './data-dir'
|
|
6
7
|
import { loadRuntimeSettings, getLegacyOrchestratorMaxTurns } from './runtime-settings'
|
|
7
8
|
import { getMemoryDb } from './memory-db'
|
|
8
9
|
import { getProvider } from '../providers'
|
|
@@ -23,7 +24,7 @@ export function createOrchestratorSession(
|
|
|
23
24
|
sessions[sessionId] = {
|
|
24
25
|
id: sessionId,
|
|
25
26
|
name: `[Orch] ${orchestrator.name}: ${task.slice(0, 40)}`,
|
|
26
|
-
cwd: cwd ||
|
|
27
|
+
cwd: cwd || WORKSPACE_DIR,
|
|
27
28
|
user: 'system',
|
|
28
29
|
provider: orchestrator.provider,
|
|
29
30
|
model: orchestrator.model,
|
|
@@ -63,13 +64,14 @@ export async function executeOrchestrator(
|
|
|
63
64
|
orchestrator: Agent,
|
|
64
65
|
task: string,
|
|
65
66
|
sessionId: string,
|
|
67
|
+
taskId?: string,
|
|
66
68
|
): Promise<string> {
|
|
67
69
|
// Use LangGraph for all non-CLI providers (including OpenAI-compatible custom providers)
|
|
68
70
|
const isCliProvider = orchestrator.provider === 'claude-cli' || orchestrator.provider === 'codex-cli' || orchestrator.provider === 'opencode-cli'
|
|
69
71
|
if (!isCliProvider) {
|
|
70
72
|
console.log(`[orchestrator] Using LangGraph engine for ${orchestrator.name} (${orchestrator.provider})`)
|
|
71
73
|
const { executeLangGraphOrchestrator } = await import('./orchestrator-lg')
|
|
72
|
-
return executeLangGraphOrchestrator(orchestrator, task, sessionId)
|
|
74
|
+
return executeLangGraphOrchestrator(orchestrator, task, sessionId, taskId)
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
// claude-cli fallback (no structured tool calling)
|
|
@@ -156,7 +158,10 @@ async function executeOrchestratorLegacy(
|
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
const
|
|
161
|
+
const windowedHistory = conversationHistory.length > 10
|
|
162
|
+
? [conversationHistory[0], ...conversationHistory.slice(-9)]
|
|
163
|
+
: conversationHistory
|
|
164
|
+
const fullText = await callProvider(orchestrator, systemPrompt, windowedHistory)
|
|
160
165
|
conversationHistory.push({ role: 'assistant', text: fullText })
|
|
161
166
|
|
|
162
167
|
// Save to session
|
|
@@ -189,6 +194,21 @@ async function executeOrchestratorLegacy(
|
|
|
189
194
|
role: 'user',
|
|
190
195
|
text: `[Agent ${agent.name} result]:\n${subResult}`,
|
|
191
196
|
})
|
|
197
|
+
// Save structured delegation message for rich card rendering
|
|
198
|
+
session.messages.push({
|
|
199
|
+
role: 'assistant' as const,
|
|
200
|
+
text: `Delegated to ${agent.name}: ${cmd.delegate.task.slice(0, 100)}`,
|
|
201
|
+
time: Date.now(),
|
|
202
|
+
toolEvents: [{
|
|
203
|
+
name: 'delegate_to_agent',
|
|
204
|
+
input: JSON.stringify({ agentName: agent.name, agentId: agent.id, task: cmd.delegate.task }),
|
|
205
|
+
output: subResult.slice(0, 2000),
|
|
206
|
+
}],
|
|
207
|
+
})
|
|
208
|
+
session.lastActiveAt = Date.now()
|
|
209
|
+
const ds = loadSessions()
|
|
210
|
+
ds[sessionId] = session
|
|
211
|
+
saveSessions(ds)
|
|
192
212
|
}
|
|
193
213
|
|
|
194
214
|
if (cmd.memory_store) {
|
|
@@ -248,7 +268,7 @@ async function executeSubTask(
|
|
|
248
268
|
const childSession = {
|
|
249
269
|
id: childId,
|
|
250
270
|
name: `[Agent] ${agent.name}: ${task.slice(0, 40)}`,
|
|
251
|
-
cwd: parentSession?.cwd ||
|
|
271
|
+
cwd: parentSession?.cwd || WORKSPACE_DIR,
|
|
252
272
|
user: 'system',
|
|
253
273
|
provider: agent.provider,
|
|
254
274
|
model: agent.model,
|
|
@@ -310,7 +330,7 @@ export async function callProvider(
|
|
|
310
330
|
model: agent.model,
|
|
311
331
|
credentialId: agent.credentialId,
|
|
312
332
|
apiEndpoint: agent.apiEndpoint,
|
|
313
|
-
cwd:
|
|
333
|
+
cwd: WORKSPACE_DIR,
|
|
314
334
|
tools: agent.tools || [],
|
|
315
335
|
messages: history.map((h) => ({
|
|
316
336
|
role: h.role as 'user' | 'assistant',
|
package/src/lib/server/queue.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import crypto from 'crypto'
|
|
2
2
|
import { loadTasks, saveTasks, loadQueue, saveQueue, loadAgents, loadSchedules, saveSchedules, loadSessions, saveSessions, loadSettings } from './storage'
|
|
3
|
+
import { notify } from './ws-hub'
|
|
4
|
+
import { WORKSPACE_DIR } from './data-dir'
|
|
3
5
|
import { createOrchestratorSession, executeOrchestrator } from './orchestrator'
|
|
4
6
|
import { formatValidationFailure, validateTaskCompletion } from './task-validation'
|
|
5
7
|
import { ensureTaskCompletionReport } from './task-reports'
|
|
6
8
|
import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
7
9
|
import { executeSessionChatTurn } from './chat-execution'
|
|
8
10
|
import { extractTaskResult, formatResultBody } from './task-result'
|
|
11
|
+
import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
9
12
|
import type { Agent, BoardTask, Message } from '@/types'
|
|
10
13
|
|
|
11
14
|
let processing = false
|
|
@@ -119,7 +122,7 @@ async function executeTaskRun(
|
|
|
119
122
|
): Promise<string> {
|
|
120
123
|
const prompt = task.description || task.title
|
|
121
124
|
if (agent?.isOrchestrator) {
|
|
122
|
-
return executeOrchestrator(agent, prompt, sessionId)
|
|
125
|
+
return executeOrchestrator(agent, prompt, sessionId, task.id)
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
const run = await executeSessionChatTurn({
|
|
@@ -389,7 +392,7 @@ export function validateCompletedTasksQueue() {
|
|
|
389
392
|
}
|
|
390
393
|
}
|
|
391
394
|
|
|
392
|
-
if (tasksDirty) saveTasks(tasks)
|
|
395
|
+
if (tasksDirty) { saveTasks(tasks); notify('tasks') }
|
|
393
396
|
if (sessionsDirty) saveSessions(sessions)
|
|
394
397
|
if (demoted > 0) {
|
|
395
398
|
console.warn(`[queue] Demoted ${demoted} invalid completed task(s) to failed after validation audit`)
|
|
@@ -510,9 +513,12 @@ export async function processNext() {
|
|
|
510
513
|
task.startedAt = Date.now()
|
|
511
514
|
task.retryScheduledAt = null
|
|
512
515
|
task.deadLetteredAt = null
|
|
516
|
+
// Clear transient failure fields so validation/error state reflects only this attempt.
|
|
517
|
+
task.error = null
|
|
518
|
+
task.validation = null
|
|
513
519
|
task.updatedAt = Date.now()
|
|
514
520
|
|
|
515
|
-
const taskCwd = task.cwd ||
|
|
521
|
+
const taskCwd = task.cwd || WORKSPACE_DIR
|
|
516
522
|
let sessionId = ''
|
|
517
523
|
const scheduleTask = task as ScheduleTaskMeta
|
|
518
524
|
const isScheduleTask = scheduleTask.sourceType === 'schedule'
|
|
@@ -688,6 +694,8 @@ export async function processNext() {
|
|
|
688
694
|
}
|
|
689
695
|
|
|
690
696
|
saveTasks(t2)
|
|
697
|
+
notify('tasks')
|
|
698
|
+
notify('runs')
|
|
691
699
|
disableSessionHeartbeat(t2[taskId].sessionId)
|
|
692
700
|
}
|
|
693
701
|
const doneTask = t2[taskId]
|
|
@@ -698,6 +706,10 @@ export async function processNext() {
|
|
|
698
706
|
})
|
|
699
707
|
notifyMainChatScheduleResult(doneTask)
|
|
700
708
|
notifyAgentThreadTaskResult(doneTask)
|
|
709
|
+
// Clean up LangGraph checkpoints for completed tasks
|
|
710
|
+
getCheckpointSaver().deleteThread(taskId).catch((e) =>
|
|
711
|
+
console.warn(`[queue] Failed to clean up checkpoints for task ${taskId}:`, e)
|
|
712
|
+
)
|
|
701
713
|
console.log(`[queue] Task "${task.title}" completed`)
|
|
702
714
|
} else {
|
|
703
715
|
if (doneTask?.status === 'queued') {
|
|
@@ -735,6 +747,8 @@ export async function processNext() {
|
|
|
735
747
|
})
|
|
736
748
|
}
|
|
737
749
|
saveTasks(t2)
|
|
750
|
+
notify('tasks')
|
|
751
|
+
notify('runs')
|
|
738
752
|
disableSessionHeartbeat(t2[taskId].sessionId)
|
|
739
753
|
if (retryState === 'retry') {
|
|
740
754
|
const qRetry = loadQueue()
|
|
@@ -31,6 +31,7 @@ interface QueueEntry {
|
|
|
31
31
|
message: string
|
|
32
32
|
imagePath?: string
|
|
33
33
|
imageUrl?: string
|
|
34
|
+
attachedFiles?: string[]
|
|
34
35
|
onEvents: Array<(event: SSEEvent) => void>
|
|
35
36
|
signalController: AbortController
|
|
36
37
|
maxRuntimeMs?: number
|
|
@@ -236,6 +237,7 @@ async function drainExecution(executionKey: string): Promise<void> {
|
|
|
236
237
|
message: next.message,
|
|
237
238
|
imagePath: next.imagePath,
|
|
238
239
|
imageUrl: next.imageUrl,
|
|
240
|
+
attachedFiles: next.attachedFiles,
|
|
239
241
|
internal: next.run.internal,
|
|
240
242
|
source: next.run.source,
|
|
241
243
|
runId: next.run.id,
|
|
@@ -333,6 +335,7 @@ export interface EnqueueSessionRunInput {
|
|
|
333
335
|
message: string
|
|
334
336
|
imagePath?: string
|
|
335
337
|
imageUrl?: string
|
|
338
|
+
attachedFiles?: string[]
|
|
336
339
|
internal?: boolean
|
|
337
340
|
source?: string
|
|
338
341
|
mode?: SessionQueueMode
|
|
@@ -384,7 +387,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
384
387
|
|
|
385
388
|
const running = state.runningByExecution.get(executionKey)
|
|
386
389
|
const q = queueForExecution(executionKey)
|
|
387
|
-
if (mode === 'collect' && !input.imagePath && !input.imageUrl) {
|
|
390
|
+
if (mode === 'collect' && !input.imagePath && !input.imageUrl && !input.attachedFiles?.length) {
|
|
388
391
|
const nowMs = now()
|
|
389
392
|
const candidate = q.at(-1)
|
|
390
393
|
const canCoalesce = !!candidate
|
|
@@ -393,6 +396,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
393
396
|
&& candidate.run.source === source
|
|
394
397
|
&& !candidate.imagePath
|
|
395
398
|
&& !candidate.imageUrl
|
|
399
|
+
&& !candidate.attachedFiles?.length
|
|
396
400
|
&& (nowMs - candidate.run.queuedAt) <= COLLECT_COALESCE_WINDOW_MS
|
|
397
401
|
|
|
398
402
|
if (candidate && canCoalesce) {
|
|
@@ -444,6 +448,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
444
448
|
message: input.message,
|
|
445
449
|
imagePath: input.imagePath,
|
|
446
450
|
imageUrl: input.imageUrl,
|
|
451
|
+
attachedFiles: input.attachedFiles,
|
|
447
452
|
onEvents: input.onEvent ? [input.onEvent] : [],
|
|
448
453
|
signalController: new AbortController(),
|
|
449
454
|
maxRuntimeMs: effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
|