@swarmclawai/swarmclaw 0.6.4 → 0.6.7

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 (143) hide show
  1. package/README.md +62 -30
  2. package/package.json +10 -1
  3. package/src/app/api/agents/[id]/clone/route.ts +40 -0
  4. package/src/app/api/agents/route.ts +39 -14
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
  6. package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
  7. package/src/app/api/chatrooms/[id]/route.ts +34 -2
  8. package/src/app/api/chatrooms/route.ts +26 -3
  9. package/src/app/api/connectors/[id]/health/route.ts +64 -0
  10. package/src/app/api/connectors/route.ts +17 -2
  11. package/src/app/api/knowledge/route.ts +6 -1
  12. package/src/app/api/openclaw/doctor/route.ts +17 -0
  13. package/src/app/api/schedules/[id]/run/route.ts +3 -0
  14. package/src/app/api/sessions/[id]/chat/route.ts +5 -1
  15. package/src/app/api/sessions/route.ts +11 -2
  16. package/src/app/api/tasks/[id]/route.ts +18 -13
  17. package/src/app/api/tasks/route.ts +44 -1
  18. package/src/app/api/usage/route.ts +16 -7
  19. package/src/app/api/wallets/[id]/approve/route.ts +62 -0
  20. package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
  21. package/src/app/api/wallets/[id]/route.ts +118 -0
  22. package/src/app/api/wallets/[id]/send/route.ts +118 -0
  23. package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
  24. package/src/app/api/wallets/route.ts +74 -0
  25. package/src/app/globals.css +8 -0
  26. package/src/cli/index.js +20 -0
  27. package/src/cli/index.ts +223 -39
  28. package/src/cli/spec.js +14 -0
  29. package/src/components/agents/agent-avatar.tsx +15 -1
  30. package/src/components/agents/agent-card.tsx +38 -6
  31. package/src/components/agents/agent-chat-list.tsx +79 -3
  32. package/src/components/agents/agent-sheet.tsx +191 -26
  33. package/src/components/auth/setup-wizard.tsx +268 -353
  34. package/src/components/chat/chat-area.tsx +24 -9
  35. package/src/components/chat/chat-header.tsx +48 -19
  36. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  37. package/src/components/chat/delegation-banner.test.ts +27 -0
  38. package/src/components/chat/delegation-banner.tsx +109 -23
  39. package/src/components/chat/message-bubble.tsx +17 -16
  40. package/src/components/chat/message-list.tsx +6 -5
  41. package/src/components/chat/streaming-bubble.tsx +3 -2
  42. package/src/components/chat/thinking-indicator.tsx +3 -2
  43. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  44. package/src/components/chatrooms/agent-hover-card.tsx +1 -1
  45. package/src/components/chatrooms/chatroom-input.tsx +1 -1
  46. package/src/components/chatrooms/chatroom-message.tsx +165 -23
  47. package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
  48. package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
  49. package/src/components/chatrooms/chatroom-view.tsx +62 -17
  50. package/src/components/connectors/connector-health.tsx +120 -0
  51. package/src/components/connectors/connector-list.tsx +1 -1
  52. package/src/components/connectors/connector-sheet.tsx +9 -0
  53. package/src/components/home/home-view.tsx +25 -3
  54. package/src/components/input/chat-input.tsx +8 -1
  55. package/src/components/knowledge/knowledge-list.tsx +1 -1
  56. package/src/components/knowledge/knowledge-sheet.tsx +1 -1
  57. package/src/components/layout/app-layout.tsx +35 -4
  58. package/src/components/memory/memory-agent-list.tsx +1 -1
  59. package/src/components/memory/memory-browser.tsx +1 -0
  60. package/src/components/memory/memory-card.tsx +3 -2
  61. package/src/components/memory/memory-detail.tsx +3 -3
  62. package/src/components/memory/memory-sheet.tsx +2 -2
  63. package/src/components/projects/project-detail.tsx +4 -4
  64. package/src/components/schedules/schedule-list.tsx +55 -9
  65. package/src/components/schedules/schedule-sheet.tsx +134 -23
  66. package/src/components/secrets/secret-sheet.tsx +1 -1
  67. package/src/components/secrets/secrets-list.tsx +1 -1
  68. package/src/components/sessions/session-card.tsx +1 -1
  69. package/src/components/shared/agent-picker-list.tsx +1 -1
  70. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  71. package/src/components/shared/command-palette.tsx +237 -0
  72. package/src/components/shared/connector-platform-icon.tsx +1 -0
  73. package/src/components/shared/settings/section-user-preferences.tsx +4 -4
  74. package/src/components/skills/skill-list.tsx +1 -1
  75. package/src/components/skills/skill-sheet.tsx +1 -1
  76. package/src/components/tasks/task-board.tsx +3 -3
  77. package/src/components/tasks/task-card.tsx +22 -2
  78. package/src/components/tasks/task-sheet.tsx +112 -17
  79. package/src/components/usage/metrics-dashboard.tsx +13 -25
  80. package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
  81. package/src/components/wallets/wallet-panel.tsx +616 -0
  82. package/src/components/wallets/wallet-section.tsx +100 -0
  83. package/src/hooks/use-swipe.ts +49 -0
  84. package/src/lib/providers/anthropic.ts +16 -2
  85. package/src/lib/providers/claude-cli.ts +7 -1
  86. package/src/lib/providers/index.ts +7 -0
  87. package/src/lib/providers/ollama.ts +16 -2
  88. package/src/lib/providers/openai.ts +7 -2
  89. package/src/lib/providers/openclaw.ts +6 -1
  90. package/src/lib/providers/provider-defaults.ts +7 -0
  91. package/src/lib/schedule-templates.ts +115 -0
  92. package/src/lib/server/agent-registry.ts +2 -2
  93. package/src/lib/server/alert-dispatch.ts +64 -0
  94. package/src/lib/server/chat-execution.ts +76 -4
  95. package/src/lib/server/chatroom-health.ts +60 -0
  96. package/src/lib/server/chatroom-helpers.test.ts +94 -0
  97. package/src/lib/server/chatroom-helpers.ts +86 -12
  98. package/src/lib/server/chatroom-routing.ts +65 -0
  99. package/src/lib/server/connectors/discord.ts +3 -0
  100. package/src/lib/server/connectors/email.ts +267 -0
  101. package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
  102. package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
  103. package/src/lib/server/connectors/manager.ts +239 -5
  104. package/src/lib/server/connectors/openclaw.ts +3 -0
  105. package/src/lib/server/connectors/slack.ts +6 -0
  106. package/src/lib/server/connectors/telegram.ts +18 -0
  107. package/src/lib/server/connectors/types.ts +2 -0
  108. package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
  109. package/src/lib/server/connectors/whatsapp-text.ts +26 -0
  110. package/src/lib/server/connectors/whatsapp.ts +17 -5
  111. package/src/lib/server/cost.ts +70 -0
  112. package/src/lib/server/create-notification.ts +2 -0
  113. package/src/lib/server/daemon-state.ts +124 -0
  114. package/src/lib/server/dag-validation.ts +115 -0
  115. package/src/lib/server/memory-db.ts +12 -7
  116. package/src/lib/server/openclaw-doctor.ts +48 -0
  117. package/src/lib/server/orchestrator-lg.ts +12 -2
  118. package/src/lib/server/orchestrator.ts +6 -1
  119. package/src/lib/server/queue-followups.test.ts +224 -0
  120. package/src/lib/server/queue.ts +238 -24
  121. package/src/lib/server/scheduler.ts +3 -0
  122. package/src/lib/server/session-run-manager.ts +22 -1
  123. package/src/lib/server/session-tools/chatroom.ts +11 -2
  124. package/src/lib/server/session-tools/context-mgmt.ts +2 -2
  125. package/src/lib/server/session-tools/index.ts +8 -2
  126. package/src/lib/server/session-tools/memory.ts +23 -4
  127. package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
  128. package/src/lib/server/session-tools/shell.ts +1 -1
  129. package/src/lib/server/session-tools/wallet.ts +124 -0
  130. package/src/lib/server/session-tools/web.ts +2 -2
  131. package/src/lib/server/solana.ts +122 -0
  132. package/src/lib/server/storage.ts +158 -6
  133. package/src/lib/server/stream-agent-chat.ts +126 -63
  134. package/src/lib/server/task-mention.test.ts +41 -0
  135. package/src/lib/server/task-mention.ts +3 -2
  136. package/src/lib/setup-defaults.ts +277 -0
  137. package/src/lib/tool-definitions.ts +1 -0
  138. package/src/lib/validation/schemas.ts +69 -0
  139. package/src/lib/view-routes.ts +1 -0
  140. package/src/stores/use-app-store.ts +15 -3
  141. package/src/stores/use-chatroom-store.ts +52 -2
  142. package/src/types/index.ts +98 -2
  143. package/tsconfig.json +2 -1
