@swarmclawai/swarmclaw 0.6.7 → 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 +82 -39
- 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 +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- 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/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/graph/route.ts +46 -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/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- 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]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -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 +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- 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/checkpoint-timeline.tsx +112 -0
- 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 +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- 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 +37 -7
- package/src/components/home/home-view.tsx +54 -24
- 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 +87 -19
- 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-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- 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 +28 -9
- 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/hint-tip.tsx +31 -0
- 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 +149 -4
- 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 +224 -0
- 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 +72 -48
- 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 +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -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/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -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 +115 -16
- 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 +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -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 +32 -2
- 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 +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- 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 +78 -0
- 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 +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- 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 +24 -5
- 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/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +155 -10
|
@@ -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}`,
|
|
@@ -883,7 +935,7 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
883
935
|
const sessions = loadSessions()
|
|
884
936
|
const session = sessions[input.sessionId]
|
|
885
937
|
if (!session) return null
|
|
886
|
-
if (!
|
|
938
|
+
if (!isProtectedMainSession(session)) return handleAgentHeartbeatResult(session, input)
|
|
887
939
|
|
|
888
940
|
const now = Date.now()
|
|
889
941
|
const state = normalizeState(session.mainLoopState, now)
|
|
@@ -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
|
|
@@ -3,12 +3,15 @@ const MAIN_SESSION_NAME = '__main__'
|
|
|
3
3
|
export function isProtectedMainSession(session: any): boolean {
|
|
4
4
|
if (!session || typeof session !== 'object') return false
|
|
5
5
|
if (session.mainSession === true) return true
|
|
6
|
-
|
|
7
|
-
const name = typeof session.name === 'string' ? session.name.trim() : ''
|
|
8
|
-
if (name === MAIN_SESSION_NAME) return true
|
|
6
|
+
if (session.sessionType === 'orchestrated') return true
|
|
9
7
|
|
|
10
8
|
const id = typeof session.id === 'string' ? session.id.trim() : ''
|
|
11
9
|
if (id.startsWith('main-')) return true
|
|
10
|
+
if (id.startsWith('agent-thread-')) return true
|
|
11
|
+
|
|
12
|
+
const name = typeof session.name === 'string' ? session.name.trim() : ''
|
|
13
|
+
if (name === MAIN_SESSION_NAME) return true
|
|
14
|
+
if (name.startsWith('agent-thread:')) return true
|
|
12
15
|
|
|
13
16
|
return false
|
|
14
17
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { runMcpConformanceCheck } from './mcp-conformance.ts'
|
|
4
|
+
|
|
5
|
+
test('runMcpConformanceCheck reports connect/list failure for unsupported transport', async () => {
|
|
6
|
+
const result = await runMcpConformanceCheck({
|
|
7
|
+
id: 'bad',
|
|
8
|
+
name: 'Bad MCP',
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
transport: 'invalid-transport' as any,
|
|
11
|
+
createdAt: Date.now(),
|
|
12
|
+
updatedAt: Date.now(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
assert.equal(result.ok, false)
|
|
16
|
+
assert.equal(result.toolsCount, 0)
|
|
17
|
+
assert.ok(result.issues.some((issue) => issue.code === 'connect_or_list_failed'))
|
|
18
|
+
})
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { McpServerConfig } from '@/types'
|
|
2
|
+
import { connectMcpServer, disconnectMcpServer } from './mcp-client'
|
|
3
|
+
|
|
4
|
+
export interface McpConformanceIssue {
|
|
5
|
+
level: 'error' | 'warning'
|
|
6
|
+
code: string
|
|
7
|
+
message: string
|
|
8
|
+
toolName?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface McpConformanceOptions {
|
|
12
|
+
timeoutMs?: number
|
|
13
|
+
smokeToolName?: string | null
|
|
14
|
+
smokeToolArgs?: Record<string, unknown> | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface McpConformanceResult {
|
|
18
|
+
ok: boolean
|
|
19
|
+
serverId: string
|
|
20
|
+
serverName: string
|
|
21
|
+
checkedAt: number
|
|
22
|
+
toolsCount: number
|
|
23
|
+
smokeToolName: string | null
|
|
24
|
+
issues: McpConformanceIssue[]
|
|
25
|
+
timings: {
|
|
26
|
+
connectMs: number
|
|
27
|
+
listToolsMs: number
|
|
28
|
+
smokeInvokeMs: number | null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_TIMEOUT_MS = 12_000
|
|
33
|
+
const MIN_TIMEOUT_MS = 1_000
|
|
34
|
+
const MAX_TIMEOUT_MS = 120_000
|
|
35
|
+
|
|
36
|
+
function normalizeTimeoutMs(value: unknown): number {
|
|
37
|
+
const parsed = typeof value === 'number'
|
|
38
|
+
? value
|
|
39
|
+
: typeof value === 'string'
|
|
40
|
+
? Number.parseInt(value, 10)
|
|
41
|
+
: Number.NaN
|
|
42
|
+
if (!Number.isFinite(parsed)) return DEFAULT_TIMEOUT_MS
|
|
43
|
+
return Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, Math.trunc(parsed)))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
47
|
+
return new Promise<T>((resolve, reject) => {
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`))
|
|
50
|
+
}, timeoutMs)
|
|
51
|
+
promise.then(
|
|
52
|
+
(value) => {
|
|
53
|
+
clearTimeout(timer)
|
|
54
|
+
resolve(value)
|
|
55
|
+
},
|
|
56
|
+
(error) => {
|
|
57
|
+
clearTimeout(timer)
|
|
58
|
+
reject(error)
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
65
|
+
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizedRequired(schema: Record<string, unknown>): string[] {
|
|
69
|
+
const required = schema.required
|
|
70
|
+
if (!Array.isArray(required)) return []
|
|
71
|
+
return required.filter((entry): entry is string => typeof entry === 'string')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function findSmokeTool(tools: Array<Record<string, unknown>>, preferredName?: string | null): string | null {
|
|
75
|
+
const preferred = typeof preferredName === 'string' ? preferredName.trim() : ''
|
|
76
|
+
if (preferred) {
|
|
77
|
+
const found = tools.find((tool) => tool.name === preferred)
|
|
78
|
+
if (found) return preferred
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const noArg = tools.find((tool) => {
|
|
82
|
+
const schema = isRecord(tool.inputSchema) ? tool.inputSchema : {}
|
|
83
|
+
const required = normalizedRequired(schema)
|
|
84
|
+
return required.length === 0
|
|
85
|
+
})
|
|
86
|
+
if (noArg && typeof noArg.name === 'string' && noArg.name.trim()) return noArg.name
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validateToolSchemas(tools: Array<Record<string, unknown>>, issues: McpConformanceIssue[]): void {
|
|
91
|
+
const seenNames = new Set<string>()
|
|
92
|
+
for (const tool of tools) {
|
|
93
|
+
const toolName = typeof tool.name === 'string' ? tool.name.trim() : ''
|
|
94
|
+
if (!toolName) {
|
|
95
|
+
issues.push({
|
|
96
|
+
level: 'error',
|
|
97
|
+
code: 'tool_name_missing',
|
|
98
|
+
message: 'Tool is missing a valid name.',
|
|
99
|
+
})
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
if (seenNames.has(toolName)) {
|
|
103
|
+
issues.push({
|
|
104
|
+
level: 'error',
|
|
105
|
+
code: 'tool_name_duplicate',
|
|
106
|
+
message: `Duplicate tool name "${toolName}" detected.`,
|
|
107
|
+
toolName,
|
|
108
|
+
})
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
seenNames.add(toolName)
|
|
112
|
+
|
|
113
|
+
const schema = isRecord(tool.inputSchema) ? tool.inputSchema : null
|
|
114
|
+
if (!schema) {
|
|
115
|
+
issues.push({
|
|
116
|
+
level: 'warning',
|
|
117
|
+
code: 'tool_schema_missing',
|
|
118
|
+
message: `Tool "${toolName}" is missing an input schema.`,
|
|
119
|
+
toolName,
|
|
120
|
+
})
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const schemaType = typeof schema.type === 'string' ? schema.type : 'object'
|
|
125
|
+
if (schemaType !== 'object') {
|
|
126
|
+
issues.push({
|
|
127
|
+
level: 'warning',
|
|
128
|
+
code: 'tool_schema_non_object',
|
|
129
|
+
message: `Tool "${toolName}" schema type is "${schemaType}" (expected "object").`,
|
|
130
|
+
toolName,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const properties = isRecord(schema.properties) ? schema.properties : {}
|
|
135
|
+
const required = normalizedRequired(schema)
|
|
136
|
+
for (const req of required) {
|
|
137
|
+
if (!Object.prototype.hasOwnProperty.call(properties, req)) {
|
|
138
|
+
issues.push({
|
|
139
|
+
level: 'warning',
|
|
140
|
+
code: 'tool_schema_required_missing_property',
|
|
141
|
+
message: `Tool "${toolName}" marks "${req}" as required but it is not present in schema.properties.`,
|
|
142
|
+
toolName,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function runMcpConformanceCheck(
|
|
150
|
+
server: McpServerConfig,
|
|
151
|
+
options: McpConformanceOptions = {},
|
|
152
|
+
): Promise<McpConformanceResult> {
|
|
153
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMs)
|
|
154
|
+
const issues: McpConformanceIssue[] = []
|
|
155
|
+
const checkedAt = Date.now()
|
|
156
|
+
const result: McpConformanceResult = {
|
|
157
|
+
ok: false,
|
|
158
|
+
serverId: server.id,
|
|
159
|
+
serverName: server.name,
|
|
160
|
+
checkedAt,
|
|
161
|
+
toolsCount: 0,
|
|
162
|
+
smokeToolName: null,
|
|
163
|
+
issues,
|
|
164
|
+
timings: {
|
|
165
|
+
connectMs: 0,
|
|
166
|
+
listToolsMs: 0,
|
|
167
|
+
smokeInvokeMs: null,
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let client: unknown
|
|
172
|
+
let transport: unknown
|
|
173
|
+
const connectStart = Date.now()
|
|
174
|
+
try {
|
|
175
|
+
const conn = await withTimeout(connectMcpServer(server), timeoutMs, 'MCP connect')
|
|
176
|
+
client = conn.client
|
|
177
|
+
transport = conn.transport
|
|
178
|
+
result.timings.connectMs = Date.now() - connectStart
|
|
179
|
+
|
|
180
|
+
const mcpClient = client as { listTools: () => Promise<Record<string, unknown>>; callTool: (opts: Record<string, unknown>) => Promise<unknown> }
|
|
181
|
+
const listStart = Date.now()
|
|
182
|
+
const listResponse = await withTimeout(mcpClient.listTools(), timeoutMs, 'MCP listTools') as Record<string, unknown>
|
|
183
|
+
result.timings.listToolsMs = Date.now() - listStart
|
|
184
|
+
const tools = Array.isArray(listResponse?.tools) ? listResponse.tools as Array<Record<string, unknown>> : []
|
|
185
|
+
result.toolsCount = tools.length
|
|
186
|
+
|
|
187
|
+
validateToolSchemas(tools, issues)
|
|
188
|
+
|
|
189
|
+
const smokeToolName = findSmokeTool(tools, options.smokeToolName)
|
|
190
|
+
result.smokeToolName = smokeToolName
|
|
191
|
+
if (!smokeToolName) {
|
|
192
|
+
issues.push({
|
|
193
|
+
level: 'warning',
|
|
194
|
+
code: 'smoke_tool_missing',
|
|
195
|
+
message: 'No smoke-testable tool found (no no-arg tool and no explicit smokeToolName).',
|
|
196
|
+
})
|
|
197
|
+
} else {
|
|
198
|
+
const smokeArgs = options.smokeToolArgs && isRecord(options.smokeToolArgs)
|
|
199
|
+
? options.smokeToolArgs
|
|
200
|
+
: {}
|
|
201
|
+
const smokeStart = Date.now()
|
|
202
|
+
try {
|
|
203
|
+
await withTimeout(
|
|
204
|
+
mcpClient.callTool({ name: smokeToolName, arguments: smokeArgs }),
|
|
205
|
+
timeoutMs,
|
|
206
|
+
`MCP callTool(${smokeToolName})`,
|
|
207
|
+
)
|
|
208
|
+
} catch (err) {
|
|
209
|
+
issues.push({
|
|
210
|
+
level: 'error',
|
|
211
|
+
code: 'smoke_tool_failed',
|
|
212
|
+
message: err instanceof Error ? err.message : String(err),
|
|
213
|
+
toolName: smokeToolName,
|
|
214
|
+
})
|
|
215
|
+
} finally {
|
|
216
|
+
result.timings.smokeInvokeMs = Date.now() - smokeStart
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
issues.push({
|
|
221
|
+
level: 'error',
|
|
222
|
+
code: 'connect_or_list_failed',
|
|
223
|
+
message: err instanceof Error ? err.message : String(err),
|
|
224
|
+
})
|
|
225
|
+
} finally {
|
|
226
|
+
if (client && transport) {
|
|
227
|
+
await disconnectMcpServer(client, transport)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
result.ok = issues.every((issue) => issue.level !== 'error')
|
|
232
|
+
return result
|
|
233
|
+
}
|