@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.
Files changed (119) hide show
  1. package/README.md +85 -139
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/thread/route.ts +1 -2
  4. package/src/app/api/agents/route.ts +1 -1
  5. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  6. package/src/app/api/{sessions → chats}/[id]/main-loop/route.ts +2 -2
  7. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  8. package/src/app/api/{sessions → chats}/[id]/route.ts +4 -52
  9. package/src/app/api/{sessions → chats}/route.ts +5 -7
  10. package/src/app/api/plugins/route.ts +3 -0
  11. package/src/app/api/plugins/settings/route.ts +35 -0
  12. package/src/app/api/usage/route.ts +30 -0
  13. package/src/cli/index.js +35 -33
  14. package/src/cli/index.ts +40 -39
  15. package/src/cli/spec.js +29 -27
  16. package/src/components/agents/agent-card.tsx +1 -1
  17. package/src/components/agents/agent-chat-list.tsx +3 -3
  18. package/src/components/agents/agent-list.tsx +8 -13
  19. package/src/components/agents/agent-sheet.tsx +2 -2
  20. package/src/components/agents/cron-job-form.tsx +3 -3
  21. package/src/components/agents/inspector-panel.tsx +2 -2
  22. package/src/components/auth/setup-wizard.tsx +5 -38
  23. package/src/components/chat/chat-area.tsx +10 -14
  24. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +3 -3
  25. package/src/components/chat/chat-header.tsx +156 -73
  26. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +4 -5
  27. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  28. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  29. package/src/components/chat/message-bubble.tsx +4 -1
  30. package/src/components/chat/message-list.tsx +2 -2
  31. package/src/components/{sessions/new-session-sheet.tsx → chat/new-chat-sheet.tsx} +6 -6
  32. package/src/components/chat/session-debug-panel.tsx +1 -1
  33. package/src/components/chat/tool-request-banner.tsx +3 -3
  34. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  35. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  36. package/src/components/connectors/connector-sheet.tsx +1 -1
  37. package/src/components/home/home-view.tsx +1 -1
  38. package/src/components/layout/app-layout.tsx +23 -2
  39. package/src/components/plugins/plugin-list.tsx +475 -254
  40. package/src/components/plugins/plugin-sheet.tsx +124 -10
  41. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  42. package/src/components/shared/command-palette.tsx +0 -1
  43. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  44. package/src/components/shared/settings/section-providers.tsx +1 -1
  45. package/src/components/shared/settings/settings-page.tsx +1 -12
  46. package/src/components/usage/metrics-dashboard.tsx +73 -0
  47. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  48. package/src/lib/chat.ts +1 -1
  49. package/src/lib/{sessions.ts → chats.ts} +28 -18
  50. package/src/lib/providers/claude-cli.ts +1 -1
  51. package/src/lib/server/approvals.ts +4 -4
  52. package/src/lib/server/capability-router.ts +10 -8
  53. package/src/lib/server/chat-execution.ts +36 -105
  54. package/src/lib/server/chatroom-helpers.ts +3 -3
  55. package/src/lib/server/connectors/manager.ts +4 -4
  56. package/src/lib/server/cost.ts +34 -1
  57. package/src/lib/server/daemon-state.ts +2 -2
  58. package/src/lib/server/heartbeat-service.ts +1 -1
  59. package/src/lib/server/main-agent-loop.ts +25 -160
  60. package/src/lib/server/main-session.ts +6 -13
  61. package/src/lib/server/orchestrator-lg.ts +3 -3
  62. package/src/lib/server/orchestrator.ts +5 -5
  63. package/src/lib/server/plugins.ts +112 -4
  64. package/src/lib/server/provider-health.ts +5 -3
  65. package/src/lib/server/queue.ts +12 -10
  66. package/src/lib/server/session-run-manager.test.ts +9 -6
  67. package/src/lib/server/session-run-manager.ts +1 -3
  68. package/src/lib/server/session-tools/calendar.ts +376 -0
  69. package/src/lib/server/session-tools/canvas.ts +1 -1
  70. package/src/lib/server/session-tools/chatroom.ts +4 -2
  71. package/src/lib/server/session-tools/connector.ts +5 -2
  72. package/src/lib/server/session-tools/context.ts +7 -3
  73. package/src/lib/server/session-tools/crud.ts +14 -6
  74. package/src/lib/server/session-tools/delegate.ts +95 -8
  75. package/src/lib/server/session-tools/discovery.ts +2 -2
  76. package/src/lib/server/session-tools/edit_file.ts +4 -2
  77. package/src/lib/server/session-tools/email.ts +322 -0
  78. package/src/lib/server/session-tools/file.ts +5 -2
  79. package/src/lib/server/session-tools/git.ts +1 -1
  80. package/src/lib/server/session-tools/http.ts +1 -1
  81. package/src/lib/server/session-tools/image-gen.ts +382 -0
  82. package/src/lib/server/session-tools/index.ts +74 -49
  83. package/src/lib/server/session-tools/memory.ts +139 -2
  84. package/src/lib/server/session-tools/monitor.ts +1 -1
  85. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  86. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  87. package/src/lib/server/session-tools/platform.ts +6 -3
  88. package/src/lib/server/session-tools/plugin-creator.ts +3 -3
  89. package/src/lib/server/session-tools/replicate.ts +303 -0
  90. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  91. package/src/lib/server/session-tools/sandbox.ts +4 -2
  92. package/src/lib/server/session-tools/schedule.ts +4 -2
  93. package/src/lib/server/session-tools/session-info.ts +7 -4
  94. package/src/lib/server/session-tools/shell.ts +5 -2
  95. package/src/lib/server/session-tools/subagent.ts +2 -2
  96. package/src/lib/server/session-tools/wallet.ts +29 -2
  97. package/src/lib/server/session-tools/web.ts +44 -5
  98. package/src/lib/server/storage.ts +29 -9
  99. package/src/lib/server/stream-agent-chat.ts +72 -249
  100. package/src/lib/server/tool-aliases.ts +26 -15
  101. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  102. package/src/lib/server/tool-capability-policy.ts +32 -27
  103. package/src/lib/tool-definitions.ts +4 -0
  104. package/src/lib/validation/schemas.ts +3 -1
  105. package/src/stores/use-app-store.ts +5 -5
  106. package/src/stores/use-chat-store.ts +7 -7
  107. package/src/types/index.ts +65 -3
  108. /package/src/app/api/{sessions → chats}/[id]/browser/route.ts +0 -0
  109. /package/src/app/api/{sessions → chats}/[id]/chat/route.ts +0 -0
  110. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  111. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  112. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  113. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  114. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  115. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  116. /package/src/app/api/{sessions → chats}/[id]/messages/route.ts +0 -0
  117. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  118. /package/src/app/api/{sessions → chats}/[id]/stop/route.ts +0 -0
  119. /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, loadTasks, saveTasks, loadSettings } from './storage'
