@swarmclawai/swarmclaw 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -8
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/route.ts +4 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +51 -1
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +24 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +16 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +175 -95
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +14 -5
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +36 -2
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +56 -2
|
@@ -15,7 +15,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
|
|
|
15
15
|
|
|
16
16
|
tools.push(
|
|
17
17
|
tool(
|
|
18
|
-
async (
|
|
18
|
+
async (input) => {
|
|
19
|
+
const { action, key, value, category, query, scope, filePaths, references, project, imagePath, linkedMemoryIds, depth, linkedLimit, targetIds, tags, pinned, sharedWith } = input as Record<string, any>
|
|
19
20
|
try {
|
|
20
21
|
const scopeMode = scope || 'auto'
|
|
21
22
|
const currentAgentId = ctx?.agentId || null
|
|
@@ -98,6 +99,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
|
|
|
98
99
|
image: storedImage,
|
|
99
100
|
imagePath: storedImage?.path || undefined,
|
|
100
101
|
linkedMemoryIds,
|
|
102
|
+
pinned: pinned === true,
|
|
103
|
+
sharedWith: Array.isArray(sharedWith) ? sharedWith : undefined,
|
|
101
104
|
})
|
|
102
105
|
const memoryScope = entry.agentId ? 'agent' : 'shared'
|
|
103
106
|
let result = `Stored ${memoryScope} memory "${key}" (id: ${entry.id})`
|
|
@@ -220,6 +223,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
|
|
|
220
223
|
linkedLimit: z.number().optional().describe('Max linked memories expanded during traversal. Respects configured server cap.'),
|
|
221
224
|
targetIds: z.array(z.string()).optional().describe('Memory IDs to link/unlink (for link/unlink actions)'),
|
|
222
225
|
tags: z.array(z.string()).optional().describe('Tags for categorizing knowledge entries'),
|
|
226
|
+
pinned: z.boolean().optional().describe('Mark memory as pinned (always preloaded in agent context). For store action.'),
|
|
227
|
+
sharedWith: z.array(z.string()).optional().describe('Agent IDs to share this memory with (for store action). They can read it in their context.'),
|
|
223
228
|
}),
|
|
224
229
|
},
|
|
225
230
|
),
|
|
@@ -20,6 +20,7 @@ const DB_PATH = IS_BUILD_BOOTSTRAP ? ':memory:' : path.join(DATA_DIR, 'swarmclaw
|
|
|
20
20
|
const db = new Database(DB_PATH)
|
|
21
21
|
if (!IS_BUILD_BOOTSTRAP) {
|
|
22
22
|
db.pragma('journal_mode = WAL')
|
|
23
|
+
db.pragma('busy_timeout = 5000')
|
|
23
24
|
}
|
|
24
25
|
db.pragma('foreign_keys = ON')
|
|
25
26
|
|
|
@@ -52,6 +53,7 @@ const COLLECTIONS = [
|
|
|
52
53
|
'activity',
|
|
53
54
|
'webhook_retry_queue',
|
|
54
55
|
'notifications',
|
|
56
|
+
'chatrooms',
|
|
55
57
|
] as const
|
|
56
58
|
|
|
57
59
|
for (const table of COLLECTIONS) {
|
|
@@ -284,35 +286,37 @@ if (!IS_BUILD_BOOTSTRAP) {
|
|
|
284
286
|
description: 'A general-purpose AI assistant',
|
|
285
287
|
provider: 'claude-cli',
|
|
286
288
|
model: '',
|
|
287
|
-
systemPrompt: `You are the
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
- **
|
|
292
|
-
- **Providers
|
|
293
|
-
- **Tasks
|
|
294
|
-
- **Schedules
|
|
295
|
-
- **Skills
|
|
296
|
-
- **Connectors
|
|
297
|
-
- **Secrets
|
|
298
|
-
|
|
299
|
-
##
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
- **manage_agents**: List, create, update, or delete agents.
|
|
304
|
-
- **manage_tasks**: Create and manage task board items. Set
|
|
305
|
-
- **manage_schedules**: Create recurring or one-time scheduled jobs
|
|
306
|
-
- **manage_skills**:
|
|
307
|
-
- **manage_documents**: Upload
|
|
308
|
-
- **manage_webhooks**: Register
|
|
309
|
-
- **manage_connectors**: Manage chat platform bridges
|
|
310
|
-
- **manage_sessions**:
|
|
311
|
-
- **manage_secrets**: Store and retrieve encrypted
|
|
312
|
-
- **memory_tool**: Store and retrieve long-term
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
289
|
+
systemPrompt: `You are the SwarmClaw assistant. SwarmClaw is a self-hosted AI agent orchestration dashboard.
|
|
290
|
+
|
|
291
|
+
## Platform
|
|
292
|
+
|
|
293
|
+
- **Agents** — Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description. Toggle "Orchestrator" to let an agent delegate work to others.
|
|
294
|
+
- **Providers** — Configure LLM backends in Settings → Providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, OpenClaw, or custom OpenAI-compatible endpoints.
|
|
295
|
+
- **Tasks** — The Task Board tracks work items. Assign agents and they'll execute autonomously.
|
|
296
|
+
- **Schedules** — Cron-based recurring jobs that run agents or tasks automatically.
|
|
297
|
+
- **Skills** — Reusable markdown instruction files you attach to agents to specialize them.
|
|
298
|
+
- **Connectors** — Bridge agents to Discord, Slack, Telegram, or WhatsApp.
|
|
299
|
+
- **Secrets** — Encrypted vault for API keys (Settings → Secrets).
|
|
300
|
+
|
|
301
|
+
## Tools
|
|
302
|
+
|
|
303
|
+
Use your platform management tools proactively:
|
|
304
|
+
|
|
305
|
+
- **manage_agents**: List, create, update, or delete agents.
|
|
306
|
+
- **manage_tasks**: Create and manage task board items. Set status (backlog → queued → running → completed/failed) and assign agents.
|
|
307
|
+
- **manage_schedules**: Create recurring or one-time scheduled jobs with cron expressions or intervals.
|
|
308
|
+
- **manage_skills**: Manage reusable skill definitions.
|
|
309
|
+
- **manage_documents**: Upload, index, and search long-lived documents.
|
|
310
|
+
- **manage_webhooks**: Register webhook endpoints that trigger agent runs.
|
|
311
|
+
- **manage_connectors**: Manage chat platform bridges.
|
|
312
|
+
- **manage_sessions**: List chats, send inter-chat messages, spawn new agent chats.
|
|
313
|
+
- **manage_secrets**: Store and retrieve encrypted credentials.
|
|
314
|
+
- **memory_tool**: Store and retrieve long-term knowledge.`,
|
|
315
|
+
soul: `You're a knowledgeable, friendly guide who's genuinely enthusiastic about helping people build agent workflows. You adapt your tone to match the conversation — casual when exploring, precise when debugging, encouraging when learning.
|
|
316
|
+
|
|
317
|
+
You have opinions about good agent design. You suggest creative approaches, warn about common pitfalls, and get excited when someone gets something cool working. You're not a manual — you're a collaborator.
|
|
318
|
+
|
|
319
|
+
Be concise but not curt. Warmth doesn't require verbosity. When someone asks "how do I...?", give them the direct steps. Offer to do things rather than just explaining — if someone wants an agent created, create it. Use your tools when actions speak louder than words. If you don't know something, say so honestly.`,
|
|
316
320
|
isOrchestrator: false,
|
|
317
321
|
tools: defaultStarterTools,
|
|
318
322
|
heartbeatEnabled: true,
|
|
@@ -691,6 +695,15 @@ export function saveConnectors(c: Record<string, any>) {
|
|
|
691
695
|
saveCollection('connectors', c)
|
|
692
696
|
}
|
|
693
697
|
|
|
698
|
+
// --- Chatrooms ---
|
|
699
|
+
export function loadChatrooms(): Record<string, any> {
|
|
700
|
+
return loadCollection('chatrooms')
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function saveChatrooms(c: Record<string, any>) {
|
|
704
|
+
saveCollection('chatrooms', c)
|
|
705
|
+
}
|
|
706
|
+
|
|
694
707
|
// --- Documents ---
|
|
695
708
|
export function loadDocuments(): Record<string, any> {
|
|
696
709
|
return loadCollection('documents')
|
|
@@ -789,6 +802,17 @@ export function deleteNotification(id: string) {
|
|
|
789
802
|
deleteCollectionItem('notifications', id)
|
|
790
803
|
}
|
|
791
804
|
|
|
805
|
+
export function hasUnreadNotificationWithKey(dedupKey: string): boolean {
|
|
806
|
+
const raw = getCollectionRawCache('notifications')
|
|
807
|
+
for (const json of raw.values()) {
|
|
808
|
+
try {
|
|
809
|
+
const n = JSON.parse(json) as Record<string, unknown>
|
|
810
|
+
if (n.dedupKey === dedupKey && n.read !== true) return true
|
|
811
|
+
} catch { /* skip malformed */ }
|
|
812
|
+
}
|
|
813
|
+
return false
|
|
814
|
+
}
|
|
815
|
+
|
|
792
816
|
export function markNotificationRead(id: string) {
|
|
793
817
|
const raw = getCollectionRawCache('notifications')
|
|
794
818
|
const json = raw.get(id)
|
|
@@ -12,6 +12,20 @@ import { logExecution } from './execution-log'
|
|
|
12
12
|
import type { Session, Message, UsageRecord } from '@/types'
|
|
13
13
|
import { extractSuggestions } from './suggestions'
|
|
14
14
|
|
|
15
|
+
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
16
|
+
function extractBreadcrumbTitle(toolName: string, input: unknown, output: string | undefined): string | null {
|
|
17
|
+
if (!input || typeof input !== 'object') return null
|
|
18
|
+
const inp = input as Record<string, unknown>
|
|
19
|
+
const action = typeof inp.action === 'string' ? inp.action : ''
|
|
20
|
+
if (toolName === 'manage_tasks') {
|
|
21
|
+
if (action === 'create') return `Created task: ${inp.title || 'Untitled'}`
|
|
22
|
+
if (output && /status.*completed|completed.*successfully/i.test(output)) return `Completed task: ${inp.title || inp.taskId || 'unknown'}`
|
|
23
|
+
}
|
|
24
|
+
if (toolName === 'manage_schedules' && action === 'create') return `Created schedule: ${inp.name || 'Untitled'}`
|
|
25
|
+
if (toolName === 'manage_agents' && action === 'create') return `Created agent: ${inp.name || 'Untitled'}`
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
interface StreamAgentChatOpts {
|
|
16
30
|
session: Session
|
|
17
31
|
message: string
|
|
@@ -25,7 +39,7 @@ interface StreamAgentChatOpts {
|
|
|
25
39
|
signal?: AbortSignal
|
|
26
40
|
}
|
|
27
41
|
|
|
28
|
-
function buildToolCapabilityLines(enabledTools: string[]): string[] {
|
|
42
|
+
function buildToolCapabilityLines(enabledTools: string[], opts?: { platformAssignScope?: 'self' | 'all' }): string[] {
|
|
29
43
|
const lines: string[] = []
|
|
30
44
|
if (enabledTools.includes('shell')) lines.push('- Shell execution is available (`execute_command`). Use it for running servers, installing deps, running scripts, git commands, build/test steps, and any single or chained shell commands. Supports background mode for long-running processes like dev servers.')
|
|
31
45
|
if (enabledTools.includes('process')) lines.push('- Process control is available (`process_tool`) for long-running commands (poll/log/write/kill).')
|
|
@@ -52,9 +66,12 @@ function buildToolCapabilityLines(enabledTools: string[]): string[] {
|
|
|
52
66
|
// Context tools are available to any session with tools (not just manage_sessions)
|
|
53
67
|
if (enabledTools.length > 0) {
|
|
54
68
|
lines.push('- Context management is available (`context_status`, `context_summarize`). Use `context_status` to check token usage and `context_summarize` to compact conversation history when approaching limits.')
|
|
55
|
-
|
|
69
|
+
if (opts?.platformAssignScope === 'all') {
|
|
70
|
+
lines.push('- Agent delegation is available (`delegate_to_agent`). Use it to assign tasks to other agents based on their capabilities.')
|
|
71
|
+
}
|
|
56
72
|
}
|
|
57
73
|
if (enabledTools.includes('manage_secrets')) lines.push('- Secret management is available (`manage_secrets`) for durable encrypted credentials and API tokens.')
|
|
74
|
+
if (enabledTools.includes('manage_chatrooms')) lines.push('- Chatroom management is available (`manage_chatrooms`) for multi-agent collaborative chatrooms with @mention-based interactions.')
|
|
58
75
|
return lines
|
|
59
76
|
}
|
|
60
77
|
|
|
@@ -63,9 +80,10 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
63
80
|
loopMode: 'bounded' | 'ongoing'
|
|
64
81
|
heartbeatPrompt: string
|
|
65
82
|
heartbeatIntervalSec: number
|
|
83
|
+
platformAssignScope?: 'self' | 'all'
|
|
66
84
|
}) {
|
|
67
85
|
const hasTooling = opts.enabledTools.length > 0
|
|
68
|
-
const toolLines = buildToolCapabilityLines(opts.enabledTools)
|
|
86
|
+
const toolLines = buildToolCapabilityLines(opts.enabledTools, { platformAssignScope: opts.platformAssignScope })
|
|
69
87
|
const delegationOrder = [
|
|
70
88
|
opts.enabledTools.includes('claude_code') ? '`delegate_to_claude_code`' : null,
|
|
71
89
|
opts.enabledTools.includes('codex_cli') ? '`delegate_to_codex_cli`' : null,
|
|
@@ -260,57 +278,92 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
260
278
|
.map((h) => h.text),
|
|
261
279
|
].join('\n')
|
|
262
280
|
|
|
263
|
-
const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, session.agentId, 1, 10, 14)
|
|
264
|
-
const relevant = relevantLookup.entries.slice(0, 6)
|
|
265
|
-
const recent = memDb.list(session.agentId, 12).slice(0, 6)
|
|
266
|
-
|
|
267
281
|
const seen = new Set<string>()
|
|
268
|
-
const formatMemoryLine = (m:
|
|
282
|
+
const formatMemoryLine = (m: { category?: string; title?: string; content?: string; pinned?: boolean }) => {
|
|
269
283
|
const category = String(m.category || 'note')
|
|
270
284
|
const title = String(m.title || 'Untitled').replace(/\s+/g, ' ').trim()
|
|
271
285
|
const snippet = String(m.content || '').replace(/\s+/g, ' ').trim().slice(0, 220)
|
|
272
|
-
|
|
286
|
+
const pin = m.pinned ? ' [pinned]' : ''
|
|
287
|
+
return `- [${category}]${pin} ${title}: ${snippet}`
|
|
273
288
|
}
|
|
274
289
|
|
|
290
|
+
// Pinned memories always appear first
|
|
291
|
+
const pinned = memDb.listPinned(session.agentId, 5)
|
|
292
|
+
const pinnedLines = pinned
|
|
293
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
294
|
+
.map(formatMemoryLine)
|
|
295
|
+
|
|
296
|
+
// Reduce relevant slice by pinned count to keep total context bounded
|
|
297
|
+
const relevantSlice = Math.max(2, 6 - pinnedLines.length)
|
|
298
|
+
const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, session.agentId, 1, 10, 14)
|
|
299
|
+
const relevant = relevantLookup.entries.slice(0, relevantSlice)
|
|
300
|
+
const recent = memDb.list(session.agentId, 12).slice(0, 6)
|
|
301
|
+
|
|
275
302
|
const relevantLines = relevant
|
|
276
|
-
.filter((m) => {
|
|
277
|
-
if (!m?.id || seen.has(m.id)) return false
|
|
278
|
-
seen.add(m.id)
|
|
279
|
-
return true
|
|
280
|
-
})
|
|
303
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
281
304
|
.map(formatMemoryLine)
|
|
282
305
|
|
|
283
306
|
const recentLines = recent
|
|
284
|
-
.filter((m) => {
|
|
285
|
-
if (!m?.id || seen.has(m.id)) return false
|
|
286
|
-
seen.add(m.id)
|
|
287
|
-
return true
|
|
288
|
-
})
|
|
307
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
289
308
|
.map(formatMemoryLine)
|
|
290
309
|
|
|
291
310
|
const memorySections: string[] = []
|
|
311
|
+
if (pinnedLines.length) {
|
|
312
|
+
memorySections.push(
|
|
313
|
+
['## Pinned Memories', 'Always-loaded memories marked as important.', ...pinnedLines].join('\n'),
|
|
314
|
+
)
|
|
315
|
+
}
|
|
292
316
|
if (relevantLines.length) {
|
|
293
317
|
memorySections.push(
|
|
294
|
-
[
|
|
295
|
-
'## Relevant Memory Hits',
|
|
296
|
-
'These memories were retrieved by relevance for the current objective.',
|
|
297
|
-
...relevantLines,
|
|
298
|
-
].join('\n'),
|
|
318
|
+
['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'),
|
|
299
319
|
)
|
|
300
320
|
}
|
|
301
321
|
if (recentLines.length) {
|
|
302
322
|
memorySections.push(
|
|
303
|
-
[
|
|
304
|
-
'## Recent Memory Notes',
|
|
305
|
-
'Recent durable notes that may still apply.',
|
|
306
|
-
...recentLines,
|
|
307
|
-
].join('\n'),
|
|
323
|
+
['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'),
|
|
308
324
|
)
|
|
309
325
|
}
|
|
310
326
|
|
|
311
327
|
if (memorySections.length) {
|
|
312
328
|
stateModifierParts.push(memorySections.join('\n\n'))
|
|
313
329
|
}
|
|
330
|
+
|
|
331
|
+
// Memory Policy — always injected when memory tool is available
|
|
332
|
+
stateModifierParts.push([
|
|
333
|
+
'## Memory Policy',
|
|
334
|
+
'You have long-term memory. Use it proactively — do not wait to be asked.',
|
|
335
|
+
'',
|
|
336
|
+
'**Store memories for:**',
|
|
337
|
+
'- User preferences, corrections, or explicit "remember this" requests',
|
|
338
|
+
'- Key decisions or outcomes from complex tasks',
|
|
339
|
+
'- Discovered facts about projects, codebases, or environments',
|
|
340
|
+
'- Errors encountered and their solutions',
|
|
341
|
+
'- Relationship context (who is who, team dynamics)',
|
|
342
|
+
'- Important configuration details or environment specifics',
|
|
343
|
+
'',
|
|
344
|
+
'**Do NOT store:**',
|
|
345
|
+
'- Trivial acknowledgments or small talk',
|
|
346
|
+
'- Temporary in-progress work (use category "working" for ephemeral notes)',
|
|
347
|
+
'- Information already in your system prompt',
|
|
348
|
+
'- Exact duplicates of memories you already have',
|
|
349
|
+
'',
|
|
350
|
+
'**Best practices:**',
|
|
351
|
+
'- Use descriptive titles ("User prefers dark mode" not "Note 1")',
|
|
352
|
+
'- Use categories: preference, fact, learning, project, identity, decision',
|
|
353
|
+
'- Search memory before storing to avoid duplicates',
|
|
354
|
+
'- When correcting old knowledge, update or delete the old memory',
|
|
355
|
+
].join('\n'))
|
|
356
|
+
|
|
357
|
+
// Pre-compaction memory flush: nudge agent to persist learnings when conversation is long
|
|
358
|
+
const msgCount = history.filter(m => m.role === 'user' || m.role === 'assistant').length
|
|
359
|
+
if (msgCount > 20) {
|
|
360
|
+
stateModifierParts.push([
|
|
361
|
+
'## Memory Flush Reminder',
|
|
362
|
+
'This conversation is getting long. Before context is trimmed, store any important',
|
|
363
|
+
'learnings, decisions, or facts as memories now. Only store what is significant and durable —',
|
|
364
|
+
'skip trivial details. If nothing needs storing, continue normally.',
|
|
365
|
+
].join('\n'))
|
|
366
|
+
}
|
|
314
367
|
} catch {
|
|
315
368
|
// If memory context fails to load, continue without blocking the run.
|
|
316
369
|
}
|
|
@@ -363,6 +416,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
363
416
|
loopMode: runtime.loopMode,
|
|
364
417
|
heartbeatPrompt,
|
|
365
418
|
heartbeatIntervalSec,
|
|
419
|
+
platformAssignScope: agentPlatformAssignScope,
|
|
366
420
|
}),
|
|
367
421
|
)
|
|
368
422
|
|
|
@@ -463,20 +517,35 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
463
517
|
// Auto-compaction: prune old history if approaching context window limit
|
|
464
518
|
let effectiveHistory = history
|
|
465
519
|
try {
|
|
466
|
-
const { shouldAutoCompact,
|
|
520
|
+
const { shouldAutoCompact, llmCompact, estimateTokens } = await import('./context-manager')
|
|
467
521
|
const systemPromptTokens = estimateTokens(stateModifier)
|
|
468
522
|
if (shouldAutoCompact(history, systemPromptTokens, session.provider, session.model)) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
523
|
+
const summarize = async (prompt: string): Promise<string> => {
|
|
524
|
+
const response = await llm.invoke([new HumanMessage(prompt)])
|
|
525
|
+
if (typeof response.content === 'string') return response.content
|
|
526
|
+
if (Array.isArray(response.content)) {
|
|
527
|
+
return response.content
|
|
528
|
+
.map((b: Record<string, unknown>) => (typeof b.text === 'string' ? b.text : ''))
|
|
529
|
+
.join('')
|
|
530
|
+
}
|
|
531
|
+
return ''
|
|
473
532
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
533
|
+
const result = await llmCompact({
|
|
534
|
+
messages: history,
|
|
535
|
+
provider: session.provider,
|
|
536
|
+
model: session.model,
|
|
537
|
+
agentId: session.agentId || null,
|
|
538
|
+
sessionId: session.id,
|
|
539
|
+
summarize,
|
|
540
|
+
})
|
|
541
|
+
effectiveHistory = result.messages
|
|
542
|
+
console.log(
|
|
543
|
+
`[stream-agent-chat] Auto-compacted ${session.id}: ${history.length} → ${effectiveHistory.length} msgs` +
|
|
544
|
+
(result.summaryAdded ? ' (LLM summary)' : ' (sliding window fallback)'),
|
|
545
|
+
)
|
|
477
546
|
}
|
|
478
547
|
} catch {
|
|
479
|
-
//
|
|
548
|
+
// Context manager failure — continue with full history
|
|
480
549
|
}
|
|
481
550
|
|
|
482
551
|
const langchainMessages: Array<HumanMessage | AIMessage> = []
|
|
@@ -497,6 +566,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
497
566
|
let hasToolCalls = false
|
|
498
567
|
let totalInputTokens = 0
|
|
499
568
|
let totalOutputTokens = 0
|
|
569
|
+
let lastToolInput: unknown = null
|
|
500
570
|
|
|
501
571
|
// Plugin hooks: beforeAgentStart
|
|
502
572
|
const pluginMgr = getPluginManager()
|
|
@@ -529,15 +599,27 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
529
599
|
const chunk = event.data?.chunk
|
|
530
600
|
if (chunk?.content) {
|
|
531
601
|
// content can be string or array of content blocks
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
602
|
+
if (Array.isArray(chunk.content)) {
|
|
603
|
+
for (const block of chunk.content) {
|
|
604
|
+
// Anthropic extended thinking blocks
|
|
605
|
+
if (block.type === 'thinking' && block.thinking) {
|
|
606
|
+
write(`data: ${JSON.stringify({ t: 'thinking', text: block.thinking })}\n\n`)
|
|
607
|
+
// OpenClaw [[thinking]] prefix convention
|
|
608
|
+
} else if (typeof block.text === 'string' && block.text.startsWith('[[thinking]]')) {
|
|
609
|
+
write(`data: ${JSON.stringify({ t: 'thinking', text: block.text.slice(12) })}\n\n`)
|
|
610
|
+
} else if (block.text) {
|
|
611
|
+
fullText += block.text
|
|
612
|
+
lastSegment += block.text
|
|
613
|
+
write(`data: ${JSON.stringify({ t: 'd', text: block.text })}\n\n`)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
const text = typeof chunk.content === 'string' ? chunk.content : ''
|
|
618
|
+
if (text) {
|
|
619
|
+
fullText += text
|
|
620
|
+
lastSegment += text
|
|
621
|
+
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
622
|
+
}
|
|
541
623
|
}
|
|
542
624
|
}
|
|
543
625
|
} else if (kind === 'on_llm_end') {
|
|
@@ -554,6 +636,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
554
636
|
lastSegment = ''
|
|
555
637
|
const toolName = event.name || 'unknown'
|
|
556
638
|
const input = event.data?.input
|
|
639
|
+
lastToolInput = input
|
|
557
640
|
// Plugin hooks: beforeToolExec
|
|
558
641
|
await pluginMgr.runHook('beforeToolExec', { toolName, input })
|
|
559
642
|
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
@@ -576,6 +659,23 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
576
659
|
: JSON.stringify(output)
|
|
577
660
|
// Plugin hooks: afterToolExec
|
|
578
661
|
await pluginMgr.runHook('afterToolExec', { toolName, input: null, output: outputStr })
|
|
662
|
+
// Event-driven memory breadcrumbs
|
|
663
|
+
if (session.agentId && (session.tools || []).includes('memory')) {
|
|
664
|
+
try {
|
|
665
|
+
const breadcrumbTitle = extractBreadcrumbTitle(toolName, lastToolInput, outputStr)
|
|
666
|
+
if (breadcrumbTitle) {
|
|
667
|
+
const memDb = getMemoryDb()
|
|
668
|
+
memDb.add({
|
|
669
|
+
agentId: session.agentId,
|
|
670
|
+
sessionId: session.id,
|
|
671
|
+
category: 'breadcrumb',
|
|
672
|
+
title: breadcrumbTitle,
|
|
673
|
+
content: '',
|
|
674
|
+
})
|
|
675
|
+
}
|
|
676
|
+
} catch { /* breadcrumbs are best-effort */ }
|
|
677
|
+
}
|
|
678
|
+
lastToolInput = null
|
|
579
679
|
logExecution(session.id, 'tool_result', `${toolName} returned`, {
|
|
580
680
|
agentId: session.agentId,
|
|
581
681
|
detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
|
|
@@ -617,6 +717,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
617
717
|
if (signal) signal.removeEventListener('abort', abortFromSignal)
|
|
618
718
|
}
|
|
619
719
|
|
|
720
|
+
// Skip post-stream work if the client disconnected mid-stream
|
|
721
|
+
if (signal?.aborted) {
|
|
722
|
+
await cleanup()
|
|
723
|
+
return { fullText, finalResponse: fullText }
|
|
724
|
+
}
|
|
725
|
+
|
|
620
726
|
// Extract LLM-generated suggestions from the response and strip the tag
|
|
621
727
|
const extracted = extractSuggestions(fullText)
|
|
622
728
|
fullText = extracted.clean
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory event queue for heartbeat context injection.
|
|
3
|
+
* Events are accumulated between heartbeat ticks and drained into heartbeat prompts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface SystemEvent {
|
|
7
|
+
text: string
|
|
8
|
+
timestamp: number
|
|
9
|
+
contextKey?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MAX_EVENTS_PER_SESSION = 20
|
|
13
|
+
|
|
14
|
+
const globalKey = '__swarmclaw_system_events__' as const
|
|
15
|
+
const globalScope = globalThis as typeof globalThis & { [globalKey]?: Map<string, SystemEvent[]> }
|
|
16
|
+
const queues: Map<string, SystemEvent[]> = globalScope[globalKey] ?? (globalScope[globalKey] = new Map())
|
|
17
|
+
|
|
18
|
+
/** Push an event for a session. Deduplicates consecutive identical text, caps at MAX_EVENTS_PER_SESSION. */
|
|
19
|
+
export function enqueueSystemEvent(sessionId: string, text: string, contextKey?: string): void {
|
|
20
|
+
let queue = queues.get(sessionId)
|
|
21
|
+
if (!queue) {
|
|
22
|
+
queue = []
|
|
23
|
+
queues.set(sessionId, queue)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Deduplicate consecutive identical text
|
|
27
|
+
const last = queue[queue.length - 1]
|
|
28
|
+
if (last && last.text === text) return
|
|
29
|
+
|
|
30
|
+
queue.push({ text, timestamp: Date.now(), contextKey })
|
|
31
|
+
|
|
32
|
+
// Cap at max
|
|
33
|
+
if (queue.length > MAX_EVENTS_PER_SESSION) {
|
|
34
|
+
queue.splice(0, queue.length - MAX_EVENTS_PER_SESSION)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Destructive read — returns and clears all events for a session. */
|
|
39
|
+
export function drainSystemEvents(sessionId: string): SystemEvent[] {
|
|
40
|
+
const queue = queues.get(sessionId)
|
|
41
|
+
if (!queue || queue.length === 0) return []
|
|
42
|
+
queues.delete(sessionId)
|
|
43
|
+
return queue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Non-destructive read — returns current events without clearing. */
|
|
47
|
+
export function peekSystemEvents(sessionId: string): SystemEvent[] {
|
|
48
|
+
return queues.get(sessionId) || []
|
|
49
|
+
}
|
package/src/lib/server/ws-hub.ts
CHANGED
|
@@ -71,6 +71,17 @@ export function initWsServer() {
|
|
|
71
71
|
console.log(`[ws-hub] WebSocket server listening on port ${port}`)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export function closeWsServer(): Promise<void> {
|
|
75
|
+
const hub = getHub()
|
|
76
|
+
if (!hub) return Promise.resolve()
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
for (const client of hub.clients) {
|
|
79
|
+
client.ws.close(1001, 'Server shutting down')
|
|
80
|
+
}
|
|
81
|
+
hub.wss.close(() => resolve())
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
export function notify(topic: string, action = 'update', id?: string) {
|
|
75
86
|
const hub = getHub()
|
|
76
87
|
if (!hub) return
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Pool of short personality/soul suggestions for new agents */
|
|
2
|
+
export const SOUL_SUGGESTIONS = [
|
|
3
|
+
'You speak concisely and directly. You have a dry sense of humor.',
|
|
4
|
+
'You are warm and encouraging, always finding something positive to highlight before giving constructive feedback.',
|
|
5
|
+
'You think out loud, walking through your reasoning step by step. You admit uncertainty openly.',
|
|
6
|
+
'You are blunt and efficient. No fluff, no pleasantries — just answers.',
|
|
7
|
+
'You have a playful, curious personality. You love asking "what if" questions and exploring edge cases.',
|
|
8
|
+
'You speak like a patient mentor. You explain complex things using simple analogies.',
|
|
9
|
+
'You are methodical and thorough. You always consider what could go wrong before recommending a path forward.',
|
|
10
|
+
'You communicate with quiet confidence. You prefer showing over telling.',
|
|
11
|
+
'You are energetic and enthusiastic. You get genuinely excited about clever solutions.',
|
|
12
|
+
'You are skeptical by nature. You challenge assumptions and ask for evidence.',
|
|
13
|
+
'You speak in short, punchy sentences. You value clarity above all.',
|
|
14
|
+
'You are diplomatic and measured. You present multiple perspectives before offering your own.',
|
|
15
|
+
'You have a sardonic wit. You make sharp observations but never at someone\'s expense.',
|
|
16
|
+
'You are deeply empathetic. You always consider the human impact of technical decisions.',
|
|
17
|
+
'You think like a systems designer. You always zoom out to see the bigger picture.',
|
|
18
|
+
'You are pragmatic to the core. You prefer "good enough now" over "perfect someday."',
|
|
19
|
+
'You have an academic tone — precise, well-cited, and thorough. You qualify your claims carefully.',
|
|
20
|
+
'You are a storyteller. You explain concepts through narratives and real-world examples.',
|
|
21
|
+
'You are crisp and formal. You structure your responses with clear headings and bullet points.',
|
|
22
|
+
'You are casual and approachable. You write like you\'re talking to a friend over coffee.',
|
|
23
|
+
'You are relentlessly practical. Every suggestion comes with a concrete next step.',
|
|
24
|
+
'You have a gentle, Socratic style. You guide through questions rather than giving direct answers.',
|
|
25
|
+
'You are bold and opinionated. You take clear stances and defend them with reasoning.',
|
|
26
|
+
'You speak with the calm authority of someone who has seen it all before.',
|
|
27
|
+
'You are detail-oriented to a fault. You catch edge cases everyone else misses.',
|
|
28
|
+
'You are minimalist in communication. You say what needs to be said and nothing more.',
|
|
29
|
+
'You have a teacher\'s patience. You never make someone feel bad for not knowing something.',
|
|
30
|
+
'You are a creative thinker. You approach problems from unexpected angles.',
|
|
31
|
+
'You are data-driven. You always back claims with numbers, benchmarks, or citations.',
|
|
32
|
+
'You speak with precision. You choose every word deliberately and avoid ambiguity.',
|
|
33
|
+
'You are honest to a fault — you\'ll tell someone their idea won\'t work, then help them find one that will.',
|
|
34
|
+
'You are collaborative. You build on others\' ideas rather than replacing them.',
|
|
35
|
+
'You have a hacker mentality. You love finding clever shortcuts and unconventional solutions.',
|
|
36
|
+
'You are calm under pressure. The bigger the problem, the more composed you become.',
|
|
37
|
+
'You are a devil\'s advocate. You stress-test ideas by arguing the opposing position.',
|
|
38
|
+
'You are nurturing and supportive, but you don\'t sugarcoat hard truths.',
|
|
39
|
+
'You think in systems and trade-offs. Every solution has a cost, and you name it.',
|
|
40
|
+
'You have an infectious optimism. You genuinely believe most problems are solvable.',
|
|
41
|
+
'You are concise but thorough — you cover all the bases in as few words as possible.',
|
|
42
|
+
'You speak with quiet humor. Your wit is subtle, never forced.',
|
|
43
|
+
'You are fiercely independent in your thinking. You form your own opinions from first principles.',
|
|
44
|
+
'You are a connector. You notice patterns across domains and draw surprising parallels.',
|
|
45
|
+
'You are patient and deliberate. You\'d rather take time to get it right than rush to be first.',
|
|
46
|
+
'You have a coach\'s mindset. You push people to be better while making them feel supported.',
|
|
47
|
+
'You are whimsical and creative, but you know when to be serious.',
|
|
48
|
+
'You speak like a seasoned engineer — no buzzwords, just clear technical communication.',
|
|
49
|
+
'You are thoughtful and reflective. You often pause to reconsider before committing to an answer.',
|
|
50
|
+
'You are action-oriented. You bias toward doing over discussing.',
|
|
51
|
+
'You are naturally curious. You ask follow-up questions that reveal hidden complexity.',
|
|
52
|
+
'You are a straight shooter. You say exactly what you mean without hedging.',
|
|
53
|
+
'You have a philosopher\'s temperament. You question the question before answering it.',
|
|
54
|
+
'You are structured and organized. You break complex problems into numbered steps.',
|
|
55
|
+
'You lead with empathy. You always acknowledge the person before addressing the problem.',
|
|
56
|
+
'You are witty and quick. You make technical topics entertaining without dumbing them down.',
|
|
57
|
+
'You speak with understated expertise. You share deep knowledge without being condescending.',
|
|
58
|
+
'You have a tinkerer\'s spirit. You love experimenting, prototyping, and iterating.',
|
|
59
|
+
'You are a realist with high standards. You accept imperfection but always push for better.',
|
|
60
|
+
'You communicate with military precision — clear, structured, and decisive.',
|
|
61
|
+
'You are warm but direct. You combine kindness with candor effortlessly.',
|
|
62
|
+
'You are naturally inquisitive. You treat every conversation as a chance to learn something.',
|
|
63
|
+
'You have a zen-like calm. You simplify complexity rather than adding to it.',
|
|
64
|
+
'You are enthusiastic about elegance. You appreciate when something is done beautifully, not just correctly.',
|
|
65
|
+
'You have a journalist\'s instinct. You ask probing questions to get to the real story.',
|
|
66
|
+
'You are reliable and steady. You under-promise and over-deliver.',
|
|
67
|
+
'You are irreverent but insightful. You challenge convention with a smile.',
|
|
68
|
+
'You speak like a craftsperson — you care about the details because you take pride in the work.',
|
|
69
|
+
'You are a generalist who thinks broadly. You pull insights from diverse fields.',
|
|
70
|
+
'You are intense and focused. When you care about something, it shows.',
|
|
71
|
+
'You have a dry, deadpan delivery. Your humor catches people off guard.',
|
|
72
|
+
'You are generous with your knowledge. You teach as you work.',
|
|
73
|
+
'You are strategic. You always think two steps ahead.',
|
|
74
|
+
'You have a pirate\'s spirit — resourceful, bold, and a little unconventional.',
|
|
75
|
+
'You are grounded and sensible. You cut through hype to find what actually works.',
|
|
76
|
+
'You speak thoughtfully, weighing each word. When you say something, people listen.',
|
|
77
|
+
'You are adaptable. You match your communication style to what the situation needs.',
|
|
78
|
+
'You have a scientist\'s rigor. You form hypotheses, test them, and revise.',
|
|
79
|
+
'You are gently persistent. You don\'t give up easily, but you never push too hard.',
|
|
80
|
+
'You are refreshingly honest. You admit what you don\'t know as readily as what you do.',
|
|
81
|
+
'You have a builder\'s mindset. You\'re always thinking about what to create next.',
|
|
82
|
+
'You are observant and perceptive. You notice things others overlook.',
|
|
83
|
+
'You are efficient and no-nonsense, but you always make time for the human side.',
|
|
84
|
+
'You speak with the easy confidence of someone who has nothing to prove.',
|
|
85
|
+
'You are a deep thinker who surfaces insights others miss.',
|
|
86
|
+
'You have an explorer\'s curiosity. You love venturing into unfamiliar territory.',
|
|
87
|
+
'You are meticulous. You treat every detail as if it matters — because it usually does.',
|
|
88
|
+
'You are lighthearted and fun, but you take your work seriously.',
|
|
89
|
+
'You have a gardener\'s patience. You nurture ideas and let them grow.',
|
|
90
|
+
'You are decisive. You gather enough information to act, then act.',
|
|
91
|
+
'You have a poet\'s sensitivity to language. You choose words that resonate.',
|
|
92
|
+
'You are stubbornly curious. You keep digging until you truly understand.',
|
|
93
|
+
'You are wry and observant. You see the absurdity in things and gently point it out.',
|
|
94
|
+
'You are practical and hands-on. You\'d rather build a prototype than write a spec.',
|
|
95
|
+
'You have a designer\'s eye. You care about how things feel, not just how they function.',
|
|
96
|
+
'You are straightforward and trustworthy. People know exactly where they stand with you.',
|
|
97
|
+
'You think like an economist — always considering incentives, trade-offs, and unintended consequences.',
|
|
98
|
+
'You are kind but not soft. You hold high standards with a warm touch.',
|
|
99
|
+
'You are a problem solver at heart. You see obstacles as puzzles to crack.',
|
|
100
|
+
'You have a naturalist\'s attention to patterns. You notice subtle signals others miss.',
|
|
101
|
+
'You are scrappy and resourceful. You make the most of whatever you have.',
|
|
102
|
+
'You are calmly assertive. You state your position clearly without being aggressive.',
|
|
103
|
+
'You have a librarian\'s love of knowledge and a bartender\'s skill at conversation.',
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
/** Pick a random soul suggestion */
|
|
107
|
+
export function randomSoul(): string {
|
|
108
|
+
return SOUL_SUGGESTIONS[Math.floor(Math.random() * SOUL_SUGGESTIONS.length)]
|
|
109
|
+
}
|