@@ -9,6 +9,100 @@ import type { Message } from '@/types'
9
9
  import { ensureMainSessionFlag } from './main-session'
10
10
  export const UPLOAD_DIR = path.join(DATA_DIR, 'uploads')
11
11
 
12
+ // --- LRU Cache ---
13
+
14
+ const DEFAULT_LRU_CAPACITY = 5000
15
+
16
+ /** Per-collection capacity overrides from COLLECTION_CACHE_LIMITS env var (JSON). */
17
+ function parseCacheLimits(): Record<string, number> {
18
+ const raw = process.env.COLLECTION_CACHE_LIMITS
19
+ if (!raw) return {}
20
+ try {
21
+ const parsed: unknown = JSON.parse(raw)
22
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
23
+ const result: Record<string, number> = {}
24
+ for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
25
+ if (typeof v === 'number' && v > 0) result[k] = v
26
+ }
27
+ return result
28
+ } catch {
29
+ return {}
30
+ }
31
+ }
32
+
33
+ const cacheLimits = parseCacheLimits()
34
+
35
+ function capacityFor(collection: string): number {
36
+ return cacheLimits[collection] ?? DEFAULT_LRU_CAPACITY
37
+ }
38
+
39
+ /**
40
+ * A Map wrapper with LRU eviction. JS Maps iterate in insertion order,
41
+ * so the *first* key is the least-recently-used entry.
42
+ */
43
+ class LRUMap<K, V> {
44
+ private readonly map = new Map<K, V>()
45
+ readonly capacity: number
46
+
47
+ constructor(capacity: number) {
48
+ this.capacity = Math.max(1, capacity)
49
+ }
50
+
51
+ get(key: K): V | undefined {
52
+ if (!this.map.has(key)) return undefined
53
+ const value = this.map.get(key)!
54
+ // Move to end (most-recently-used)
55
+ this.map.delete(key)
56
+ this.map.set(key, value)
57
+ return value
58
+ }
59
+
60
+ set(key: K, value: V): this {
61
+ if (this.map.has(key)) {
62
+ this.map.delete(key)
63
+ }
64
+ this.map.set(key, value)
65
+ // Evict oldest if over capacity
66
+ if (this.map.size > this.capacity) {
67
+ const oldest = this.map.keys().next().value as K
68
+ this.map.delete(oldest)
69
+ }
70
+ return this
71
+ }
72
+
73
+ has(key: K): boolean {
74
+ return this.map.has(key)
75
+ }
76
+
77
+ delete(key: K): boolean {
78
+ return this.map.delete(key)
79
+ }
80
+
81
+ get size(): number {
82
+ return this.map.size
83
+ }
84
+
85
+ clear(): void {
86
+ this.map.clear()
87
+ }
88
+
89
+ keys(): MapIterator<K> {
90
+ return this.map.keys()
91
+ }
92
+
93
+ values(): MapIterator<V> {
94
+ return this.map.values()
95
+ }
96
+
97
+ entries(): MapIterator<[K, V]> {
98
+ return this.map.entries()
99
+ }
100
+
101
+ [Symbol.iterator](): MapIterator<[K, V]> {
102
+ return this.map[Symbol.iterator]()
103
+ }
104
+ }
105
+
12
106
  // Ensure directories exist
