@swarmclawai/swarmclaw 1.2.5 → 1.2.8

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 (115) hide show
  1. package/README.md +24 -17
  2. package/next.config.ts +1 -0
  3. package/package.json +3 -2
  4. package/scripts/easy-setup.mjs +1 -1
  5. package/scripts/postinstall.mjs +1 -1
  6. package/skills/swarmclaw.md +115 -0
  7. package/skills/tools/browser.md +131 -0
  8. package/skills/tools/execute.md +98 -0
  9. package/skills/tools/files.md +98 -0
  10. package/skills/tools/memory.md +104 -0
  11. package/skills/tools/platform.md +144 -0
  12. package/skills/tools/skills.md +83 -0
  13. package/src/app/api/chats/[id]/messages/route.ts +23 -19
  14. package/src/app/api/chats/messages-route.test.ts +105 -51
  15. package/src/app/api/mcp-servers/[id]/test/route.ts +3 -2
  16. package/src/app/api/openclaw/deploy/route.ts +2 -0
  17. package/src/app/api/setup/check-provider/route.ts +10 -2
  18. package/src/app/api/setup/doctor/route.ts +4 -4
  19. package/src/components/agents/agent-chat-list.tsx +23 -1
  20. package/src/components/agents/inspector-panel.tsx +165 -48
  21. package/src/components/chat/chat-area.tsx +38 -9
  22. package/src/components/chat/message-list.tsx +33 -19
  23. package/src/components/gateways/gateway-sheet.tsx +5 -2
  24. package/src/lib/agent-execute-defaults.test.ts +24 -0
  25. package/src/lib/agent-execute-defaults.ts +62 -0
  26. package/src/lib/chat/queued-message-queue.test.ts +134 -1
  27. package/src/lib/chat/queued-message-queue.ts +77 -2
  28. package/src/lib/providers/index.test.ts +108 -0
  29. package/src/lib/providers/index.ts +38 -15
  30. package/src/lib/server/agents/agent-service.ts +5 -0
  31. package/src/lib/server/builtin-extensions.ts +1 -0
  32. package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +1 -1
  33. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +1 -0
  34. package/src/lib/server/chat-execution/chat-execution-utils.ts +2 -2
  35. package/src/lib/server/chat-execution/chat-turn-preparation.ts +79 -42
  36. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -0
  37. package/src/lib/server/chat-execution/continuation-evaluator.ts +8 -0
  38. package/src/lib/server/chat-execution/memory-mutation-tools.ts +1 -1
  39. package/src/lib/server/chat-execution/message-classifier.ts +11 -1
  40. package/src/lib/server/chat-execution/prompt-builder.test.ts +28 -0
  41. package/src/lib/server/chat-execution/prompt-builder.ts +14 -1
  42. package/src/lib/server/chat-execution/prompt-mode.test.ts +24 -0
  43. package/src/lib/server/chat-execution/prompt-mode.ts +5 -1
  44. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +6 -4
  45. package/src/lib/server/chat-execution/stream-agent-chat.ts +45 -16
  46. package/src/lib/server/chatrooms/chatroom-routing.test.ts +4 -0
  47. package/src/lib/server/connectors/discord.ts +2 -2
  48. package/src/lib/server/connectors/matrix.ts +3 -2
  49. package/src/lib/server/connectors/signal.ts +5 -4
  50. package/src/lib/server/connectors/slack.ts +10 -9
  51. package/src/lib/server/connectors/teams.ts +3 -2
  52. package/src/lib/server/connectors/telegram.ts +4 -4
  53. package/src/lib/server/connectors/whatsapp.ts +2 -2
  54. package/src/lib/server/daemon/controller.ts +7 -0
  55. package/src/lib/server/gateways/gateway-profile-service.ts +19 -1
  56. package/src/lib/server/messages/message-repository.test.ts +70 -0
  57. package/src/lib/server/messages/message-repository.ts +11 -6
  58. package/src/lib/server/openclaw/deploy.ts +32 -2
  59. package/src/lib/server/plugins-advanced.test.ts +1 -2
  60. package/src/lib/server/provider-health.ts +1 -1
  61. package/src/lib/server/runtime/process-manager.ts +13 -9
  62. package/src/lib/server/runtime/session-run-manager/queries.ts +15 -0
  63. package/src/lib/server/runtime/session-run-manager.test.ts +58 -0
  64. package/src/lib/server/sandbox/session-runtime.test.ts +18 -1
  65. package/src/lib/server/sandbox/session-runtime.ts +40 -28
  66. package/src/lib/server/session-tools/autonomy-tools.test.ts +7 -9
  67. package/src/lib/server/session-tools/context.ts +1 -1
  68. package/src/lib/server/session-tools/credential-env.ts +109 -0
  69. package/src/lib/server/session-tools/crud.ts +3 -3
  70. package/src/lib/server/session-tools/edit_file.ts +3 -2
  71. package/src/lib/server/session-tools/execute.test.ts +58 -0
  72. package/src/lib/server/session-tools/execute.ts +334 -0
  73. package/src/lib/server/session-tools/files-tool.ts +635 -0
  74. package/src/lib/server/session-tools/index.ts +14 -4
  75. package/src/lib/server/session-tools/memory-tool.ts +242 -0
  76. package/src/lib/server/session-tools/memory.ts +1 -1
  77. package/src/lib/server/session-tools/openclaw-nodes.ts +3 -2
  78. package/src/lib/server/session-tools/openclaw-workspace.ts +3 -2
  79. package/src/lib/server/session-tools/platform-tool.ts +617 -0
  80. package/src/lib/server/session-tools/session-info.ts +3 -2
  81. package/src/lib/server/session-tools/session-tools-wiring.test.ts +3 -4
  82. package/src/lib/server/session-tools/shell.ts +7 -122
  83. package/src/lib/server/session-tools/skills-tool.ts +396 -0
  84. package/src/lib/server/session-tools/web.ts +2 -2
  85. package/src/lib/server/storage-normalization.ts +2 -0
  86. package/src/lib/server/tool-aliases.ts +2 -1
  87. package/src/lib/server/tool-capability-policy-advanced.test.ts +9 -2
  88. package/src/lib/server/tool-capability-policy.test.ts +2 -1
  89. package/src/lib/server/tool-capability-policy.ts +60 -33
  90. package/src/lib/server/tool-planning.ts +11 -0
  91. package/src/lib/setup-defaults.ts +5 -0
  92. package/src/lib/tool-definitions.ts +1 -0
  93. package/src/lib/validation/schemas.test.ts +16 -0
  94. package/src/lib/validation/schemas.ts +16 -0
  95. package/src/stores/use-chat-store.test.ts +231 -0
  96. package/src/stores/use-chat-store.ts +62 -13
  97. package/src/types/agent.ts +348 -0
  98. package/src/types/app-settings.ts +175 -0
  99. package/src/types/approval.ts +27 -0
  100. package/src/types/connector.ts +187 -0
  101. package/src/types/extension.ts +386 -0
  102. package/src/types/index.ts +16 -3555
  103. package/src/types/message.ts +57 -0
  104. package/src/types/misc.ts +739 -0
  105. package/src/types/mission.ts +185 -0
  106. package/src/types/protocol.ts +422 -0
  107. package/src/types/provider.ts +52 -0
  108. package/src/types/run.ts +183 -0
  109. package/src/types/schedule.ts +59 -0
  110. package/src/types/session.ts +265 -0
  111. package/src/types/skill.ts +157 -0
  112. package/src/types/task.ts +140 -0
  113. package/src/types/working-state.ts +211 -0
  114. package/src/views/settings/section-heartbeat.tsx +2 -2
  115. package/src/lib/server/session-tools/sandbox.ts +0 -281
