@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
@@ -23,12 +23,12 @@ import { buildSessionTools } from './session-tools'
23
23
  import type { StructuredToolInterface } from '@langchain/core/tools'
24
24
  import type { Session } from '@/types'
25
25
  import { stripMainLoopMetaForPersistence } from './main-agent-loop'
26
+ import { getPluginManager } from './plugins'
26
27
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
27
- import { getMemoryDb } from './memory-db'
28
28
  import { routeTaskIntent } from './capability-router'
29
29
  import { notify } from './ws-hub'
30
30
  import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
31
- import { toolIdMatches } from './tool-aliases'
31
+ import { pluginIdMatches } from './tool-aliases'
32
32
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
33
33
  import {
34
34
  getCachedLlmResponse,
@@ -39,7 +39,7 @@ import {
39
39
  import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
40
40
  import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
41
41
  import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
42
- type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'
42
+ type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
43
43
 
44
44
  /** Slice history from the most recent context-clear marker forward */
45
45
  function applyContextClearBoundary(messages: Message[]): Message[] {
@@ -50,6 +50,8 @@ function applyContextClearBoundary(messages: Message[]): Message[] {
50
50
  }
51
51
 
52
52
  interface SessionWithTools {
53
+ plugins?: string[] | null
54
+ /** @deprecated Use plugins */
53
55
  tools?: string[] | null
54
56
  }
55
57
 
@@ -144,6 +146,7 @@ function requestedToolNamesFromMessage(message: string): string[] {
144
146
  'delegate_to_claude_code',
145
147
  'delegate_to_codex_cli',
146
148
  'delegate_to_opencode_cli',
149
+ 'delegate_to_gemini_cli',
147
150
  'connector_message_tool',
148
151
  'sessions_tool',
149
152
  'whoami_tool',
@@ -326,7 +329,7 @@ function extractDelegationTask(message: string, toolName: string): string | null
326
329
  }
327
330
 
328
331
  function hasToolEnabled(session: SessionWithTools, toolName: string): boolean {
329
- return toolIdMatches(session?.tools || [], toolName)
332
+ return pluginIdMatches(session?.plugins || session?.tools || [], toolName)
330
333
  }
331
334
 
332
335
  function enabledDelegationTools(session: SessionWithTools): DelegateTool[] {
@@ -334,6 +337,7 @@ function enabledDelegationTools(session: SessionWithTools): DelegateTool[] {
334
337
  if (hasToolEnabled(session, 'claude_code') || hasToolEnabled(session, 'delegate')) tools.push('delegate_to_claude_code')
335
338
  if (hasToolEnabled(session, 'codex_cli')) tools.push('delegate_to_codex_cli')
336
339
  if (hasToolEnabled(session, 'opencode_cli')) tools.push('delegate_to_opencode_cli')
340
+ if (hasToolEnabled(session, 'gemini_cli')) tools.push('delegate_to_gemini_cli')
337
341
  return tools
338
342
  }
339
343
 
@@ -401,8 +405,8 @@ function syncSessionFromAgent(sessionId: string): void {
401
405
  const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
402
406
  if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
403
407
  }
404
- if (!Array.isArray(session.tools)) {
405
- session.tools = Array.isArray(agent.tools) ? [...agent.tools] : []
408
+ if (!Array.isArray(session.plugins)) {
409
+ session.plugins = Array.isArray(agent.plugins) ? [...agent.plugins] : []
406
410
  changed = true
407
411
  }
408
412
 
@@ -529,75 +533,6 @@ function estimateConversationTone(text: string): string {
529
533
  return 'neutral'
530
534
  }
531
535
 
532
- const AUTO_MEMORY_MIN_INTERVAL_MS = 45 * 60 * 1000
533
-
534
- function normalizeMemoryText(value: string): string {
535
- return (value || '').replace(/\s+/g, ' ').trim()
536
- }
537
-
538
- function shouldStoreAutoMemoryNote(opts: {
539
- session: Session
540
- source: string
541
- internal: boolean
542
- message: string
543
- response: string
544
- now: number
545
- }): boolean {
546
- const { session, source, internal, message, response, now } = opts
547
- if (internal) return false
548
- if (source !== 'chat' && source !== 'connector') return false
549
- if (!session?.agentId) return false
550
- if (!Array.isArray(session.tools) || !session.tools.includes('memory')) return false
551
- const msg = (message || '').trim()
552
- const resp = (response || '').trim()
553
- if (msg.length < 20 || resp.length < 40) return false
554
- if (/^(ok|okay|cool|thanks|thx|got it|nice)[.! ]*$/i.test(msg)) return false
555
- if (resp === 'HEARTBEAT_OK') return false
556
- const last = typeof session.lastAutoMemoryAt === 'number' ? session.lastAutoMemoryAt : 0
557
- if (last > 0 && now - last < AUTO_MEMORY_MIN_INTERVAL_MS) return false
558
- return true
559
- }
560
-
561
- function storeAutoMemoryNote(opts: {
562
- session: Session
563
- message: string
564
- response: string
565
- source: string
566
- now: number
567
- }): string | null {
568
- const { session, message, response, source, now } = opts
569
- try {
570
- const db = getMemoryDb()
571
- const compactMessage = message.replace(/\s+/g, ' ').trim().slice(0, 220)
572
- const compactResponse = response.replace(/\s+/g, ' ').trim().slice(0, 700)
573
- const title = `[auto] ${compactMessage.slice(0, 90)}`
574
- const content = [
575
- `source: ${source}`,
576
- `user_request: ${compactMessage}`,
577
- `assistant_outcome: ${compactResponse}`,
578
- ].join('\n')
579
- const latest = db.getLatestBySessionCategory?.(session.id, 'execution')
580
- if (latest) {
581
- const sameTitle = normalizeMemoryText(latest.title) === normalizeMemoryText(title)
582
- const sameContent = normalizeMemoryText(latest.content) === normalizeMemoryText(content)
583
- if (sameTitle && sameContent) {
584
- session.lastAutoMemoryAt = now
585
- return latest.id
586
- }
587
- }
588
- const created = db.add({
589
- agentId: session.agentId as string,
590
- sessionId: session.id as string,
591
- category: 'execution',
592
- title,
593
- content,
594
- })
595
- session.lastAutoMemoryAt = now
596
- return created?.id || null
597
- } catch {
598
- return null
599
- }
600
- }
601
536
 
602
537
  export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promise<ExecuteChatTurnResult> {
603
538
  const { message } = input
@@ -620,29 +555,29 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
620
555
  if (!session) throw new Error(`Session not found: ${sessionId}`)
621
556
 
622
557
  const appSettings = loadSettings()
623
- const toolPolicy = resolveSessionToolPolicy(session.tools, appSettings)
558
+ const toolPolicy = resolveSessionToolPolicy(session.plugins, appSettings)
624
559
  const isHeartbeatRun = internal && source === 'heartbeat'
625
560
  const isAutoRunNoHistory = isHeartbeatRun || (internal && source === 'main-loop-followup')
626
561
  const heartbeatStatus = session.mainLoopState?.status || 'idle'
627
- const mainLoopIdle = session.name === '__main__'
562
+ const mainLoopIdle = session.id.startsWith('agent-thread-')
628
563
  && (heartbeatStatus === 'ok' || heartbeatStatus === 'idle')
629
564
  && !(session.mainLoopState?.pendingEvents?.length > 0)
630
565
  const heartbeatStatusOnly = isHeartbeatRun && mainLoopIdle
631
- const toolsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledTools
632
- let sessionForRun = toolsForRun === session.tools
566
+ const pluginsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledPlugins
567
+ let sessionForRun = pluginsForRun === session.plugins
633
568
  ? session
634
- : { ...session, tools: toolsForRun }
569
+ : { ...session, plugins: pluginsForRun }
635
570
 
636
571
  // Apply model override for heartbeat runs (cheaper model)
637
572
  if (isHeartbeatRun && input.modelOverride) {
638
573
  sessionForRun = { ...sessionForRun, model: input.modelOverride }
639
574
  }
640
575
 
641
- if (!heartbeatStatusOnly && toolPolicy.blockedTools.length > 0) {
642
- const blockedSummary = toolPolicy.blockedTools
576
+ if (!heartbeatStatusOnly && toolPolicy.blockedPlugins.length > 0) {
577
+ const blockedSummary = toolPolicy.blockedPlugins
643
578
  .map((entry) => `${entry.tool} (${entry.reason})`)
644
579
  .join(', ')
645
- onEvent?.({ t: 'err', text: `Capability policy blocked tools for this run: ${blockedSummary}` })
580
+ onEvent?.({ t: 'err', text: `Capability policy blocked plugins for this run: ${blockedSummary}` })
646
581
  }
647
582
 
648
583
  // --- Agent spend-limit enforcement (hourly/daily/monthly) ---
@@ -861,7 +796,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
861
796
  const responseCacheConfig = resolveLlmResponseCacheConfig(appSettings)
862
797
  let responseCacheHit = false
863
798
  let responseCacheInput: LlmResponseCacheKeyInput | null = null
864
- const hasTools = !!sessionForRun.tools?.length && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
799
+ const hasPlugins = !!(sessionForRun.plugins?.length || sessionForRun.tools?.length) && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
865
800
 
866
801
  let durationMs = 0
867
802
  const startTs = Date.now()
@@ -873,8 +808,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
873
808
  ? getSessionMessages(sessionId).slice(-6)
874
809
  : undefined
875
810
 
876
- console.log(`[chat-execution] provider=${providerType}, hasTools=${hasTools}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, tools=${(sessionForRun.tools || []).length}`)
877
- if (hasTools) {
811
+ console.log(`[chat-execution] provider=${providerType}, hasPlugins=${hasPlugins}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, plugins=${(sessionForRun.plugins || sessionForRun.tools || []).length}`)
812
+ if (hasPlugins) {
878
813
  fullResponse = (await streamAgentChat({
879
814
  session: sessionForRun,
880
815
  message: message,
@@ -967,7 +902,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
967
902
 
968
903
  // Record usage for the direct (non-tools) streamChat path.
969
904
  // streamAgentChat already calls appendUsage internally for the tools path.
970
- if (!hasTools && fullResponse && !errorMessage && !responseCacheHit) {
905
+ if (!hasPlugins && fullResponse && !errorMessage && !responseCacheHit) {
971
906
  const inputTokens = directUsage.received ? directUsage.inputTokens : Math.ceil(message.length / 4)
972
907
  const outputTokens = directUsage.received ? directUsage.outputTokens : Math.ceil(fullResponse.length / 4)
973
908
  const totalTokens = inputTokens + outputTokens
@@ -998,7 +933,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
998
933
  ? requestedToolNamesFromMessage(message)
999
934
  : []
1000
935
  const routingDecision = (!internal && source === 'chat')
1001
- ? routeTaskIntent(message, toolsForRun, appSettings)
936
+ ? routeTaskIntent(message, pluginsForRun, appSettings)
1002
937
  : null
1003
938
  const calledNames = new Set((toolEvents || []).map((t) => t.name))
1004
939
 
@@ -1034,6 +969,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1034
969
  if (requestedName === 'delegate_to_opencode_cli') {
1035
970
  return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
1036
971
  }
972
+ if (requestedName === 'delegate_to_gemini_cli') {
973
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
974
+ }
1037
975
 
1038
976
  const managePrefix = 'manage_'
1039
977
  if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
@@ -1065,7 +1003,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1065
1003
  return false
1066
1004
  }
1067
1005
  const agent = session.agentId ? loadAgents()[session.agentId] : null
1068
- const { tools, cleanup } = await buildSessionTools(session.cwd, sessionForRun.tools || [], {
1006
+ const { tools, cleanup } = await buildSessionTools(session.cwd, sessionForRun.plugins || sessionForRun.tools || [], {
1069
1007
  agentId: session.agentId || null,
1070
1008
  sessionId,
1071
1009
  platformAssignScope: agent?.platformAssignScope || 'self',
@@ -1109,10 +1047,11 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1109
1047
  }
1110
1048
  }
1111
1049
 
1112
- const forcedDelegationTools: Array<'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'> = [
1050
+ const forcedDelegationTools: DelegateTool[] = [
1113
1051
  'delegate_to_claude_code',
1114
1052
  'delegate_to_codex_cli',
1115
1053
  'delegate_to_opencode_cli',
1054
+ 'delegate_to_gemini_cli',
1116
1055
  ]
1117
1056
  for (const toolName of forcedDelegationTools) {
1118
1057
  if (!requestedToolNames.includes(toolName)) continue
@@ -1220,7 +1159,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1220
1159
  }
1221
1160
 
1222
1161
  const finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
1223
- const textForPersistence = stripMainLoopMetaForPersistence(finalText, internal)
1162
+ const textForPersistence = stripMainLoopMetaForPersistence(finalText)
1224
1163
 
1225
1164
  // Emit status SSE event from [MAIN_LOOP_META] if present
1226
1165
  if (internal && finalText) {
@@ -1391,24 +1330,16 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1391
1330
  }
1392
1331
  }
1393
1332
 
1394
- const autoMemoryEligible = shouldStoreAutoMemoryNote({
1395
- session: current,
1396
- source,
1397
- internal,
1398
- message: message,
1399
- response: textForPersistence,
1400
- now: Date.now(),
1401
- })
1402
- if (autoMemoryEligible) {
1403
- const storedId = storeAutoMemoryNote({
1333
+ // Fire afterChatTurn hook for all enabled plugins (memory auto-save, logging, etc.)
1334
+ try {
1335
+ await getPluginManager().runHook('afterChatTurn', {
1404
1336
  session: current,
1405
- message: message,
1337
+ message,
1406
1338
  response: textForPersistence,
1407
1339
  source,
1408
- now: Date.now(),
1340
+ internal,
1409
1341
  })
1410
- if (storedId) changed = true
1411
- }
1342
+ } catch { /* afterChatTurn hooks are non-critical */ }
1412
1343
 
1413
1344
  // Don't extend idle timeout for heartbeat runs — only user-initiated activity counts
1414
1345
  if (source !== 'heartbeat' && source !== 'heartbeat-wake' && source !== 'main-loop-followup') {
@@ -126,9 +126,9 @@ export function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<str
126
126
  .map((id) => {
127
127
  const a = agents[id]
128
128
  if (!a) return null
129
- const tools = a.tools?.length ? `Tools: ${a.tools.join(', ')}` : 'No specialized tools'
129
+ const plugins = (a.plugins || a.tools)?.length ? `Plugins: ${(a.plugins || a.tools)!.join(', ')}` : 'No specialized plugins'
130
130
  const desc = a.description || a.soul || 'No description'
131
- return `- **${a.name}**: ${desc}\n ${tools}`
131
+ return `- **${a.name}**: ${desc}\n ${plugins}`
132
132
  })
133
133
  .filter(Boolean)
134
134
  .join('\n')
@@ -187,7 +187,7 @@ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session
187
187
  messages: [],
188
188
  createdAt: Date.now(),
189
189
  lastActiveAt: Date.now(),
190
- tools: agent.tools || [],
190
+ plugins: agent.plugins || agent.tools || [],
191
191
  agentId: agent.id,
192
192
  }
193
193
  }
@@ -622,7 +622,7 @@ async function handleConnectorCommand(params: {
622
622
  const all = Array.isArray(session.messages) ? session.messages : []
623
623
  const userCount = all.filter((m: { role?: string }) => m?.role === 'user').length
624
624
  const assistantCount = all.filter((m: { role?: string }) => m?.role === 'assistant').length
625
- const toolsCount = Array.isArray(session.tools) ? session.tools.length : 0
625
+ const toolsCount = Array.isArray(session.plugins) ? session.plugins.length : 0
626
626
  const statusText = [
627
627
  `Status for ${connector.platform} / ${connector.name}:`,
628
628
  `- Agent: ${agentName}`,
@@ -645,7 +645,7 @@ async function handleConnectorCommand(params: {
645
645
  session.claudeSessionId = null
646
646
  session.codexThreadId = null
647
647
  session.opencodeSessionId = null
648
- session.delegateResumeIds = { claudeCode: null, codex: null, opencode: null }
648
+ session.delegateResumeIds = { claudeCode: null, codex: null, opencode: null, gemini: null }
649
649
  session.lastActiveAt = Date.now()
650
650
  persistSession(session)
651
651
  return `Reset complete for ${connector.platform} channel thread. Cleared ${cleared} message(s).`
@@ -1000,7 +1000,7 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
1000
1000
  lastActiveAt: Date.now(),
1001
1001
  sessionType: 'human' as const,
1002
1002
  agentId: agent.id,
1003
- tools: agent.tools || [],
1003
+ plugins: agent.plugins || agent.tools || [],
1004
1004
  }
1005
1005
  sessions[id] = session
1006
1006
  saveSessions(sessions)
@@ -1148,7 +1148,7 @@ If media sending fails, report the exact error and retry with a corrected path/t
1148
1148
  let fullText = ''
1149
1149
  let mediaExtractionText = ''
1150
1150
  let connectorToolDeliveredCurrentChannel = false
1151
- const hasTools = session.tools?.length && session.provider !== 'claude-cli'
1151
+ const hasTools = session.plugins?.length && session.provider !== 'claude-cli'
1152
1152
  console.log(`[connector] Routing message to agent "${agent.name}" (${agent.provider}/${agent.model}), hasTools=${!!hasTools}`)
1153
1153
 
1154
1154
  if (hasTools) {
@@ -1,4 +1,5 @@
1
- import type { Agent, UsageRecord } from '@/types'
1
+ import type { Agent, UsageRecord, PluginDefinitionCost } from '@/types'
2
+ import type { StructuredToolInterface } from '@langchain/core/tools'
2
3
  import { loadSessions, loadUsage } from './storage'
3
4
 
4
5
  // Model cost table: [inputCostPer1M, outputCostPer1M] in USD
@@ -65,6 +66,38 @@ export function getModelCosts(): Record<string, [number, number]> {
65
66
  return { ...MODEL_COSTS }
66
67
  }
67
68
 
69
+ /**
70
+ * Estimate the number of tokens a tool definition occupies in the LLM context.
71
+ * Uses ~4 chars per token as a rough approximation.
72
+ */
73
+ export function estimateToolDefinitionTokens(t: StructuredToolInterface): number {
74
+ let chars = (t.name || '').length + (t.description || '').length
75
+ try {
76
+ const schema = typeof t.schema === 'object' ? JSON.stringify(t.schema) : ''
77
+ chars += schema.length
78
+ } catch { /* ignore */ }
79
+ return Math.ceil(chars / 4)
80
+ }
81
+
82
+ /**
83
+ * Build per-plugin definition cost estimates from a set of tools and their plugin mapping.
84
+ */
85
+ export function buildPluginDefinitionCosts(
86
+ tools: StructuredToolInterface[],
87
+ toolToPluginMap: Record<string, string>,
88
+ ): PluginDefinitionCost[] {
89
+ const totals = new Map<string, number>()
90
+ for (const t of tools) {
91
+ const pluginId = toolToPluginMap[t.name] || '_unknown'
92
+ const tokens = estimateToolDefinitionTokens(t)
93
+ totals.set(pluginId, (totals.get(pluginId) || 0) + tokens)
94
+ }
95
+ return Array.from(totals.entries()).map(([pluginId, estimatedTokens]) => ({
96
+ pluginId,
97
+ estimatedTokens,
98
+ }))
99
+ }
100
+
68
101
  export interface AgentSpendWindows {
69
102
  hourly: number
70
103
  daily: number
@@ -413,14 +413,14 @@ async function processWebhookRetries() {
413
413
  claudeSessionId: null,
414
414
  codexThreadId: null,
415
415
  opencodeSessionId: null,
416
- delegateResumeIds: { claudeCode: null, codex: null, opencode: null },
416
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
417
417
  messages: [],
418
418
  createdAt: ts,
419
419
  lastActiveAt: ts,
420
420
  sessionType: 'orchestrated',
421
421
  agentId: agent.id,
422
422
  parentSessionId: null,
423
- tools: agent.tools || [],
423
+ plugins: agent.plugins || agent.tools || [],
424
424
  heartbeatEnabled: (agent.heartbeatEnabled as boolean | undefined) ?? true,
425
425
  heartbeatIntervalSec: (agent.heartbeatIntervalSec as number | null | undefined) ?? null,
426
426
  }
@@ -377,7 +377,7 @@ async function tickHeartbeats() {
377
377
 
378
378
  for (const session of Object.values(sessions) as any[]) {
379
379
  if (!session?.id) continue
380
- if (!Array.isArray(session.tools) || session.tools.length === 0) continue
380
+ if (!Array.isArray(session.plugins) || session.plugins.length === 0) continue
381
381
  if (session.sessionType && session.sessionType !== 'human' && session.sessionType !== 'orchestrated') continue
382
382
 
383
383
  // Check if this session or its agent has explicit heartbeat opt-in