13
107
  for (const dir of [DATA_DIR, UPLOAD_DIR, WORKSPACE_DIR]) {
14
108
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
@@ -26,12 +120,12 @@ db.pragma('foreign_keys = ON')
26
120
 
27
121
  const collectionCacheKey = '__swarmclaw_storage_collection_cache__' as const
28
122
  type StorageGlobals = typeof globalThis & {
29
- [collectionCacheKey]?: Map<string, Map<string, string>>
123
+ [collectionCacheKey]?: Map<string, LRUMap<string, string>>
30
124
  }
31
125
  const storageGlobals = globalThis as StorageGlobals
32
- const collectionCache: Map<string, Map<string, string>> =
126
+ const collectionCache: Map<string, LRUMap<string, string>> =
33
127
  storageGlobals[collectionCacheKey]
34
- ?? (storageGlobals[collectionCacheKey] = new Map<string, Map<string, string>>())
128
+ ?? (storageGlobals[collectionCacheKey] = new Map<string, LRUMap<string, string>>())
35
129
 
36
130
  // Collection tables (id → JSON blob)
37
131
  const COLLECTIONS = [
@@ -54,6 +148,11 @@ const COLLECTIONS = [
54
148
  'webhook_retry_queue',
55
149
  'notifications',
56
150
  'chatrooms',
151
+ 'wallets',
152
+ 'wallet_transactions',
153
+ 'wallet_balance_history',
154
+ 'moderation_logs',
155
+ 'connector_health',
57
156
  ] as const
58
157
 
59
158
  for (const table of COLLECTIONS) {
@@ -66,16 +165,16 @@ db.exec(`CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY CHECK (id = 1)
66
165
  db.exec(`CREATE TABLE IF NOT EXISTS usage (session_id TEXT NOT NULL, data TEXT NOT NULL)`)
67
166
  db.exec(`CREATE INDEX IF NOT EXISTS idx_usage_session ON usage(session_id)`)
68
167
 
69
- function readCollectionRaw(table: string): Map<string, string> {
168
+ function readCollectionRaw(table: string): LRUMap<string, string> {
70
169
  const rows = db.prepare(`SELECT id, data FROM ${table}`).all() as { id: string; data: string }[]
71
- const raw = new Map<string, string>()
170
+ const raw = new LRUMap<string, string>(capacityFor(table))
72
171
  for (const row of rows) {
73
172
  raw.set(row.id, row.data)
74
173
  }
75
174
  return raw
76
175
  }
77
176
 
78
- function getCollectionRawCache(table: string): Map<string, string> {
177
+ function getCollectionRawCache(table: string): LRUMap<string, string> {
79
178
  // Always reload from SQLite so concurrent Next.js workers/processes
80
179
  // observe each other's writes immediately.
81
180
  const loaded = readCollectionRaw(table)
@@ -826,6 +925,59 @@ export function markNotificationRead(id: string) {
826
925
  }
827
926
  }
828
927
 
928
+ // --- Wallets ---
929
+ export function loadWallets(): Record<string, unknown> {
930
+ return loadCollection('wallets')
931
+ }
932
+
933
+ export function upsertWallet(id: string, wallet: unknown) {
934
+ upsertCollectionItem('wallets', id, wallet)
935
+ }
936
+
937
+ export function deleteWallet(id: string) {
938
+ deleteCollectionItem('wallets', id)
939
+ }
940
+
941
+ // --- Wallet Transactions ---
942
+ export function loadWalletTransactions(): Record<string, unknown> {
943
+ return loadCollection('wallet_transactions')
944
+ }
945
+
946
+ export function upsertWalletTransaction(id: string, tx: unknown) {
947
+ upsertCollectionItem('wallet_transactions', id, tx)
948
+ }
949
+
950
+ export function deleteWalletTransaction(id: string) {
951
+ deleteCollectionItem('wallet_transactions', id)
952
+ }
953
+
954
+ // --- Wallet Balance History ---
955
+ export function loadWalletBalanceHistory(): Record<string, unknown> {
956
+ return loadCollection('wallet_balance_history')
957
+ }
958
+
959
+ export function upsertWalletBalanceSnapshot(id: string, snapshot: unknown) {
960
+ upsertCollectionItem('wallet_balance_history', id, snapshot)
961
+ }
962
+
963
+ // --- Moderation Logs ---
964
+ export function loadModerationLogs(): Record<string, unknown> {
965
+ return loadCollection('moderation_logs')
966
+ }
967
+
968
+ export function appendModerationLog(id: string, entry: unknown) {
969
+ upsertCollectionItem('moderation_logs', id, entry)
970
+ }
971
+
972
+ // --- Connector Health ---
973
+ export function loadConnectorHealth(): Record<string, unknown> {
974
+ return loadCollection('connector_health')
975
+ }
976
+
977
+ export function upsertConnectorHealthEvent(id: string, event: unknown) {
978
+ upsertCollectionItem('connector_health', id, event)
979
+ }
980
+
829
981
  export function getSessionMessages(sessionId: string): Message[] {
830
982
  const stmt = db.prepare('SELECT data FROM sessions WHERE id = ?')
831
983
  const row = stmt.get(sessionId) as { data: string } | undefined
@@ -42,37 +42,38 @@ interface StreamAgentChatOpts {
42
42
 
43
43
  function buildToolCapabilityLines(enabledTools: string[], opts?: { platformAssignScope?: 'self' | 'all' }): string[] {
44
44
  const lines: string[] = []
45
- 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.')
46
- if (enabledTools.includes('process')) lines.push('- Process control is available (`process_tool`) for long-running commands (poll/log/write/kill).')
45
+ if (enabledTools.includes('shell')) lines.push('- I can run shell commands (`execute_command`) servers, installs, scripts, git, builds, anything. I can run things in the background for long-lived processes like dev servers.')
46
+ if (enabledTools.includes('process')) lines.push('- I can manage running processes (`process_tool`) check status, read logs, send input, or stop them.')
47
47
  if (enabledTools.includes('files') || enabledTools.includes('copy_file') || enabledTools.includes('move_file') || enabledTools.includes('delete_file')) {
48
- lines.push('- File operations are available (`read_file`, `write_file`, `list_files`, `copy_file`, `move_file`, `send_file`). `delete_file` is destructive and may be disabled unless explicitly enabled.')
48
+ lines.push('- I can read, write, copy, move, and send files (`read_file`, `write_file`, `list_files`, `copy_file`, `move_file`, `send_file`). Deleting files is destructive, so that may need explicit permission.')
49
49
  }
50
- if (enabledTools.includes('edit_file')) lines.push('- Precise single-match replacement is available (`edit_file`).')
51
- if (enabledTools.includes('web_search')) lines.push('- Web search is available (`web_search`). Use it for external research, options discovery, and validation.')
52
- if (enabledTools.includes('web_fetch')) lines.push('- URL content extraction is available (`web_fetch`) for source-backed analysis.')
53
- if (enabledTools.includes('browser')) lines.push('- Browser automation is available (`browser`). Use it for interactive websites and screenshots.')
54
- if (enabledTools.includes('claude_code')) lines.push('- Claude delegation is available (`delegate_to_claude_code`) for deep coding/refactor tasks. Resume IDs may be returned via `[delegate_meta]`.')
55
- if (enabledTools.includes('codex_cli')) lines.push('- Codex delegation is available (`delegate_to_codex_cli`) for deep coding/refactor tasks. Resume IDs may be returned via `[delegate_meta]`.')
56
- if (enabledTools.includes('opencode_cli')) lines.push('- OpenCode delegation is available (`delegate_to_opencode_cli`) for deep coding/refactor tasks. Resume IDs may be returned via `[delegate_meta]`.')
57
- if (enabledTools.includes('memory')) lines.push('- Long-term memory is available (`memory_tool`) to store and recall durable context.')
58
- if (enabledTools.includes('sandbox')) lines.push('- Sandboxed code execution is available (`sandbox_exec`). Write and run JS/TS (Deno) or Python scripts in an isolated environment. Output includes stdout, stderr, and any files created as downloadable artifacts.')
59
- if (enabledTools.includes('manage_agents')) lines.push('- Agent management is available (`manage_agents`) to create or adjust specialist agents.')
60
- if (enabledTools.includes('manage_tasks')) lines.push('- Task management is available (`manage_tasks`) to create and track execution plans.')
61
- if (enabledTools.includes('manage_schedules')) lines.push('- Schedule management is available (`manage_schedules`) for recurring/ongoing runs.')
62
- if (enabledTools.includes('manage_documents')) lines.push('- Document indexing/search is available (`manage_documents`) for long-term knowledge and retrieval.')
63
- if (enabledTools.includes('manage_webhooks')) lines.push('- Webhook registration is available (`manage_webhooks`) so external events can trigger agent work.')
64
- if (enabledTools.includes('manage_skills')) lines.push('- Skill management is available (`manage_skills`) to add reusable capabilities.')
65
- if (enabledTools.includes('manage_connectors')) lines.push('- Connector management is available (`manage_connectors`) for channels like WhatsApp/Telegram/Slack, plus proactive outbound notifications via `connector_message_tool`.')
66
- if (enabledTools.includes('manage_sessions')) lines.push('- Session management is available (`manage_sessions`, `sessions_tool`, `whoami_tool`, `search_history_tool`) for session identity, history lookup, delegation, and inter-session messaging.')
50
+ if (enabledTools.includes('edit_file')) lines.push('- I can make precise edits to files (`edit_file`) — surgical find-and-replace without rewriting the whole file.')
51
+ if (enabledTools.includes('web_search')) lines.push('- I can search the web (`web_search`) for research, fact-checking, and discovery.')
52
+ if (enabledTools.includes('web_fetch')) lines.push('- I can fetch and read web pages (`web_fetch`) to pull in real content for analysis.')
53
+ if (enabledTools.includes('browser')) lines.push('- I can control a browser (`browser`) navigate sites, fill forms, take screenshots, interact with web apps.')
54
+ if (enabledTools.includes('claude_code')) lines.push('- I can hand off deep coding work to Claude Code (`delegate_to_claude_code`) for complex multi-file refactors and code generation. Resume IDs may come back via `[delegate_meta]`.')
55
+ if (enabledTools.includes('codex_cli')) lines.push('- I can hand off deep coding work to Codex (`delegate_to_codex_cli`) for complex multi-file refactors and code generation. Resume IDs may come back via `[delegate_meta]`.')
56
+ if (enabledTools.includes('opencode_cli')) lines.push('- I can hand off deep coding work to OpenCode (`delegate_to_opencode_cli`) for complex multi-file refactors and code generation. Resume IDs may come back via `[delegate_meta]`.')
57
+ if (enabledTools.includes('memory')) lines.push('- I have long-term memory (`memory_tool`) I can remember things across conversations and recall them when needed.')
58
+ if (enabledTools.includes('sandbox')) lines.push('- I can run code in a sandbox (`sandbox_exec`) JS/TS via Deno or Python, in an isolated environment. I get stdout, stderr, and any files created.')
59
+ if (enabledTools.includes('manage_agents')) lines.push('- I can create and configure other agents (`manage_agents`) spin up specialists when a task calls for it.')
60
+ if (enabledTools.includes('manage_tasks')) lines.push('- I can manage tasks (`manage_tasks`) create plans, track progress, and stay organized over time.')
61
+ if (enabledTools.includes('manage_schedules')) lines.push('- I can set up schedules (`manage_schedules`) for recurring work or future follow-ups.')
62
+ if (enabledTools.includes('manage_documents')) lines.push('- I can store and search documents (`manage_documents`) for long-term knowledge and reference.')
63
+ if (enabledTools.includes('manage_webhooks')) lines.push('- I can register webhooks (`manage_webhooks`) so external events can trigger my work automatically.')
64
+ if (enabledTools.includes('manage_skills')) lines.push('- I can manage reusable skills (`manage_skills`) building blocks I can learn and apply.')
65
+ if (enabledTools.includes('manage_connectors')) lines.push('- I can manage messaging channels (`manage_connectors`) WhatsApp, Telegram, Slack, Discord — and send proactive messages via `connector_message_tool`.')
66
+ if (enabledTools.includes('manage_sessions')) lines.push('- I can manage chat sessions (`manage_sessions`, `sessions_tool`, `whoami_tool`, `search_history_tool`) check my identity, look up past conversations, message other sessions, and coordinate work.')
67
67
  // Context tools are available to any session with tools (not just manage_sessions)
68
68
  if (enabledTools.length > 0) {
69
- 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.')
69
+ lines.push('- I can monitor my own context usage (`context_status`) and compact my conversation history (`context_summarize`) when I\'m running low on space.')
70
70
  if (opts?.platformAssignScope === 'all') {
71
- lines.push('- Agent delegation is available (`delegate_to_agent`). Use it to assign tasks to other agents based on their capabilities.')
71
+ lines.push('- I can delegate tasks to other agents (`delegate_to_agent`) based on their strengths and availability.')
72
72
  }
73
73
  }
74
- if (enabledTools.includes('manage_secrets')) lines.push('- Secret management is available (`manage_secrets`) for durable encrypted credentials and API tokens.')
75
- if (enabledTools.includes('manage_chatrooms')) lines.push('- Chatroom management is available (`manage_chatrooms`) for multi-agent collaborative chatrooms with @mention-based interactions.')
74
+ if (enabledTools.includes('manage_secrets')) lines.push('- I can store and retrieve encrypted secrets (`manage_secrets`) API keys, credentials, tokens.')
75
+ if (enabledTools.includes('manage_chatrooms')) lines.push('- I can create and participate in chatrooms (`manage_chatrooms`) for multi-agent collaboration with @mention-based discussions.')
76
+ if (enabledTools.includes('wallet')) lines.push('- I have my own crypto wallet (`wallet_tool`) — I can check my balance, send SOL, and review my transaction history.')
76
77
  return lines
77
78
  }
78
79
 
@@ -92,8 +93,8 @@ function buildAgenticExecutionPolicy(opts: {
92
93
  ].filter(Boolean) as string[]
93
94
  const hasDelegationTool = delegationOrder.length > 0
94
95
  return [
95
- '## Agentic Execution Policy',
96
- 'You are not a passive chatbot. Execute work proactively and use available tools to gather evidence, create artifacts, and make progress.',
96
+ '## How I Work',
97
+ 'I take initiative. When there\'s work to do, I do it — I use my tools to research, build, and make real progress rather than just talking about it.',
97
98
  hasTooling
98
99
  ? 'For open-ended requests, run an action loop: plan briefly, execute tools, evaluate results, then continue until meaningful progress is achieved.'
99
100
  : 'This session has no tools enabled, so be explicit about what tool access is needed for deeper execution.',
@@ -139,6 +140,18 @@ function buildAgenticExecutionPolicy(opts: {
139
140
  hasDelegationTool
140
141
  ? 'CRITICAL — tool selection: ALWAYS use `execute_command` for running servers, dev servers, HTTP servers, installing dependencies, running scripts, git operations, process management, starting/stopping services, or any command the user wants to "run". Delegation tools (Claude/Codex/OpenCode) CANNOT keep a server running — their session ends and the process dies. `execute_command` with background=true is the ONLY way to run persistent processes.'
141
142
  : '',
143
+ opts.enabledTools.includes('shell')
144
+ ? 'When the user asks for an IP address or network URL, execute shell commands to resolve it and return the concrete value. Never reply with placeholders like `<your-local-ip>` and never tell the user to run `ifconfig`/`ipconfig` themselves unless shell access is unavailable.'
145
+ : '',
146
+ opts.enabledTools.includes('shell')
147
+ ? 'For long-lived servers/processes: start with `execute_command` using `background=true`, capture the returned processId, then verify with `process_tool` status/log before claiming success. If the process exits or crashes, retry with a corrected command and report what changed.'
148
+ : '',
149
+ opts.enabledTools.includes('shell')
150
+ ? 'Do not claim a server is running unless there is direct tool evidence (process status/log output).'
151
+ : '',
152
+ opts.enabledTools.includes('shell')
153
+ ? 'If `execute_command` fails due workdir/path traversal, retry without a workdir override or use a safe relative path under the current session cwd.'
154
+ : '',
142
155
  hasDelegationTool
143
156
  ? `Only use CLI delegation (${delegationOrder.join(' -> ')}) for tasks that need deep code understanding across multiple files: large refactors, complex debugging, multi-file code generation, or test suites. Never delegate when the user says "run", "start", "serve", "execute", or "test it locally".`
144
157
  : '',
@@ -178,7 +191,7 @@ function buildAgenticExecutionPolicy(opts: {
178
191
  opts.heartbeatIntervalSec > 0
179
192
  ? `Expected heartbeat cadence is roughly every ${opts.heartbeatIntervalSec} seconds while ongoing work is active.`
180
193
  : '',
181
- toolLines.length ? 'Available capabilities:\n' + toolLines.join('\n') : '',
194
+ toolLines.length ? 'What I can do:\n' + toolLines.join('\n') : '',
182
195
  ].filter(Boolean).join('\n')
183
196
  }
184
197
 
@@ -192,6 +205,10 @@ export interface StreamAgentChatResult {
192
205
 
193
206
  export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
194
207
  const { session, message, imagePath, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
208
+ const sessionToolsWithImplicitProcess = Array.from(new Set([
209
+ ...(session.tools || []),
210
+ ...((session.tools || []).includes('shell') ? ['process'] : []),
211
+ ]))
195
212
 
196
213
  // fallbackCredentialIds is intentionally accepted for compatibility with caller signatures.
197
214
  void fallbackCredentialIds
@@ -249,6 +266,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
249
266
  agentMcpServerIds = agent?.mcpServerIds
250
267
  agentMcpDisabledTools = agent?.mcpDisabledTools
251
268
  if (!hasProvidedSystemPrompt) {
269
+ // Identity block — make sure the agent knows who it is
270
+ const identityLines = [`## My Identity`, `My name is ${agent?.name || 'Agent'}.`]
271
+ if (agent?.description) identityLines.push(agent.description)
272
+ identityLines.push('I should always refer to myself by this name. I am not "Assistant" — I have my own name and identity.')
273
+ stateModifierParts.push(identityLines.join(' '))
252
274
  if (agent?.soul) stateModifierParts.push(agent.soul)
253
275
  if (agent?.systemPrompt) stateModifierParts.push(agent.systemPrompt)
254
276
  if (agent?.skillIds?.length) {
@@ -262,7 +284,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
262
284
  }
263
285
 
264
286
  if (!hasProvidedSystemPrompt) {
265
- stateModifierParts.push('You are a capable AI assistant with tool access. Be execution-oriented and outcome-focused.')
287
+ stateModifierParts.push('I\'m here to get things done. I take action, use my tools, and focus on outcomes.')
266
288
  }
267
289
 
268
290
  // Thinking level guidance (applies to all providers via system prompt)
@@ -339,28 +361,28 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
339
361
 
340
362
  // Memory Policy — always injected when memory tool is available
341
363
  stateModifierParts.push([
342
- '## Memory Policy',
343
- 'You have long-term memory. Use it proactivelydo not wait to be asked.',
364
+ '## My Memory',
365
+ 'I have long-term memory that persists across conversations. I use it naturallyI don\'t wait to be asked to remember things.',
344
366
  '',
345
- '**Store memories for:**',
346
- '- User preferences, corrections, or explicit "remember this" requests',
347
- '- Key decisions or outcomes from complex tasks',
348
- '- Discovered facts about projects, codebases, or environments',
349
- '- Errors encountered and their solutions',
350
- '- Relationship context (who is who, team dynamics)',
351
- '- Important configuration details or environment specifics',
367
+ '**Things worth remembering:**',
368
+ '- What the user likes, dislikes, or has corrected me on',
369
+ '- Important decisions, outcomes, and lessons learned',
370
+ '- What I\'ve discovered about projects, codebases, or environments',
371
+ '- Problems I\'ve hit and how I solved them',
372
+ '- Who people are and how they relate to each other',
373
+ '- Configuration details and environment specifics that I\'ll need again',
352
374
  '',
353
- '**Do NOT store:**',
354
- '- Trivial acknowledgments or small talk',
355
- '- Temporary in-progress work (use category "working" for ephemeral notes)',
356
- '- Information already in your system prompt',
357
- '- Exact duplicates of memories you already have',
375
+ '**Not worth cluttering my memory with:**',
376
+ '- Throwaway acknowledgments or small talk',
377
+ '- Work-in-progress that\'ll change soon (use category "working" for scratch notes)',
378
+ '- Things already in my system prompt',
379
+ '- Something I\'ve already stored',
358
380
  '',
359
- '**Best practices:**',
360
- '- Use descriptive titles ("User prefers dark mode" not "Note 1")',
381
+ '**Good habits:**',
382
+ '- Give memories clear titles ("User prefers dark mode" not "Note 1")',
361
383
  '- Use categories: preference, fact, learning, project, identity, decision',
362
- '- Search memory before storing to avoid duplicates',
363
- '- When correcting old knowledge, update or delete the old memory',
384
+ '- Check what I already know before storing something new',
385
+ '- When I learn something that corrects old knowledge, update or remove the old memory',
364
386
  ].join('\n'))
365
387
 
366
388
  // Pre-compaction memory flush: nudge agent to persist learnings when conversation is long
@@ -368,9 +390,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
368
390
  if (msgCount > 20) {
369
391
  stateModifierParts.push([
370
392
  '## Memory Flush Reminder',
371
- 'This conversation is getting long. Before context is trimmed, store any important',
372
- 'learnings, decisions, or facts as memories now. Only store what is significant and durable —',
373
- 'skip trivial details. If nothing needs storing, continue normally.',
393
+ 'This conversation is getting long and I might lose older context soon. I should save anything',
394
+ 'important I\'ve learned, decided, or discovered to my memory now things I\'d want to recall',
395
+ 'in future conversations. Only what matters, not every detail. If there\'s nothing worth saving, carry on.',
374
396
  ].join('\n'))
375
397
  }
376
398
  } catch {
@@ -389,12 +411,44 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
389
411
  }
390
412
  }
391
413
 
414
+ // Inject wallet context when agent has wallet tool enabled and a linked wallet
415
+ if ((session.tools || []).includes('wallet') && session.agentId) {
416
+ try {
417
+ const { loadWallets } = await import('./storage')
418
+ const wallets = loadWallets() as Record<string, { agentId: string; publicKey: string; chain: string; spendingLimitLamports?: number; dailyLimitLamports?: number; requireApproval: boolean }>
419
+ const wallet = Object.values(wallets).find((w) => w.agentId === session.agentId)
420
+ if (wallet) {
421
+ const { getBalance, lamportsToSol } = await import('./solana')
422
+ let balanceSol = 0
423
+ try {
424
+ const lamports = await getBalance(wallet.publicKey)
425
+ balanceSol = lamportsToSol(lamports)
426
+ } catch { /* RPC failure — show 0 */ }
427
+ const perTxLimit = lamportsToSol(wallet.spendingLimitLamports ?? 100_000_000)
428
+ const dailyLimit = lamportsToSol(wallet.dailyLimitLamports ?? 1_000_000_000)
429
+ stateModifierParts.push([
430
+ '## Your Wallet',
431
+ `You own a ${wallet.chain} wallet — this is your wallet, your funds, your address. Speak about it in the first person ("my wallet", "my balance", "I have X SOL"). You can spend from it freely within your limits${wallet.requireApproval ? ', though the user needs to approve each send before it goes through' : ''}.`,
432
+ `- Address: ${wallet.publicKey}`,
433
+ `- Balance: ${balanceSol} SOL`,
434
+ `- Per-transaction limit: ${perTxLimit} SOL`,
435
+ `- Daily limit: ${dailyLimit} SOL`,
436
+ 'Use the `wallet_tool` to check your balance, send SOL, or view your transaction history.',
437
+ ].join('\n'))
438
+ }
439
+ } catch {
440
+ // Wallet context is non-critical
441
+ }
442
+ }
443
+
392
444
  // Tell the LLM about tools it could use but doesn't have enabled
393
445
  {
394
- const enabledSet = new Set(session.tools || [])
446
+ const enabledSet = new Set(sessionToolsWithImplicitProcess)
395
447
  const allToolIds = [
396
- 'shell', 'files', 'edit_file', 'process', 'web_search', 'web_fetch', 'browser', 'memory',
448
+ 'shell', 'files', 'copy_file', 'move_file', 'delete_file', 'edit_file', 'process',
449
+ 'web_search', 'web_fetch', 'browser', 'memory',
397
450
  'claude_code', 'codex_cli', 'opencode_cli',
451
+ 'sandbox', 'create_document', 'create_spreadsheet', 'http_request', 'git', 'wallet',
398
452
  'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills',
399
453
  'manage_documents', 'manage_webhooks', 'manage_connectors', 'manage_sessions', 'manage_secrets',
400
454
  ]
@@ -403,13 +457,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
403
457
  const allDisabled = [...disabled, ...mcpDisabled]
404
458
  if (allDisabled.length > 0) {
405
459
  stateModifierParts.push(
406
- `## Disabled Tools\nThe following tools exist but are not enabled for you: ${allDisabled.join(', ')}.\n` +
407
- 'If you need one of these to complete a task, use the `request_tool_access` tool to ask the user for permission.',
460
+ `## Tools I Don't Have Yet\nI don't currently have access to: ${allDisabled.join(', ')}.\n` +
461
+ 'If I need any of these for a task, I can ask the user to enable them with `request_tool_access`.',
408
462
  )
409
463
  }
410
464
  }
411
465
 
412
- if (settings.suggestionsEnabled !== false) {
466
+ if (settings.suggestionsEnabled === true) {
413
467
  stateModifierParts.push(
414
468
  [
415
469
  '## Follow-up Suggestions',
@@ -423,7 +477,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
423
477
 
424
478
  stateModifierParts.push(
425
479
  buildAgenticExecutionPolicy({
426
- enabledTools: session.tools || [],
480
+ enabledTools: sessionToolsWithImplicitProcess,
427
481
  loopMode: runtime.loopMode,
428
482
  heartbeatPrompt,
429
483
  heartbeatIntervalSec,
@@ -433,7 +487,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
433
487
 
434
488
  const stateModifier = stateModifierParts.join('\n\n')
435
489
 
436
- const { tools, cleanup } = await buildSessionTools(session.cwd, session.tools || [], {
490
+ const { tools, cleanup } = await buildSessionTools(session.cwd, sessionToolsWithImplicitProcess, {
437
491
  agentId: session.agentId,
438
492
  sessionId: session.id,
439
493
  platformAssignScope: agentPlatformAssignScope,
@@ -647,13 +701,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
647
701
  }
648
702
  }
649
703
  } else if (kind === 'on_llm_end') {
650
- // Track token usage from LLM responses
651
- const usage = event.data?.output?.llmOutput?.tokenUsage
652
- || event.data?.output?.llmOutput?.usage
653
- || event.data?.output?.usage_metadata
704
+ // Track token usage from LLM responses — check all known LangChain event shapes
705
+ const output = event.data?.output
706
+ const usage = output?.llmOutput?.tokenUsage
707
+ || output?.llmOutput?.usage
708
+ || output?.usage_metadata
709
+ || output?.response_metadata?.usage
710
+ || output?.response_metadata?.tokenUsage
654
711
  if (usage) {
655
- totalInputTokens += usage.promptTokens || usage.input_tokens || 0
656
- totalOutputTokens += usage.completionTokens || usage.output_tokens || 0
712
+ totalInputTokens += usage.promptTokens || usage.input_tokens || usage.prompt_tokens || 0
713
+ totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
657
714
  }
658
715
  } else if (kind === 'on_tool_start') {
659
716
  hasToolCalls = true
@@ -759,7 +816,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
759
816
  write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ thinking: accumulatedThinking }) })}\n\n`)
760
817
  }
761
818
 
762
- // Track cost
819
+ // Track cost — fall back to character-count estimation when providers
820
+ // don't surface token counts through LangChain's on_llm_end event.
821
+ if (totalInputTokens === 0 && totalOutputTokens === 0 && fullText) {
822
+ const historyText = history.map((m) => m.text || '').join('')
823
+ totalInputTokens = Math.ceil((message.length + historyText.length + (systemPrompt?.length || 0)) / 4)
824
+ totalOutputTokens = Math.ceil(fullText.length / 4)
825
+ }
763
826
  const totalTokens = totalInputTokens + totalOutputTokens
764
827
  if (totalTokens > 0) {
765
828
  const cost = estimateCost(session.model, totalInputTokens, totalOutputTokens)
@@ -0,0 +1,41 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import type { Agent } from '@/types'
4
+ import { parseMentionedAgentId, resolveTaskAgentFromDescription } from './task-mention'
5
+
6
+ const now = Date.now()
7
+ const agents: Record<string, Agent> = {
8
+ default: {
9
+ id: 'default',
10
+ name: 'Assistant',
11
+ description: '',
12
+ systemPrompt: '',
13
+ provider: 'openai',
14
+ model: 'gpt-4o',
15
+ createdAt: now,
16
+ updatedAt: now,
17
+ },
18
+ coder: {
19
+ id: 'coder',
20
+ name: 'CodeBot',
21
+ description: '',
22
+ systemPrompt: '',
23
+ provider: 'openai',
24
+ model: 'gpt-4o',
25
+ createdAt: now,
26
+ updatedAt: now,
27
+ },
28
+ }
29
+
30
+ describe('task-mention', () => {
31
+ it('matches mentions with trailing punctuation', () => {
32
+ const found = parseMentionedAgentId('Please hand this to @CodeBot, thanks.', agents)
33
+ assert.equal(found, 'coder')
34
+ })
35
+
36
+ it('falls back to current agent when no mention is present', () => {
37
+ const resolved = resolveTaskAgentFromDescription('No mention here', 'default', agents)
38
+ assert.equal(resolved, 'default')
39
+ })
40
+ })
41
+
@@ -8,12 +8,13 @@ export function parseMentionedAgentId(
8
8
  description: string,
9
9
  agents: Record<string, Agent>,
10
10
  ): string | null {
11
- const mentionRegex = /@(\S+)/g
11
+ const mentionRegex = /(?:^|[\s(])@([a-zA-Z0-9._-]+)/g
12
12
  const agentList = Object.values(agents)
13
13
  let match: RegExpExecArray | null
14
14
 
15
15
  while ((match = mentionRegex.exec(description)) !== null) {
16
- const mention = match[1].toLowerCase()
16
+ const mention = (match[1] || '').toLowerCase().replace(/[.,!?;:]+$/g, '')
17
+ if (!mention) continue
17
18
 
18
19
  // Exact name match (case-insensitive)
19
20
  const exact = agentList.find((a) => a.name.toLowerCase() === mention)