4
+ import { loadSessions, saveSessions, loadAgents, saveAgents, loadSettings } from './storage'
5
5
  import { log } from './logger'
6
6
  import { getMemoryDb } from './memory-db'
7
- import { isProtectedMainSession } from './main-session'
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 (!Array.isArray(session?.tools) || !session.tools.includes('memory')) return
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?: { hasMemoryTool?: boolean; agent?: Record<string, unknown> | null; session?: Record<string, unknown> | null }): string {
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
- hasMemoryTool
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 isProtectedMainSession(session)
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 || inferGoalFromSessionMessages(session) || null
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
- hasMemoryTool
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, internal: boolean): 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 (!isProtectedMainSession(session)) return handleAgentHeartbeatResult(session, input)
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 (userGoal) {
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
- && (!!userGoal || !!userGoalContract?.objective)
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, { hasMemoryTool, agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
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, { hasMemoryTool, agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
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
- state.missionTaskId = upsertMissionTask(session, state, now)
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
- const MAIN_SESSION_NAME = '__main__'
2
-
3
- export function isProtectedMainSession(session: any): boolean {
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
- tools: agent.tools || [],
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 tools = a.tools?.length ? ` [tools: ${a.tools.join(', ')}]` : ''
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}${tools}${skills}`
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
- tools: Array.isArray(orchestrator.tools) ? [...orchestrator.tools] : [],
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 tools = a.tools?.length ? ` [tools: ${a.tools.join(', ')}]` : ''
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}${tools}${skills}`
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
- tools: agent.tools || [],
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
- tools: agent.tools || [],
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 isEnabled = config[id]?.enabled !== false
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
- return config[filename]?.enabled !== false
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 enabled = config[id]?.enabled !== false
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 {