@@ -76,6 +76,7 @@ import {
76
76
  import { checkAgentBudgetLimits } from '@/lib/server/cost'
77
77
  import {
78
78
  classifyMessage,
79
+ type MessageClassification,
79
80
  toMessageSemanticsSummary,
80
81
  } from '@/lib/server/chat-execution/message-classifier'
81
82
  import {
@@ -87,6 +88,7 @@ import {
87
88
  } from '@/lib/server/chat-execution/chat-execution-utils'
88
89
  import { loadEstopState } from '@/lib/server/runtime/estop'
89
90
  import { buildToolSection, joinPromptSegments } from '@/lib/server/chat-execution/prompt-builder'
91
+ import { resolvePromptMode, type PromptMode } from '@/lib/server/chat-execution/prompt-mode'
90
92
  import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
91
93
  import type { ExecuteChatTurnInput } from '@/lib/server/chat-execution/chat-execution'
92
94
 
@@ -320,13 +322,17 @@ function buildLightHeartbeatSystemPrompt(session: Session): string | undefined {
320
322
  return parts.join('\n\n')
321
323
  }
322
324
 
323
- function buildAgentSystemPrompt(session: Session): string | undefined {
325
+ function buildAgentSystemPrompt(
326
+ session: Session,
327
+ options?: { lightweightDirectChat?: boolean },
328
+ ): string | undefined {
324
329
  if (!session.agentId) return undefined
325
330
  const agent = getAgent(session.agentId)
326
331
  if (!agent) return undefined
327
332
 
328
333
  const settings = loadSettings()
329
334
  const allowSilentReplies = isDirectConnectorSession(session)
335
+ const lightweightDirectChat = options?.lightweightDirectChat === true
330
336
  const parts: string[] = []
331
337
  const enabledExtensions = listUniversalToolAccessExtensionIds(
332
338
  getEnabledCapabilityIds(session).length > 0 ? getEnabledCapabilityIds(session) : getEnabledCapabilityIds(agent),
@@ -341,7 +347,7 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
341
347
  if (agent.description) identityLines.push(`Description: ${agent.description}`)
342
348
  identityLines.push('I should always embody this identity. I am not just an "Assistant" — I am a persistent companion with my own soul and presence.')
343
349
  parts.push(identityLines.join('\n'))
344
- const continuityBlock = buildIdentityContinuityContext(session, agent)
350
+ const continuityBlock = lightweightDirectChat ? null : buildIdentityContinuityContext(session, agent)
345
351
  if (continuityBlock) parts.push(continuityBlock)
346
352
 
347
353
  const runtimeLines = [
@@ -358,50 +364,57 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
358
364
  if (agent.soul) parts.push(`## Soul\n${agent.soul}`)
359
365
  if (agent.systemPrompt) parts.push(`## System Prompt\n${agent.systemPrompt}`)
360
366
 
361
- try {
362
- const runtimeSkills = resolveRuntimeSkills({
363
- cwd: session.cwd,
364
- enabledExtensions,
365
- agentId: agent.id,
366
- sessionId: session.id,
367
- userId: session.user,
368
- agentSkillIds: agent.skillIds || [],
369
- storedSkills: loadSkills(),
370
- selectedSkillId: session.skillRuntimeState?.selectedSkillId || null,
371
- })
372
- parts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
373
- } catch {
374
- // Runtime skills are non-critical during prompt assembly.
375
- }
367
+ if (!lightweightDirectChat) {
368
+ try {
369
+ const runtimeSkills = resolveRuntimeSkills({
370
+ cwd: session.cwd,
371
+ enabledExtensions,
372
+ agentId: agent.id,
373
+ sessionId: session.id,
374
+ userId: session.user,
375
+ agentSkillIds: agent.skillIds || [],
376
+ storedSkills: loadSkills(),
377
+ selectedSkillId: session.skillRuntimeState?.selectedSkillId || null,
378
+ })
379
+ parts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
380
+ } catch {
381
+ // Runtime skills are non-critical during prompt assembly.
382
+ }
376
383
 
377
- try {
378
- const wsCtx = buildWorkspaceContext({ cwd: session.cwd })
379
- if (wsCtx.block) parts.push(wsCtx.block)
380
- } catch {
381
- // Workspace context is non-critical.
384
+ try {
385
+ const wsCtx = buildWorkspaceContext({ cwd: session.cwd })
386
+ if (wsCtx.block) parts.push(wsCtx.block)
387
+ } catch {
388
+ // Workspace context is non-critical.
389
+ }
382
390
  }
383
391
 
384
392
  const thinkingHint = [
385
393
  '## Output Format',
386
394
  'If your model supports internal reasoning/thinking, put all internal analysis inside <think>...</think> tags.',
387
395
  'Your final response to the user should be clear and concise.',
396
+ ...(lightweightDirectChat
397
+ ? ['This is a lightweight direct chat turn. Reply naturally in 1-3 short sentences. Do not delegate, plan, or narrate tools unless the user adds a concrete task that needs that escalation.']
398
+ : []),
388
399
  allowSilentReplies
389
400
  ? 'When you truly have nothing to say, respond with ONLY: NO_MESSAGE'
390
401
  : 'For direct user chats, always send a visible reply. Never answer with NO_MESSAGE or HEARTBEAT_OK unless this is an explicit heartbeat poll.',
391
402
  ]
392
403
  parts.push(thinkingHint.join('\n'))
393
404
 
394
- if (enabledExtensions.length === 0) {
395
- parts.push(buildNoToolsGuidance().join('\n'))
396
- } else {
397
- parts.push(buildEnabledToolsAutonomyGuidance().join('\n'))
405
+ if (!lightweightDirectChat) {
406
+ if (enabledExtensions.length === 0) {
407
+ parts.push(buildNoToolsGuidance().join('\n'))
408
+ } else {
409
+ parts.push(buildEnabledToolsAutonomyGuidance().join('\n'))
410
+ }
411
+ const toolSectionLines = buildToolSection(enabledExtensions)
412
+ if (toolSectionLines.length > 0) parts.push(['## Tool Discipline', ...toolSectionLines].join('\n'))
413
+ const operatingGuidance = collectCapabilityOperatingGuidance(enabledExtensions)
414
+ if (operatingGuidance.length > 0) parts.push(['## Tool Guidance', ...operatingGuidance].join('\n'))
415
+ const capabilityLines = collectCapabilityDescriptions(enabledExtensions)
416
+ if (capabilityLines.length > 0) parts.push(['## Tool Capabilities', ...capabilityLines].join('\n'))
398
417
  }
399
- const toolSectionLines = buildToolSection(enabledExtensions)
400
- if (toolSectionLines.length > 0) parts.push(['## Tool Discipline', ...toolSectionLines].join('\n'))
401
- const operatingGuidance = collectCapabilityOperatingGuidance(enabledExtensions)
402
- if (operatingGuidance.length > 0) parts.push(['## Tool Guidance', ...operatingGuidance].join('\n'))
403
- const capabilityLines = collectCapabilityDescriptions(enabledExtensions)
404
- if (capabilityLines.length > 0) parts.push(['## Tool Capabilities', ...capabilityLines].join('\n'))
405
418
 
406
419
  parts.push([
407
420
  '## Heartbeats',
@@ -470,6 +483,8 @@ export interface PreparedExecutableChatTurn {
470
483
  runStartedAt: number
471
484
  runMessageStartIndex: number
472
485
  toolPolicy: ReturnType<typeof resolveSessionToolPolicy>
486
+ classification: MessageClassification | null
487
+ promptMode: PromptMode
473
488
  }
474
489
 
475
490
  export type PreparedChatTurn = PreparedBlockedChatTurn | PreparedExecutableChatTurn
@@ -620,6 +635,24 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
620
635
  }
621
636
  }
622
637
 
638
+ const turnHistory = getMessages(sessionId)
639
+ const classification = !internal
640
+ ? await classifyMessage({
641
+ sessionId,
642
+ agentId: session.agentId || null,
643
+ message,
644
+ history: turnHistory,
645
+ }).catch(() => null as MessageClassification | null)
646
+ : null
647
+ const lightweightDirectChat = classification?.isLightweightDirectChat === true
648
+ && !internal
649
+ && source === 'chat'
650
+ && !isDirectConnectorSession(sessionForRun)
651
+ const promptMode = resolvePromptMode(sessionForRun, { preferMinimalPrompt: lightweightDirectChat })
652
+ if (lightweightDirectChat && sessionForRun.thinkingLevel !== 'minimal') {
653
+ sessionForRun = { ...sessionForRun, thinkingLevel: 'minimal' }
654
+ }
655
+
623
656
  if (isHeartbeatRun && input.modelOverride) {
624
657
  sessionForRun = { ...sessionForRun, model: input.modelOverride }
625
658
  }
@@ -632,7 +665,10 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
632
665
  if (extensionsForRun.length > 0) {
633
666
  const modelResolvePrompt = heartbeatLightContext
634
667
  ? (joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), executionBriefContextBlock) || '')
635
- : (joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), executionBriefContextBlock) || '')
668
+ : (joinSystemPromptBlocks(
669
+ buildAgentSystemPrompt(sessionForRun, { lightweightDirectChat }),
670
+ executionBriefContextBlock,
671
+ ) || '')
636
672
  const modelResolve = await runCapabilityBeforeModelResolve(
637
673
  {
638
674
  session: sessionForRun,
@@ -724,14 +760,7 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
724
760
  if (shouldPersistUserMessage) {
725
761
  const [linkAnalysis, semantics] = await Promise.all([
726
762
  !internal ? runLinkUnderstanding(message) : Promise.resolve([]),
727
- classifyMessage({
728
- sessionId,
729
- agentId: session.agentId || null,
730
- message,
731
- history: getMessages(sessionId),
732
- })
733
- .then((classification) => toMessageSemanticsSummary(classification))
734
- .catch(() => undefined),
763
+ Promise.resolve(toMessageSemanticsSummary(classification)),
735
764
  ])
736
765
  const guardedUserText = guardUntrustedText({
737
766
  text: message,
@@ -745,6 +774,7 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
745
774
  role: 'user',
746
775
  text: guardedUserText,
747
776
  time: Date.now(),
777
+ runId: lifecycleRunId,
748
778
  imagePath: imagePath || undefined,
749
779
  imageUrl: imageUrl || undefined,
750
780
  attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
@@ -805,7 +835,12 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
805
835
 
806
836
  const systemPrompt = heartbeatLightContext
807
837
  ? joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), executionBriefContextBlock)
808
- : (hasExtensions ? undefined : joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), executionBriefContextBlock))
838
+ : (hasExtensions
839
+ ? undefined
840
+ : joinSystemPromptBlocks(
841
+ buildAgentSystemPrompt(sessionForRun, { lightweightDirectChat }),
842
+ executionBriefContextBlock,
843
+ ))
809
844
 
810
845
  return {
811
846
  kind: 'ready',
@@ -837,5 +872,7 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
837
872
  runStartedAt,
838
873
  runMessageStartIndex,
839
874
  toolPolicy,
875
+ classification,
876
+ promptMode,
840
877
  }
841
878
  }
@@ -76,6 +76,8 @@ export async function executePreparedChatTurn(params: {
76
76
  isAutoRunNoHistory,
77
77
  executionBrief,
78
78
  executionBriefContextBlock,
79
+ classification,
80
+ promptMode,
79
81
  } = prepared
80
82
 
81
83
  const emit = partialPersistence.emit
@@ -151,6 +153,8 @@ export async function executePreparedChatTurn(params: {
151
153
  history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
152
154
  signal: abortController.signal,
153
155
  source,
156
+ classification,
157
+ promptMode,
154
158
  })
155
159
  fullResponse = result.finalResponse || result.fullText
156
160
  } else {
@@ -102,6 +102,13 @@ function checkUnfinishedToolCallsPending(ctx: ContinuationContext): Continuation
102
102
  return null
103
103
  }
104
104
 
105
+ function checkLightweightDirectChat(ctx: ContinuationContext): ContinuationDecision | null {
106
+ if (ctx.classification?.isLightweightDirectChat !== true) return null
107
+ if (!ctx.state.fullText.trim()) return null
108
+ if (ctx.state.hasToolCalls || ctx.state.streamedToolEvents.length > 0) return null
109
+ return { type: false, requiredToolReminderNames: [] }
110
+ }
111
+
105
112
  function checkLoopDetection(ctx: ContinuationContext): ContinuationDecision | null {
106
113
  const isToolFrequency = (ctx.state.loopDetectionTriggered?.detector === 'tool_frequency') || ctx.state.toolFrequencyBlocked
107
114
  if (!ctx.state.loopDetectionTriggered && !isToolFrequency) return null
@@ -412,6 +419,7 @@ export function evaluateContinuation(ctx: ContinuationContext): ContinuationDeci
412
419
  const checks = [
413
420
  checkUnfinishedToolCallsPending,
414
421
  checkLoopDetection,
422
+ checkLightweightDirectChat,
415
423
  checkCoordinatorDelegation,
416
424
  checkExecutionContinuation,
417
425
  checkRequiredTools,
@@ -112,7 +112,7 @@ export function shouldTerminateOnSuccessfulMemoryMutation(params: {
112
112
  : exactToolName === 'memory_update'
113
113
  ? 'update'
114
114
  : resolveToolAction(params.toolInput)
115
- if (action !== 'store' && action !== 'update') return false
115
+ if (action !== 'store' && action !== 'update' && action !== 'write') return false
116
116
  const output = extractSuggestions(params.toolOutput || '').clean.trim()
117
117
  if (!output || /^error[:\s]/i.test(output)) return false
118
118
  if (!/^(stored|updated) memory\b/i.test(output)) return false
@@ -31,6 +31,7 @@ export const MessageClassificationSchema = z.object({
31
31
  taskIntent: TaskIntentSchema,
32
32
  isDeliverableTask: z.boolean(),
33
33
  isBroadGoal: z.boolean(),
34
+ isLightweightDirectChat: z.boolean().optional().default(false),
34
35
  walletIntent: z.enum(['none', 'read_only', 'transactional']),
35
36
  hasHumanSignals: z.boolean(),
36
37
  hasSignificantEvent: z.boolean(),
@@ -47,6 +48,7 @@ export interface MessageClassification {
47
48
  taskIntent: MessageTaskIntent
48
49
  isDeliverableTask: boolean
49
50
  isBroadGoal: boolean
51
+ isLightweightDirectChat?: boolean
50
52
  walletIntent: 'none' | 'read_only' | 'transactional'
51
53
  hasHumanSignals: boolean
52
54
  hasSignificantEvent: boolean
@@ -102,6 +104,7 @@ function buildClassificationPrompt(message: string, recentHistory: string): stri
102
104
  '- taskIntent: The primary execution intent. Use exactly one of: "coding", "research", "browsing", "outreach", "scheduling", or "general". Choose "coding" for repo/code/build/debug/edit tasks. Choose "research" for gathering current info or synthesizing sources. Choose "browsing" for page navigation, rendered-page inspection, form work, or literal browser workflows. Choose "outreach" for sending/sharing/delivering updates to an external channel. Choose "scheduling" for reminders, recurring work, monitoring, or follow-up scheduling. Choose "general" when none of the above clearly fits.',
103
105
  '- isDeliverableTask (bool): The user wants a concrete artifact produced — a document, report, plan, proposal, landing page, dashboard, HTML file, markdown file, brief, copy, screenshots, or similar deliverable. NOT simple Q&A, code fixes, or single-command tasks.',
104
106
  '- isBroadGoal (bool): The message describes a broad, multi-step goal (50+ chars, no code blocks, no file paths, no numbered lists). Short questions ending with "?" are NOT broad goals.',
107
+ '- isLightweightDirectChat (bool): This is a low-signal direct chat turn that should get a natural lightweight reply, such as a greeting, acknowledgment, check-in, or simple social/direct question that does NOT require research, file work, planning, delegation, or tool execution.',
105
108
  '- walletIntent: "none" if no crypto/wallet/trading context. "read_only" if mentioning wallet/crypto but only for checking balances, viewing transactions, or research. "transactional" if the user wants to swap, trade, buy, sell, mint, claim, deposit, withdraw, bridge, or execute a transaction.',
106
109
  '- hasHumanSignals (bool): The message contains personal signals — preferences ("I prefer", "call me"), relationships ("my wife", "my partner", "my kid"), life events ("birthday", "wedding", "promotion", "moving", "graduation", "hospital"), or personal disclosures.',
107
110
  '- hasSignificantEvent (bool): The message mentions a notable life/work event or milestone (birthday, anniversary, wedding, graduation, promotion, new job, relocation, illness, funeral, travel, house, deadline, launch).',
@@ -115,13 +118,14 @@ function buildClassificationPrompt(message: string, recentHistory: string): stri
115
118
  '',
116
119
  'Rules:',
117
120
  '- Be conservative. When unsure, default to false/none/empty.',
121
+ '- Mark isLightweightDirectChat true only when a short natural reply is enough and escalating into planning, delegation, or tool execution would be unnecessary.',
118
122
  '- A message can be both a deliverable task AND a broad goal.',
119
123
  '- "walletIntent" should be "transactional" only if the user wants to execute a state-changing action, not just discuss crypto.',
120
124
  '- For "explicitToolRequests", only include tools the user explicitly mentions by name or clear synonym. Do not infer tool needs from the task type.',
121
125
  '- Prefer the most execution-relevant taskIntent. Example: "research this and send me a voice note" is "research", not "outreach".',
122
126
  '',
123
127
  'Output shape:',
124
- '{"taskIntent":"coding|research|browsing|outreach|scheduling|general","isDeliverableTask":bool,"isBroadGoal":bool,"walletIntent":"none|read_only|transactional","hasHumanSignals":bool,"hasSignificantEvent":bool,"isResearchSynthesis":bool,"workType":"coding|research|writing|review|operations|general","wantsScreenshots":bool,"wantsOutboundDelivery":bool,"wantsVoiceDelivery":bool,"explicitToolRequests":[],"confidence":0.0-1.0}',
128
+ '{"taskIntent":"coding|research|browsing|outreach|scheduling|general","isDeliverableTask":bool,"isBroadGoal":bool,"isLightweightDirectChat":bool,"walletIntent":"none|read_only|transactional","hasHumanSignals":bool,"hasSignificantEvent":bool,"isResearchSynthesis":bool,"workType":"coding|research|writing|review|operations|general","wantsScreenshots":bool,"wantsOutboundDelivery":bool,"wantsVoiceDelivery":bool,"explicitToolRequests":[],"confidence":0.0-1.0}',
125
129
  '',
126
130
  recentHistory ? `Recent context:\n${recentHistory}\n` : '',
127
131
  `User message: ${JSON.stringify(message)}`,
@@ -276,6 +280,7 @@ export function toMessageSemanticsSummary(classification: MessageClassification
276
280
  isDeliverableTask: classification.isDeliverableTask,
277
281
  isBroadGoal: classification.isBroadGoal,
278
282
  isResearchSynthesis: classification.isResearchSynthesis,
283
+ isLightweightDirectChat: classification.isLightweightDirectChat === true,
279
284
  hasHumanSignals: classification.hasHumanSignals,
280
285
  hasSignificantEvent: classification.hasSignificantEvent,
281
286
  wantsScreenshots: classification.wantsScreenshots === true,
@@ -324,3 +329,8 @@ export function isResearchSynthesis(classification: MessageClassification | null
324
329
  void routingIntent
325
330
  return classification?.isResearchSynthesis === true
326
331
  }
332
+
333
+ export function isLightweightDirectChat(classification: MessageClassification | null, message?: string): boolean {
334
+ void message
335
+ return classification?.isLightweightDirectChat === true
336
+ }
@@ -26,4 +26,32 @@ describe('buildAgenticExecutionPolicy', () => {
26
26
  assert.ok(prompt.includes('use the concrete tool now'))
27
27
  assert.ok(prompt.includes('prefer the direct `manage_*` tool'))
28
28
  })
29
+
30
+ it('adds lightweight direct-chat guidance when classification marks the turn as lightweight', () => {
31
+ const prompt = buildAgenticExecutionPolicy({
32
+ enabledExtensions: ['memory', 'files', 'delegate'],
33
+ loopMode: 'bounded',
34
+ heartbeatPrompt: 'HEARTBEAT',
35
+ heartbeatIntervalSec: 120,
36
+ userMessage: 'Hello',
37
+ history: [],
38
+ classification: {
39
+ taskIntent: 'general',
40
+ isDeliverableTask: false,
41
+ isBroadGoal: false,
42
+ isLightweightDirectChat: true,
43
+ walletIntent: 'none',
44
+ hasHumanSignals: false,
45
+ hasSignificantEvent: false,
46
+ isResearchSynthesis: false,
47
+ workType: 'general',
48
+ explicitToolRequests: [],
49
+ confidence: 0.98,
50
+ },
51
+ })
52
+
53
+ assert.ok(prompt.includes('## Lightweight Chat'))
54
+ assert.ok(prompt.includes('Reply naturally and briefly.'))
55
+ assert.ok(prompt.includes('prefer 1-3 short sentences'))
56
+ })
29
57
  })
@@ -75,7 +75,6 @@ function buildExtensionCapabilityLines(enabledExtensions: string[], opts?: { del
75
75
 
76
76
  const DISPLAY_TOOL_ALIASES: Record<string, string[]> = {
77
77
  files: ['send_file'],
78
- shell: ['sandbox_exec', 'sandbox_list_runtimes'],
79
78
  }
80
79
 
81
80
  function buildExactToolNameList(enabledExtensions: string[]): string[] {
@@ -158,6 +157,7 @@ export function buildToolDisciplineLines(enabledExtensions: string[]): string[]
158
157
  ...(researchSearchTools.length || researchFetchTools.length ? [...researchSearchTools, ...researchFetchTools] : []),
159
158
  ...httpTools,
160
159
  ...(uniqueTools.includes('shell') ? ['shell'] : []),
160
+ ...(uniqueTools.includes('execute') ? ['execute'] : []),
161
161
  ...(uniqueTools.includes('browser') ? ['browser'] : []),
162
162
  ]))
163
163
  if (alternateResearchTools.length >= 2) {
@@ -330,6 +330,7 @@ export function buildAgenticExecutionPolicy(opts: {
330
330
  const hasManageSessions = opts.enabledExtensions.some((toolId) => (canonicalizeExtensionId(toolId) || toolId) === 'manage_sessions')
331
331
  const hasManageTasks = opts.enabledExtensions.some((toolId) => (canonicalizeExtensionId(toolId) || toolId) === 'manage_tasks')
332
332
  const hasManageSkills = opts.enabledExtensions.some((toolId) => (canonicalizeExtensionId(toolId) || toolId) === 'manage_skills')
333
+ const lightweightDirectChat = opts.classification?.isLightweightDirectChat === true && !opts.isDirectConnectorSession
333
334
  const hasDelegationTools = opts.enabledExtensions.some((toolId) => {
334
335
  const canonical = canonicalizeExtensionId(toolId) || toolId
335
336
  return canonical === 'delegate' || canonical === 'spawn_subagent'
@@ -359,6 +360,15 @@ export function buildAgenticExecutionPolicy(opts: {
359
360
  : 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
360
361
  )
361
362
 
363
+ if (lightweightDirectChat) {
364
+ parts.push(
365
+ '## Lightweight Chat',
366
+ 'This turn is a lightweight direct chat. Reply naturally and briefly.',
367
+ 'Do not delegate, create tasks, outline a workflow, or narrate tools unless the user adds a concrete task that actually requires that escalation.',
368
+ 'For greetings, acknowledgements, and simple social questions, a short human-sounding answer is sufficient.',
369
+ )
370
+ }
371
+
362
372
  if (hasTooling) {
363
373
  parts.push(
364
374
  '## Routing Matrix',
@@ -444,6 +454,9 @@ export function buildAgenticExecutionPolicy(opts: {
444
454
  ]),
445
455
  'Keep responses concise. Bullet points over prose. After file operations, confirm the result briefly (path and status) without echoing the full file contents.',
446
456
  'Do not end every reply with a question. Only ask when a specific missing detail blocks progress. When a task is done, state the result and stop.',
457
+ ...(lightweightDirectChat
458
+ ? ['For this turn, prefer 1-3 short sentences over bullets, planning, or process narration.']
459
+ : []),
447
460
  opts.responseStyle === 'concise'
448
461
  ? `IMPORTANT: Be extremely concise.${opts.responseMaxChars ? ` Keep responses under ${opts.responseMaxChars} characters.` : ' Target under 500 characters.'} Lead with the answer, skip preamble.`
449
462
  : opts.responseStyle === 'detailed'
@@ -0,0 +1,24 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { resolvePromptMode } from '@/lib/server/chat-execution/prompt-mode'
5
+
6
+ describe('resolvePromptMode', () => {
7
+ it('returns full for root sessions by default', () => {
8
+ assert.equal(resolvePromptMode({ id: 'root' } as never), 'full')
9
+ })
10
+
11
+ it('prefers minimal mode for lightweight direct-chat turns', () => {
12
+ assert.equal(
13
+ resolvePromptMode({ id: 'root' } as never, { preferMinimalPrompt: true }),
14
+ 'minimal',
15
+ )
16
+ })
17
+
18
+ it('keeps delegated child sessions in minimal mode', () => {
19
+ assert.equal(
20
+ resolvePromptMode({ id: 'child', parentSessionId: 'parent' } as never, { preferMinimalPrompt: false }),
21
+ 'minimal',
22
+ )
23
+ })
24
+ })
@@ -20,7 +20,11 @@ export type PromptMode = 'full' | 'minimal' | 'none'
20
20
  * proactive memory, thinking guidance
21
21
  * - `none` — reserved for bare identity (light heartbeat path)
22
22
  */
23
- export function resolvePromptMode(session: Session): PromptMode {
23
+ export function resolvePromptMode(
24
+ session: Session,
25
+ options?: { preferMinimalPrompt?: boolean },
26
+ ): PromptMode {
24
27
  if (session.parentSessionId) return 'minimal'
28
+ if (options?.preferMinimalPrompt) return 'minimal'
25
29
  return 'full'
26
30
  }
@@ -65,14 +65,13 @@ const streamContinuationSource = _readSibling('stream-continuation.ts')
65
65
  const streamSources = `${streamAgentChatSource}\n${streamContinuationSource}`
66
66
 
67
67
  describe('buildToolDisciplineLines', () => {
68
- it('lists exact callable tool names for extension families like sandbox and browser', () => {
68
+ it('lists exact callable tool names for legacy sandbox aliases and browser', () => {
69
69
  const lines = buildToolAvailabilityLines(['sandbox', 'browser', 'manage_schedules'])
70
70
 
71
71
  assert.equal(lines[0], 'Tool names are case-sensitive. Call tools exactly as listed.')
72
72
  assert.ok(lines.includes('- `browser`'))
73
+ assert.ok(lines.includes('- `execute`'))
73
74
  assert.ok(lines.includes('- `manage_schedules`'))
74
- assert.ok(lines.includes('- `sandbox_exec`'))
75
- assert.ok(lines.includes('- `sandbox_list_runtimes`'))
76
75
  })
77
76
 
78
77
  it('tells the agent to use direct platform tools when manage_platform is absent', () => {
@@ -1033,7 +1032,7 @@ describe('shouldForceDeliverableFollowthrough', () => {
1033
1032
  { name: 'web', input: '{"action":"fetch","url":"https://example.com/topic"}', output: '<html>topic</html>' },
1034
1033
  ],
1035
1034
  history: [
1036
- { role: 'user', text: 'Research 3 topics, take screenshots, write markdown and PDF files, then build a site for each topic.' },
1035
+ { role: 'user', text: 'Research 3 topics, take screenshots, write markdown and PDF files, then build a site for each topic.', time: Date.now() },
1037
1036
  ],
1038
1037
  }),
1039
1038
  true,
@@ -1275,6 +1274,7 @@ describe('parseClassificationResponse', () => {
1275
1274
 
1276
1275
  describe('message classifier adapter functions', () => {
1277
1276
  const deliverableClassification: MessageClassification = {
1277
+ taskIntent: 'general',
1278
1278
  isDeliverableTask: true,
1279
1279
  isBroadGoal: true,
1280
1280
  walletIntent: 'none',
@@ -1286,6 +1286,7 @@ describe('message classifier adapter functions', () => {
1286
1286
  }
1287
1287
 
1288
1288
  const walletClassification: MessageClassification = {
1289
+ taskIntent: 'general',
1289
1290
  isDeliverableTask: false,
1290
1291
  isBroadGoal: false,
1291
1292
  walletIntent: 'transactional',
@@ -1297,6 +1298,7 @@ describe('message classifier adapter functions', () => {
1297
1298
  }
1298
1299
 
1299
1300
  const humanSignalClassification: MessageClassification = {
1301
+ taskIntent: 'general',
1300
1302
  isDeliverableTask: false,
1301
1303
  isBroadGoal: false,
1302
1304
  walletIntent: 'none',
@@ -190,6 +190,7 @@ interface StreamAgentChatOpts {
190
190
  fallbackCredentialIds?: string[]
191
191
  signal?: AbortSignal
192
192
  promptMode?: PromptMode
193
+ classification?: MessageClassification | null
193
194
  /** Run source (e.g. 'heartbeat', 'chat', 'scheduler') — used for heartbeat-specific tuning. */
194
195
  source?: string
195
196
  }
@@ -223,10 +224,23 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
223
224
 
224
225
  async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
225
226
  const startTs = Date.now()
226
- const { session, message, imagePath, imageUrl, attachedFiles, apiKey, systemPrompt, executionBrief, extraSystemContext, write, history, fallbackCredentialIds, signal } = opts
227
+ const {
228
+ session,
229
+ message,
230
+ imagePath,
231
+ imageUrl,
232
+ attachedFiles,
233
+ apiKey,
234
+ systemPrompt,
235
+ executionBrief,
236
+ extraSystemContext,
237
+ write,
238
+ history,
239
+ fallbackCredentialIds,
240
+ signal,
241
+ classification: providedClassification,
242
+ } = opts
227
243
  const isHeartbeat = isHeartbeatSource(opts.source)
228
- const promptMode: PromptMode = opts.promptMode ?? resolvePromptMode(session)
229
- const isMinimalPrompt = promptMode === 'minimal'
230
244
  const isConnectorSession = isDirectConnectorSession(session)
231
245
  const rawExtensions = getEnabledCapabilityIds(session)
232
246
  const hasShellCapability = rawExtensions.some((toolId) => ['shell', 'execute_command'].includes(String(toolId)))
@@ -241,6 +255,23 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
241
255
 
242
256
  const sessionAgent = session.agentId ? getAgent(session.agentId) : null
243
257
 
258
+ const classificationPromise = providedClassification !== undefined
259
+ ? Promise.resolve(providedClassification)
260
+ : classifyMessage({
261
+ sessionId: session.id,
262
+ agentId: session.agentId,
263
+ message,
264
+ history,
265
+ }).catch(() => null as MessageClassification | null)
266
+ const classification = await classificationPromise
267
+ const lightweightDirectChat = classification?.isLightweightDirectChat === true
268
+ && !isConnectorSession
269
+ && !isHeartbeat
270
+ const promptMode: PromptMode = opts.promptMode ?? resolvePromptMode(session, {
271
+ preferMinimalPrompt: lightweightDirectChat,
272
+ })
273
+ const isMinimalPrompt = promptMode === 'minimal'
274
+
244
275
  // Resolve agent's thinking level for provider-native params
245
276
  let agentThinkingLevel: 'minimal' | 'low' | 'medium' | 'high' | undefined
246
277
  if (session.thinkingLevel) {
@@ -248,6 +279,9 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
248
279
  } else if (sessionAgent) {
249
280
  agentThinkingLevel = sessionAgent.thinkingLevel
250
281
  }
282
+ if (lightweightDirectChat) {
283
+ agentThinkingLevel = 'minimal'
284
+ }
251
285
 
252
286
  const llm = buildChatModel({
253
287
  provider: session.provider,
@@ -296,16 +330,6 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
296
330
  return Math.max(0, Math.min(3600, Math.trunc(parsed)))
297
331
  })()
298
332
 
299
- // -------------------------------------------------------------------------
300
- // Start message classification in the background (LLM-based, ~200-800ms)
301
- // -------------------------------------------------------------------------
302
- const classificationPromise = classifyMessage({
303
- sessionId: session.id,
304
- agentId: session.agentId,
305
- message,
306
- history,
307
- }).catch(() => null as MessageClassification | null)
308
-
309
333
  // -------------------------------------------------------------------------
310
334
  // System prompt assembly (stays inline — many async calls + local state)
311
335
  // -------------------------------------------------------------------------
@@ -426,8 +450,6 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
426
450
  const suggestionsBlock = buildSuggestionsSection(settings.suggestionsEnabled, isMinimalPrompt)
427
451
  if (suggestionsBlock) promptParts.push(suggestionsBlock)
428
452
 
429
- // Await classification before building the agentic execution policy
430
- const classification = await classificationPromise
431
453
  const delegationAdvisory = sessionAgent && agentDelegationEnabled
432
454
  ? resolveDelegationAdvisory({
433
455
  currentAgent: sessionAgent,
@@ -1138,7 +1160,14 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
1138
1160
  }
1139
1161
 
1140
1162
  // Async LLM-based incomplete-action check: catches "I'll run the deployment:" with no tool calls
1141
- if (!shouldContinue && outcome && !state.hasToolCalls && state.fullText.trim().length > 0 && state.fullText.trim().length < 500) {
1163
+ if (
1164
+ !shouldContinue
1165
+ && outcome
1166
+ && !state.hasToolCalls
1167
+ && classification?.isLightweightDirectChat !== true
1168
+ && state.fullText.trim().length > 0
1169
+ && state.fullText.trim().length < 500
1170
+ ) {
1142
1171
  const completeness = await evaluateResponseCompleteness({
1143
1172
  sessionId: session.id,
1144
1173
  agentId: session.agentId,
@@ -16,6 +16,8 @@ const agents: Record<string, Agent> = {
16
16
  model: 'gpt-test',
17
17
  systemPrompt: '',
18
18
  capabilities: ['deploy', 'infrastructure'],
19
+ createdAt: Date.now(),
20
+ updatedAt: Date.now(),
19
21
  },
20
22
  design: {
21
23
  id: 'design',
@@ -25,6 +27,8 @@ const agents: Record<string, Agent> = {
25
27
  model: 'gpt-test',
26
28
  systemPrompt: '',
27
29
  capabilities: ['design', 'ui'],
30
+ createdAt: Date.now(),
31
+ updatedAt: Date.now(),
28
32
  },
29
33
  }
30
34
 
@@ -217,8 +217,8 @@ const discord: PlatformConnector = {
217
217
  return String(sent.id || '')
218
218
  },
219
219
  })
220
- } catch (err: any) {
221
- log.error(TAG, 'Error handling message:', err.message)
220
+ } catch (err: unknown) {
221
+ log.error(TAG, 'Error handling message:', errorMessage(err))
222
222
  try {
223
223
  await message.reply('Sorry, I encountered an error processing your message.')
224
224
  } catch { /* ignore */ }