@swarmclawai/swarmclaw 0.7.1 → 0.7.2
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 +85 -139
- package/package.json +1 -1
- package/src/app/api/agents/[id]/thread/route.ts +1 -2
- package/src/app/api/agents/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/main-loop/route.ts +2 -2
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +4 -52
- package/src/app/api/{sessions → chats}/route.ts +5 -7
- package/src/app/api/plugins/route.ts +3 -0
- package/src/app/api/plugins/settings/route.ts +35 -0
- package/src/app/api/usage/route.ts +30 -0
- package/src/cli/index.js +35 -33
- package/src/cli/index.ts +40 -39
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-chat-list.tsx +3 -3
- package/src/components/agents/agent-list.tsx +8 -13
- package/src/components/agents/agent-sheet.tsx +2 -2
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +2 -2
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +10 -14
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +3 -3
- package/src/components/chat/chat-header.tsx +156 -73
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +4 -5
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +2 -2
- package/src/components/{sessions/new-session-sheet.tsx → chat/new-chat-sheet.tsx} +6 -6
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/connectors/connector-sheet.tsx +1 -1
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/layout/app-layout.tsx +23 -2
- package/src/components/plugins/plugin-list.tsx +475 -254
- package/src/components/plugins/plugin-sheet.tsx +124 -10
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/command-palette.tsx +0 -1
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/settings-page.tsx +1 -12
- package/src/components/usage/metrics-dashboard.tsx +73 -0
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/approvals.ts +4 -4
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution.ts +36 -105
- package/src/lib/server/chatroom-helpers.ts +3 -3
- package/src/lib/server/connectors/manager.ts +4 -4
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +2 -2
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/main-agent-loop.ts +25 -160
- package/src/lib/server/main-session.ts +6 -13
- package/src/lib/server/orchestrator-lg.ts +3 -3
- package/src/lib/server/orchestrator.ts +5 -5
- package/src/lib/server/plugins.ts +112 -4
- package/src/lib/server/provider-health.ts +5 -3
- package/src/lib/server/queue.ts +12 -10
- package/src/lib/server/session-run-manager.test.ts +9 -6
- package/src/lib/server/session-run-manager.ts +1 -3
- package/src/lib/server/session-tools/calendar.ts +376 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +5 -2
- package/src/lib/server/session-tools/context.ts +7 -3
- package/src/lib/server/session-tools/crud.ts +14 -6
- package/src/lib/server/session-tools/delegate.ts +95 -8
- package/src/lib/server/session-tools/discovery.ts +2 -2
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +322 -0
- package/src/lib/server/session-tools/file.ts +5 -2
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/image-gen.ts +382 -0
- package/src/lib/server/session-tools/index.ts +74 -49
- package/src/lib/server/session-tools/memory.ts +139 -2
- package/src/lib/server/session-tools/monitor.ts +1 -1
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform.ts +6 -3
- package/src/lib/server/session-tools/plugin-creator.ts +3 -3
- package/src/lib/server/session-tools/replicate.ts +303 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +4 -2
- package/src/lib/server/session-tools/session-info.ts +7 -4
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +2 -2
- package/src/lib/server/session-tools/wallet.ts +29 -2
- package/src/lib/server/session-tools/web.ts +44 -5
- package/src/lib/server/storage.ts +29 -9
- package/src/lib/server/stream-agent-chat.ts +72 -249
- package/src/lib/server/tool-aliases.ts +26 -15
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +32 -27
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.ts +3 -1
- package/src/stores/use-app-store.ts +5 -5
- package/src/stores/use-chat-store.ts +7 -7
- package/src/types/index.ts +65 -3
- /package/src/app/api/{sessions → chats}/[id]/browser/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/chat/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/messages/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/stop/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
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,
|
|
4
|
+
import { loadSessions, saveSessions, loadAgents, saveAgents, loadSettings } from './storage'
|
|
5
5
|
import { log } from './logger'
|
|
6
6
|
import { getMemoryDb } from './memory-db'
|
|
7
|
-
import {
|
|
7
|
+
import { isMainLoopSession } from './main-session'
|
|
8
8
|
import { logExecution } from './execution-log'
|
|
9
9
|
import {
|
|
10
10
|
mergeGoalContracts,
|
|
@@ -352,25 +352,6 @@ function appendWorkingMemoryNote(state: MainLoopState, note: string) {
|
|
|
352
352
|
state.workingMemoryNotes = [...existing.slice(-23), value]
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
function inferGoalFromUserMessage(message: string): string | null {
|
|
356
|
-
const text = (message || '').trim()
|
|
357
|
-
if (!text) return null
|
|
358
|
-
if (/^SWARM_MAIN_(MISSION_TICK|AUTO_FOLLOWUP)\b/i.test(text)) return null
|
|
359
|
-
if (/^SWARM_HEARTBEAT_CHECK\b/i.test(text)) return null
|
|
360
|
-
if (/^(ok|okay|cool|thanks|thx|got it|nice|yep|yeah|nope|nah)[.! ]*$/i.test(text)) return null
|
|
361
|
-
return text.slice(0, 600)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function inferGoalFromSessionMessages(session: any): string | null {
|
|
365
|
-
const msgs = Array.isArray(session?.messages) ? session.messages : []
|
|
366
|
-
for (let i = msgs.length - 1; i >= 0; i -= 1) {
|
|
367
|
-
const msg = msgs[i]
|
|
368
|
-
if (msg?.role !== 'user') continue
|
|
369
|
-
const inferred = inferGoalFromUserMessage(typeof msg?.text === 'string' ? msg.text : '')
|
|
370
|
-
if (inferred) return inferred
|
|
371
|
-
}
|
|
372
|
-
return null
|
|
373
|
-
}
|
|
374
355
|
|
|
375
356
|
function parseMainLoopMeta(text: string): MainLoopMeta | null {
|
|
376
357
|
const raw = (text || '').trim()
|
|
@@ -505,110 +486,7 @@ function getMissionCompletionGateReason(session: MainLoopSessionEvidenceLike | n
|
|
|
505
486
|
return 'Mission requires screenshot artifact evidence (upload link or explicit sent screenshot confirmation) before completion.'
|
|
506
487
|
}
|
|
507
488
|
|
|
508
|
-
function upsertMissionTask(session: any, state: MainLoopState, now: number): string | null {
|
|
509
|
-
if (!state.goal) return state.missionTaskId || null
|
|
510
|
-
|
|
511
|
-
const tasks = loadTasks()
|
|
512
|
-
let task = state.missionTaskId ? tasks[state.missionTaskId] : null
|
|
513
|
-
if (!task) {
|
|
514
|
-
task = Object.values(tasks).find((t: any) =>
|
|
515
|
-
t?.sessionId === session.id
|
|
516
|
-
&& t?.title?.startsWith('Mission:')
|
|
517
|
-
&& t?.status !== 'archived'
|
|
518
|
-
) as any || null
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const title = `Mission: ${state.goal.slice(0, 140)}`
|
|
522
|
-
const statusMap = {
|
|
523
|
-
idle: 'backlog',
|
|
524
|
-
progress: 'running',
|
|
525
|
-
reflection: 'running',
|
|
526
|
-
blocked: 'failed',
|
|
527
|
-
ok: 'completed',
|
|
528
|
-
} as const
|
|
529
|
-
let mappedStatus = statusMap[state.status]
|
|
530
|
-
const completionGateReason = mappedStatus === 'completed'
|
|
531
|
-
? getMissionCompletionGateReason(session, state)
|
|
532
|
-
: null
|
|
533
|
-
if (completionGateReason) mappedStatus = 'running'
|
|
534
|
-
|
|
535
|
-
let changed = false
|
|
536
|
-
const contractLines = buildGoalContractLines(state)
|
|
537
|
-
const planLines = state.planSteps.length
|
|
538
|
-
? [`plan_steps: ${state.planSteps.join(' -> ')}`]
|
|
539
|
-
: []
|
|
540
|
-
if (state.currentPlanStep) planLines.push(`current_plan_step: ${state.currentPlanStep}`)
|
|
541
|
-
if (state.reviewNote) planLines.push(`latest_review: ${state.reviewNote}`)
|
|
542
|
-
|
|
543
|
-
const baseDescription = [
|
|
544
|
-
'Autonomous mission goal tracked from main loop.',
|
|
545
|
-
`Goal: ${state.goal}`,
|
|
546
|
-
state.nextAction ? `Next action: ${state.nextAction}` : '',
|
|
547
|
-
completionGateReason ? `Completion gate: ${completionGateReason}` : '',
|
|
548
|
-
...contractLines,
|
|
549
|
-
...planLines,
|
|
550
|
-
].filter(Boolean).join('\n')
|
|
551
|
-
|
|
552
|
-
if (!task) {
|
|
553
|
-
const id = genId()
|
|
554
|
-
task = {
|
|
555
|
-
id,
|
|
556
|
-
title,
|
|
557
|
-
description: baseDescription,
|
|
558
|
-
status: mappedStatus,
|
|
559
|
-
agentId: session.agentId || 'default',
|
|
560
|
-
sessionId: session.id,
|
|
561
|
-
result: state.summary || null,
|
|
562
|
-
error: state.status === 'blocked' ? (state.summary || 'Blocked') : null,
|
|
563
|
-
createdAt: now,
|
|
564
|
-
updatedAt: now,
|
|
565
|
-
startedAt: mappedStatus === 'running' ? now : null,
|
|
566
|
-
completedAt: mappedStatus === 'completed' ? now : null,
|
|
567
|
-
queuedAt: null,
|
|
568
|
-
archivedAt: null,
|
|
569
|
-
comments: [],
|
|
570
|
-
images: [],
|
|
571
|
-
validation: null,
|
|
572
|
-
}
|
|
573
|
-
tasks[id] = task
|
|
574
|
-
changed = true
|
|
575
|
-
} else {
|
|
576
|
-
if (task.title !== title) {
|
|
577
|
-
task.title = title
|
|
578
|
-
changed = true
|
|
579
|
-
}
|
|
580
|
-
const nextDescription = baseDescription
|
|
581
|
-
if (task.description !== nextDescription) {
|
|
582
|
-
task.description = nextDescription
|
|
583
|
-
changed = true
|
|
584
|
-
}
|
|
585
|
-
if (task.status !== mappedStatus) {
|
|
586
|
-
task.status = mappedStatus
|
|
587
|
-
changed = true
|
|
588
|
-
if (mappedStatus === 'running' && !task.startedAt) task.startedAt = now
|
|
589
|
-
if (mappedStatus === 'completed') task.completedAt = now
|
|
590
|
-
}
|
|
591
|
-
const nextResult = state.summary || task.result || null
|
|
592
|
-
if (task.result !== nextResult) {
|
|
593
|
-
task.result = nextResult
|
|
594
|
-
changed = true
|
|
595
|
-
}
|
|
596
|
-
const nextError = mappedStatus === 'failed'
|
|
597
|
-
? (state.summary || state.nextAction || 'Blocked')
|
|
598
|
-
: null
|
|
599
|
-
if (task.error !== nextError) {
|
|
600
|
-
task.error = nextError
|
|
601
|
-
changed = true
|
|
602
|
-
}
|
|
603
|
-
if (changed) task.updatedAt = now
|
|
604
|
-
tasks[task.id] = task
|
|
605
|
-
}
|
|
606
489
|
|
|
607
|
-
if (changed) {
|
|
608
|
-
saveTasks(tasks)
|
|
609
|
-
}
|
|
610
|
-
return task?.id || null
|
|
611
|
-
}
|
|
612
490
|
|
|
613
491
|
function maybeStoreMissionMemoryNote(
|
|
614
492
|
session: any,
|
|
@@ -617,8 +495,7 @@ function maybeStoreMissionMemoryNote(
|
|
|
617
495
|
source: string,
|
|
618
496
|
force = false,
|
|
619
497
|
) {
|
|
620
|
-
if (!
|
|
621
|
-
if (!state.goal) return
|
|
498
|
+
if (!session?.agentId || !state.goal) return
|
|
622
499
|
if (!force && state.lastMemoryNoteAt && (now - state.lastMemoryNoteAt) < MEMORY_NOTE_MIN_INTERVAL_MS) return
|
|
623
500
|
|
|
624
501
|
const summary = state.summary || 'No summary'
|
|
@@ -670,8 +547,7 @@ function maybeStoreMissionMemoryNote(
|
|
|
670
547
|
}
|
|
671
548
|
}
|
|
672
549
|
|
|
673
|
-
function buildFollowupPrompt(state: MainLoopState, opts?: {
|
|
674
|
-
const hasMemoryTool = opts?.hasMemoryTool === true
|
|
550
|
+
function buildFollowupPrompt(state: MainLoopState, opts?: { agent?: Record<string, unknown> | null; session?: Record<string, unknown> | null }): string {
|
|
675
551
|
const identityContext = buildIdentityContext(opts?.session, opts?.agent)
|
|
676
552
|
const goal = state.goal || 'No explicit goal yet. Continue with the strongest actionable objective from recent context.'
|
|
677
553
|
const nextAction = state.nextAction || 'Determine the next highest-impact action and execute it.'
|
|
@@ -699,11 +575,10 @@ function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: bool
|
|
|
699
575
|
? 'Assist mode: execute safe internal analysis by default, and ask before irreversible external side effects (sending messages, purchases, account mutations).'
|
|
700
576
|
: 'Autonomous mode: execute safe next actions without waiting for confirmation; ask only when blocked by permissions, credentials, or policy.',
|
|
701
577
|
'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
|
|
702
|
-
|
|
703
|
-
? 'Use memory_tool actively: recall relevant prior notes before acting, and store a concise note after each meaningful step.'
|
|
704
|
-
: 'memory_tool is unavailable in this session. Keep concise progress summaries in your status/meta output.',
|
|
578
|
+
'Use any available tools actively to maintain state across turns.',
|
|
705
579
|
'If you are blocked by missing credentials, permissions, or policy limits, say exactly what is blocked and the smallest unblock needed.',
|
|
706
580
|
'For screenshot/image delivery goals (including scheduled captures), do not report status "ok" until a real artifact exists (upload link or explicit sent-file confirmation).',
|
|
581
|
+
'When the mission goal is fully completed, set status to "ok" with follow_up:false and include a clear summary of what was accomplished. The loop will auto-pause.',
|
|
707
582
|
'If no meaningful action remains right now, reply exactly HEARTBEAT_OK.',
|
|
708
583
|
'Otherwise include a concise human update, then append exactly one [MAIN_LOOP_META] JSON line.',
|
|
709
584
|
'Optionally append one [MAIN_LOOP_PLAN] JSON line when you create/revise a plan.',
|
|
@@ -714,19 +589,19 @@ function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: bool
|
|
|
714
589
|
].join('\n')
|
|
715
590
|
}
|
|
716
591
|
|
|
592
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
717
593
|
export function isMainSession(session: any): boolean {
|
|
718
|
-
return
|
|
594
|
+
return isMainLoopSession(session)
|
|
719
595
|
}
|
|
720
596
|
|
|
597
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
721
598
|
export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: string): string {
|
|
722
599
|
const now = Date.now()
|
|
723
600
|
const agents = loadAgents()
|
|
724
601
|
const agent = session.agentId ? agents[session.agentId] : null
|
|
725
602
|
const identityContext = buildIdentityContext(session, agent)
|
|
726
603
|
const state = normalizeState(session?.mainLoopState, now)
|
|
727
|
-
const goal = state.goal ||
|
|
728
|
-
const hasMemoryTool = Array.isArray(session?.tools) && session.tools.includes('memory')
|
|
729
|
-
|
|
604
|
+
const goal = state.goal || null
|
|
730
605
|
const promptGoal = goal || 'No explicit mission captured yet. Infer the mission from recent user instructions and continue proactively.'
|
|
731
606
|
const promptSummary = state.summary || 'No prior mission summary yet.'
|
|
732
607
|
const promptNextAction = state.nextAction || 'No queued action. Determine one.'
|
|
@@ -758,11 +633,10 @@ export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: strin
|
|
|
758
633
|
'Use tools where needed, verify outcomes, and avoid vague status-only replies.',
|
|
759
634
|
'Do not ask broad exploratory questions when a safe next action exists. Pick a reasonable assumption, execute, and adapt from evidence.',
|
|
760
635
|
'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
|
|
761
|
-
|
|
762
|
-
? 'Use memory_tool actively: recall relevant prior notes before acting, and store concise notes about progress, constraints, and next step after each meaningful action.'
|
|
763
|
-
: 'If memory_tool is unavailable, keep concise state in summary/next_action and continue execution.',
|
|
636
|
+
'Use any available tools actively to maintain state and recall context across turns.',
|
|
764
637
|
'Use a planner-executor-review loop: keep a concrete step plan, execute one meaningful step, then self-review and either continue or re-plan.',
|
|
765
638
|
'For screenshot/image delivery goals (including scheduled captures), do not report status "ok" until a real artifact exists (upload link or explicit sent-file confirmation).',
|
|
639
|
+
'When the mission goal is fully completed, set status to "ok" with follow_up:false and include a clear summary of what was accomplished. The loop will auto-pause.',
|
|
766
640
|
'If nothing important changed and no action is needed now, reply exactly HEARTBEAT_OK.',
|
|
767
641
|
'Otherwise: provide a concise human-readable update, then append exactly one [MAIN_LOOP_META] JSON line.',
|
|
768
642
|
'Optionally append one [MAIN_LOOP_PLAN] JSON line when creating/updating plan steps.',
|
|
@@ -775,8 +649,7 @@ export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: strin
|
|
|
775
649
|
].join('\n')
|
|
776
650
|
}
|
|
777
651
|
|
|
778
|
-
export function stripMainLoopMetaForPersistence(text: string
|
|
779
|
-
if (!internal) return text
|
|
652
|
+
export function stripMainLoopMetaForPersistence(text: string): string {
|
|
780
653
|
if (!text) return ''
|
|
781
654
|
return text
|
|
782
655
|
.split('\n')
|
|
@@ -935,30 +808,16 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
935
808
|
const sessions = loadSessions()
|
|
936
809
|
const session = sessions[input.sessionId]
|
|
937
810
|
if (!session) return null
|
|
938
|
-
if (!
|
|
811
|
+
if (!isMainLoopSession(session)) return handleAgentHeartbeatResult(session, input)
|
|
939
812
|
|
|
940
813
|
const now = Date.now()
|
|
941
814
|
const state = normalizeState(session.mainLoopState, now)
|
|
942
|
-
const hasMemoryTool = Array.isArray(session.tools) && session.tools.includes('memory')
|
|
943
815
|
state.pendingEvents = pruneEvents(state.pendingEvents, now)
|
|
944
816
|
let forceMemoryNote = false
|
|
945
817
|
|
|
946
|
-
const userGoal = inferGoalFromUserMessage(input.message)
|
|
947
818
|
const userGoalContract = parseGoalContractFromText(input.message)
|
|
948
819
|
if (!input.internal) {
|
|
949
|
-
if (
|
|
950
|
-
state.goal = userGoal
|
|
951
|
-
if (userGoalContract) state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
|
|
952
|
-
state.status = 'progress'
|
|
953
|
-
appendEvent(state, 'user_instruction', `User goal updated: ${userGoal}`, now)
|
|
954
|
-
appendTimeline(state, 'user_goal', `Goal updated: ${userGoal}`, now, state.status)
|
|
955
|
-
appendWorkingMemoryNote(state, `goal:${userGoal}`)
|
|
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
|
-
})
|
|
961
|
-
} else if (userGoalContract?.objective) {
|
|
820
|
+
if (userGoalContract?.objective) {
|
|
962
821
|
state.goal = userGoalContract.objective
|
|
963
822
|
state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
|
|
964
823
|
state.status = 'progress'
|
|
@@ -1022,13 +881,13 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1022
881
|
let followup: MainLoopFollowupRequest | null = null
|
|
1023
882
|
const shouldAutoKickFromUserGoal = !input.internal
|
|
1024
883
|
&& !input.error
|
|
1025
|
-
&&
|
|
884
|
+
&& !!userGoalContract?.objective
|
|
1026
885
|
&& !state.paused
|
|
1027
886
|
&& state.autonomyMode === 'autonomous'
|
|
1028
887
|
|
|
1029
888
|
if (shouldAutoKickFromUserGoal) {
|
|
1030
889
|
followup = {
|
|
1031
|
-
message: buildFollowupPrompt(state, {
|
|
890
|
+
message: buildFollowupPrompt(state, { agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
|
|
1032
891
|
delayMs: 1500,
|
|
1033
892
|
dedupeKey: `main-loop-user-kickoff:${input.sessionId}`,
|
|
1034
893
|
}
|
|
@@ -1116,7 +975,7 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1116
975
|
state.followupChainCount += 1
|
|
1117
976
|
const delaySec = clampInt(meta.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900)
|
|
1118
977
|
followup = {
|
|
1119
|
-
message: buildFollowupPrompt(state, {
|
|
978
|
+
message: buildFollowupPrompt(state, { agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
|
|
1120
979
|
delayMs: delaySec * 1000,
|
|
1121
980
|
dedupeKey: `main-loop-followup:${input.sessionId}`,
|
|
1122
981
|
}
|
|
@@ -1127,10 +986,15 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1127
986
|
if (state.status === 'ok' || state.status === 'blocked') {
|
|
1128
987
|
forceMemoryNote = true
|
|
1129
988
|
if (state.status === 'ok') {
|
|
989
|
+
// Auto-pause the mission loop — the goal is complete
|
|
990
|
+
state.paused = true
|
|
991
|
+
state.followupChainCount = 0
|
|
992
|
+
followup = null
|
|
1130
993
|
logExecution(input.sessionId, 'mission_complete', `Mission completed: ${toOneLine(state.goal || 'unknown', 200)}`, {
|
|
1131
994
|
agentId: session.agentId,
|
|
1132
995
|
detail: { momentumScore: state.momentumScore, followupChainCount: state.followupChainCount, missionTokens: state.missionTokens, missionCostUsd: state.missionCostUsd },
|
|
1133
996
|
})
|
|
997
|
+
appendTimeline(state, 'auto_pause', 'Mission goal completed — auto-paused.', now, state.status)
|
|
1134
998
|
}
|
|
1135
999
|
}
|
|
1136
1000
|
} else if (!isHeartbeatOk && trimmedText) {
|
|
@@ -1159,7 +1023,8 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1159
1023
|
}
|
|
1160
1024
|
}
|
|
1161
1025
|
|
|
1162
|
-
|
|
1026
|
+
// Agents don't auto-create tasks for themselves — they just do the work.
|
|
1027
|
+
// Tasks are created explicitly by the user or when delegating to another agent.
|
|
1163
1028
|
const shouldWritePeriodicMemory = !!state.summary && state.status === 'progress'
|
|
1164
1029
|
maybeStoreMissionMemoryNote(session, state, now, input.source, forceMemoryNote || shouldWritePeriodicMemory)
|
|
1165
1030
|
|
|
@@ -1,24 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Returns true for sessions that participate in the main-agent-loop
|
|
3
|
+
* (autonomous followups, mission tracking, etc).
|
|
4
|
+
* This includes agent thread sessions and orchestrated task sessions.
|
|
5
|
+
*/
|
|
6
|
+
export function isMainLoopSession(session: any): boolean {
|
|
4
7
|
if (!session || typeof session !== 'object') return false
|
|
5
|
-
if (session.mainSession === true) return true
|
|
6
8
|
if (session.sessionType === 'orchestrated') return true
|
|
7
9
|
|
|
8
10
|
const id = typeof session.id === 'string' ? session.id.trim() : ''
|
|
9
|
-
if (id.startsWith('main-')) return true
|
|
10
11
|
if (id.startsWith('agent-thread-')) return true
|
|
11
12
|
|
|
12
13
|
const name = typeof session.name === 'string' ? session.name.trim() : ''
|
|
13
|
-
if (name === MAIN_SESSION_NAME) return true
|
|
14
14
|
if (name.startsWith('agent-thread:')) return true
|
|
15
15
|
|
|
16
16
|
return false
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
export function ensureMainSessionFlag(session: any): void {
|
|
20
|
-
if (!session || typeof session !== 'object') return
|
|
21
|
-
if (isProtectedMainSession(session)) {
|
|
22
|
-
session.mainSession = true
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -121,7 +121,7 @@ async function executeSubTaskViaCli(agent: Agent, task: string, parentSessionId:
|
|
|
121
121
|
sessionType: 'orchestrated' as const,
|
|
122
122
|
agentId: agent.id,
|
|
123
123
|
parentSessionId,
|
|
124
|
-
|
|
124
|
+
plugins: agent.plugins || agent.tools || [],
|
|
125
125
|
}
|
|
126
126
|
ss(sessions)
|
|
127
127
|
|
|
@@ -156,9 +156,9 @@ export async function executeLangGraphOrchestrator(
|
|
|
156
156
|
const agents = agentIds.map((id) => allAgents[id]).filter(Boolean) as Agent[]
|
|
157
157
|
const agentListContext = agents.length
|
|
158
158
|
? '\n\nAvailable agents:\n' + agents.map((a) => {
|
|
159
|
-
const
|
|
159
|
+
const plugins = (a.plugins || a.tools)?.length ? ` [plugins: ${(a.plugins || a.tools)!.join(', ')}]` : ''
|
|
160
160
|
const skills = a.skills?.length ? ` [skills: ${a.skills.join(', ')}]` : ''
|
|
161
|
-
return `- ${a.name}: ${a.description}${
|
|
161
|
+
return `- ${a.name}: ${a.description}${plugins}${skills}`
|
|
162
162
|
}).join('\n')
|
|
163
163
|
: '\n\n(No agents available for delegation.)'
|
|
164
164
|
|
|
@@ -45,7 +45,7 @@ export function createOrchestratorSession(
|
|
|
45
45
|
sessionType: 'orchestrated' as const,
|
|
46
46
|
agentId: orchestrator.id,
|
|
47
47
|
parentSessionId: parentSessionId || null,
|
|
48
|
-
|
|
48
|
+
plugins: Array.isArray(orchestrator.plugins) ? [...orchestrator.plugins] : (Array.isArray(orchestrator.tools) ? [...orchestrator.tools] : []),
|
|
49
49
|
heartbeatEnabled: false,
|
|
50
50
|
}
|
|
51
51
|
saveSessions(sessions)
|
|
@@ -95,9 +95,9 @@ async function executeOrchestratorLegacy(
|
|
|
95
95
|
const agentIds = orchestrator.subAgentIds || []
|
|
96
96
|
const agents = agentIds.map((id) => allAgents[id]).filter(Boolean)
|
|
97
97
|
const agentList = agents.map((a) => {
|
|
98
|
-
const
|
|
98
|
+
const plugins = (a.plugins || a.tools)?.length ? ` [plugins: ${(a.plugins || a.tools)!.join(', ')}]` : ''
|
|
99
99
|
const skills = a.skills?.length ? ` [skills: ${a.skills.join(', ')}]` : ''
|
|
100
|
-
return `- ${a.name}: ${a.description}${
|
|
100
|
+
return `- ${a.name}: ${a.description}${plugins}${skills}`
|
|
101
101
|
}).join('\n')
|
|
102
102
|
|
|
103
103
|
// Load relevant memories
|
|
@@ -291,7 +291,7 @@ async function executeSubTask(
|
|
|
291
291
|
sessionType: 'orchestrated' as const,
|
|
292
292
|
agentId: agent.id,
|
|
293
293
|
parentSessionId,
|
|
294
|
-
|
|
294
|
+
plugins: agent.plugins || agent.tools || [],
|
|
295
295
|
}
|
|
296
296
|
sessions[childId] = childSession
|
|
297
297
|
saveSessions(sessions)
|
|
@@ -339,7 +339,7 @@ export async function callProvider(
|
|
|
339
339
|
credentialId: agent.credentialId,
|
|
340
340
|
apiEndpoint: agent.apiEndpoint,
|
|
341
341
|
cwd: WORKSPACE_DIR,
|
|
342
|
-
|
|
342
|
+
plugins: agent.plugins || agent.tools || [],
|
|
343
343
|
messages: history.map((h) => ({
|
|
344
344
|
role: h.role as 'user' | 'assistant',
|
|
345
345
|
text: h.text,
|
|
@@ -3,6 +3,7 @@ import path from 'path'
|
|
|
3
3
|
import { createRequire } from 'module'
|
|
4
4
|
import type { Plugin, PluginHooks, PluginMeta, PluginToolDef, PluginUIExtension, PluginProviderExtension, PluginConnectorExtension, Session } from '@/types'
|
|
5
5
|
import { DATA_DIR } from './data-dir'
|
|
6
|
+
import { expandPluginIds } from './tool-aliases'
|
|
6
7
|
import { log } from './logger'
|
|
7
8
|
import { createNotification } from './create-notification'
|
|
8
9
|
import { notify } from './ws-hub'
|
|
@@ -160,6 +161,7 @@ function normalizePlugin(mod: unknown): Plugin | null {
|
|
|
160
161
|
'message:inbound': 'transformInboundMessage',
|
|
161
162
|
'message:outbound': 'transformOutboundMessage',
|
|
162
163
|
'command:new': 'beforeAgentStart',
|
|
164
|
+
'agent:context': 'getAgentContext',
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
const pluginLogger: PluginLogger = {
|
|
@@ -390,7 +392,8 @@ class PluginManager {
|
|
|
390
392
|
|
|
391
393
|
// 1. Load Built-ins
|
|
392
394
|
for (const [id, p] of this.builtins.entries()) {
|
|
393
|
-
const
|
|
395
|
+
const explicitConfig = config[id]
|
|
396
|
+
const isEnabled = explicitConfig != null ? explicitConfig.enabled !== false : p.enabledByDefault !== false
|
|
394
397
|
if (isEnabled) {
|
|
395
398
|
this.plugins.set(id, {
|
|
396
399
|
id,
|
|
@@ -579,6 +582,102 @@ class PluginManager {
|
|
|
579
582
|
return currentText
|
|
580
583
|
}
|
|
581
584
|
|
|
585
|
+
async collectAgentContext(session: import('@/types').Session, enabledPlugins: string[], message: string, history: import('@/types').Message[]): Promise<string[]> {
|
|
586
|
+
this.load()
|
|
587
|
+
const enabledSet = new Set(enabledPlugins)
|
|
588
|
+
const parts: string[] = []
|
|
589
|
+
|
|
590
|
+
for (const [id, p] of this.plugins.entries()) {
|
|
591
|
+
if (!enabledSet.has(id)) continue
|
|
592
|
+
const hook = p.hooks.getAgentContext
|
|
593
|
+
if (!hook) continue
|
|
594
|
+
try {
|
|
595
|
+
const result = await hook({ session, enabledPlugins, message, history })
|
|
596
|
+
if (typeof result === 'string' && result.trim()) {
|
|
597
|
+
parts.push(result)
|
|
598
|
+
this.markPluginSuccess(id)
|
|
599
|
+
}
|
|
600
|
+
} catch (err: unknown) {
|
|
601
|
+
log.error('plugins', 'getAgentContext hook failed', {
|
|
602
|
+
pluginId: id,
|
|
603
|
+
pluginName: p.meta.name,
|
|
604
|
+
error: err instanceof Error ? err.message : String(err),
|
|
605
|
+
})
|
|
606
|
+
this.markPluginFailure(id, 'hook.getAgentContext', err, true)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return parts
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Collect capability descriptions from all enabled plugins for system prompt */
|
|
614
|
+
collectCapabilityDescriptions(enabledPlugins: string[]): string[] {
|
|
615
|
+
this.load()
|
|
616
|
+
const enabledSet = new Set(expandPluginIds(enabledPlugins))
|
|
617
|
+
const lines: string[] = []
|
|
618
|
+
|
|
619
|
+
for (const [id, p] of this.plugins.entries()) {
|
|
620
|
+
if (!enabledSet.has(id)) continue
|
|
621
|
+
const hook = p.hooks.getCapabilityDescription
|
|
622
|
+
if (!hook) continue
|
|
623
|
+
try {
|
|
624
|
+
const result = hook()
|
|
625
|
+
if (typeof result === 'string' && result.trim()) {
|
|
626
|
+
lines.push(`- ${result}`)
|
|
627
|
+
}
|
|
628
|
+
} catch (err: unknown) {
|
|
629
|
+
log.error('plugins', 'getCapabilityDescription hook failed', { pluginId: id, error: err instanceof Error ? err.message : String(err) })
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return lines
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Collect operating guidance from all enabled plugins */
|
|
637
|
+
collectOperatingGuidance(enabledPlugins: string[]): string[] {
|
|
638
|
+
this.load()
|
|
639
|
+
const enabledSet = new Set(expandPluginIds(enabledPlugins))
|
|
640
|
+
const lines: string[] = []
|
|
641
|
+
|
|
642
|
+
for (const [id, p] of this.plugins.entries()) {
|
|
643
|
+
if (!enabledSet.has(id)) continue
|
|
644
|
+
const hook = p.hooks.getOperatingGuidance
|
|
645
|
+
if (!hook) continue
|
|
646
|
+
try {
|
|
647
|
+
const result = hook()
|
|
648
|
+
if (result === null || result === undefined) continue
|
|
649
|
+
if (typeof result === 'string' && result.trim()) {
|
|
650
|
+
lines.push(result)
|
|
651
|
+
} else if (Array.isArray(result)) {
|
|
652
|
+
for (const line of result) {
|
|
653
|
+
if (typeof line === 'string' && line.trim()) lines.push(line)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
} catch (err: unknown) {
|
|
657
|
+
log.error('plugins', 'getOperatingGuidance hook failed', { pluginId: id, error: err instanceof Error ? err.message : String(err) })
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return lines
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Collect all settings fields declared by enabled plugins */
|
|
665
|
+
collectSettingsFields(enabledPlugins: string[]): Array<{ pluginId: string; pluginName: string; fields: import('@/types').PluginSettingsField[] }> {
|
|
666
|
+
this.load()
|
|
667
|
+
const enabledSet = new Set(expandPluginIds(enabledPlugins))
|
|
668
|
+
const result: Array<{ pluginId: string; pluginName: string; fields: import('@/types').PluginSettingsField[] }> = []
|
|
669
|
+
|
|
670
|
+
for (const [id, p] of this.plugins.entries()) {
|
|
671
|
+
if (!enabledSet.has(id)) continue
|
|
672
|
+
const fields = p.ui?.settingsFields
|
|
673
|
+
if (fields?.length) {
|
|
674
|
+
result.push({ pluginId: id, pluginName: p.meta.name, fields })
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return result
|
|
679
|
+
}
|
|
680
|
+
|
|
582
681
|
recordExternalToolFailure(pluginId: string, toolName: string, err: unknown): void {
|
|
583
682
|
this.markPluginFailure(pluginId, `tool.${toolName}`, err, true)
|
|
584
683
|
}
|
|
@@ -589,7 +688,11 @@ class PluginManager {
|
|
|
589
688
|
|
|
590
689
|
isEnabled(filename: string): boolean {
|
|
591
690
|
const config = this.loadConfig()
|
|
592
|
-
|
|
691
|
+
const explicit = config[filename]
|
|
692
|
+
if (explicit != null) return explicit.enabled !== false
|
|
693
|
+
const builtin = this.builtins.get(filename)
|
|
694
|
+
if (builtin) return builtin.enabledByDefault !== false
|
|
695
|
+
return true
|
|
593
696
|
}
|
|
594
697
|
|
|
595
698
|
listPlugins(): PluginMeta[] {
|
|
@@ -599,25 +702,28 @@ class PluginManager {
|
|
|
599
702
|
const failures = this.readFailureState()
|
|
600
703
|
const metas: PluginMeta[] = []
|
|
601
704
|
|
|
602
|
-
const describeCapabilities = (loaded?: LoadedPlugin, fallback?: Plugin): Pick<PluginMeta, 'toolCount' | 'hookCount' | 'hasUI' | 'providerCount' | 'connectorCount'> => {
|
|
705
|
+
const describeCapabilities = (loaded?: LoadedPlugin, fallback?: Plugin): Pick<PluginMeta, 'toolCount' | 'hookCount' | 'hasUI' | 'providerCount' | 'connectorCount' | 'settingsFields'> => {
|
|
603
706
|
const tools = loaded?.tools || fallback?.tools || []
|
|
604
707
|
const hooks = loaded?.hooks || fallback?.hooks || {}
|
|
605
708
|
const providers = loaded?.providers || fallback?.providers || []
|
|
606
709
|
const connectors = loaded?.connectors || fallback?.connectors || []
|
|
607
710
|
const hasUi = !!(loaded?.ui || fallback?.ui)
|
|
711
|
+
const settingsFields = loaded?.ui?.settingsFields || fallback?.ui?.settingsFields
|
|
608
712
|
return {
|
|
609
713
|
toolCount: Array.isArray(tools) ? tools.length : 0,
|
|
610
714
|
hookCount: Object.values(hooks || {}).filter((fn) => typeof fn === 'function').length,
|
|
611
715
|
hasUI: hasUi,
|
|
612
716
|
providerCount: Array.isArray(providers) ? providers.length : 0,
|
|
613
717
|
connectorCount: Array.isArray(connectors) ? connectors.length : 0,
|
|
718
|
+
settingsFields: settingsFields?.length ? settingsFields : undefined,
|
|
614
719
|
}
|
|
615
720
|
}
|
|
616
721
|
|
|
617
722
|
// Add all builtins
|
|
618
723
|
for (const [id, p] of this.builtins.entries()) {
|
|
619
724
|
const loaded = this.plugins.get(id)
|
|
620
|
-
const
|
|
725
|
+
const explicitCfg = config[id]
|
|
726
|
+
const enabled = explicitCfg != null ? explicitCfg.enabled !== false : p.enabledByDefault !== false
|
|
621
727
|
const failure = failures[id]
|
|
622
728
|
const caps = describeCapabilities(loaded, p)
|
|
623
729
|
metas.push({
|
|
@@ -625,6 +731,7 @@ class PluginManager {
|
|
|
625
731
|
description: p.description || '',
|
|
626
732
|
filename: id,
|
|
627
733
|
enabled,
|
|
734
|
+
author: 'SwarmClaw',
|
|
628
735
|
version: (p as { version?: string }).version || loaded?.meta.version || '1.0.0',
|
|
629
736
|
source: loaded?.meta.source || 'local',
|
|
630
737
|
failureCount: failure?.count,
|
|
@@ -649,6 +756,7 @@ class PluginManager {
|
|
|
649
756
|
name: loaded?.meta.name || f.replace(/\.(js|mjs)$/, ''),
|
|
650
757
|
filename: f,
|
|
651
758
|
enabled,
|
|
759
|
+
author: loaded?.meta.author,
|
|
652
760
|
version: loaded?.meta.version || '0.0.1',
|
|
653
761
|
source: loaded?.meta.source || 'marketplace',
|
|
654
762
|
createdByAgentId: config[f]?.createdByAgentId || null,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'child_process'
|
|
2
2
|
|
|
3
|
-
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'
|
|
3
|
+
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
4
4
|
|
|
5
5
|
interface ProviderHealthState {
|
|
6
6
|
failures: number
|
|
@@ -66,12 +66,14 @@ export function isProviderCoolingDown(providerId: string): boolean {
|
|
|
66
66
|
function delegateBinary(delegateTool: DelegateTool): string {
|
|
67
67
|
if (delegateTool === 'delegate_to_claude_code') return 'claude'
|
|
68
68
|
if (delegateTool === 'delegate_to_codex_cli') return 'codex'
|
|
69
|
+
if (delegateTool === 'delegate_to_gemini_cli') return 'gemini'
|
|
69
70
|
return 'opencode'
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
function delegateProviderId(delegateTool: DelegateTool): string {
|
|
73
74
|
if (delegateTool === 'delegate_to_claude_code') return 'claude-cli'
|
|
74
75
|
if (delegateTool === 'delegate_to_codex_cli') return 'codex-cli'
|
|
76
|
+
if (delegateTool === 'delegate_to_gemini_cli') return 'gemini-cli'
|
|
75
77
|
return 'opencode-cli'
|
|
76
78
|
}
|
|
77
79
|
|
|
@@ -202,14 +204,14 @@ export async function pingOpenClaw(
|
|
|
202
204
|
|
|
203
205
|
/**
|
|
204
206
|
* Ping a provider to check reachability. Returns `{ ok, message }`.
|
|
205
|
-
* Skips CLI-based providers (claude-cli, codex-cli, opencode-cli) — returns ok.
|
|
207
|
+
* Skips CLI-based providers (claude-cli, codex-cli, opencode-cli, gemini-cli) — returns ok.
|
|
206
208
|
*/
|
|
207
209
|
export async function pingProvider(
|
|
208
210
|
provider: string,
|
|
209
211
|
apiKey: string | undefined,
|
|
210
212
|
endpoint: string | undefined,
|
|
211
213
|
): Promise<{ ok: boolean; message: string }> {
|
|
212
|
-
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli']
|
|
214
|
+
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli']
|
|
213
215
|
if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider — skipped.' }
|
|
214
216
|
|
|
215
217
|
try {
|