@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.
- package/README.md +62 -30
- package/package.json +10 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +34 -2
- package/src/app/api/chatrooms/route.ts +26 -3
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +44 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/cli/index.js +20 -0
- package/src/cli/index.ts +223 -39
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +38 -6
- package/src/components/agents/agent-chat-list.tsx +79 -3
- package/src/components/agents/agent-sheet.tsx +191 -26
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +24 -9
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +17 -16
- package/src/components/chat/message-list.tsx +6 -5
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +165 -23
- package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +25 -3
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +35 -4
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +112 -17
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +76 -4
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +86 -12
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +239 -5
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +17 -5
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +238 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +8 -2
- package/src/lib/server/session-tools/memory.ts +23 -4
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +158 -6
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/stores/use-app-store.ts +15 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +98 -2
- 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,
|
|
123
|
+
[collectionCacheKey]?: Map<string, LRUMap<string, string>>
|
|
30
124
|
}
|
|
31
125
|
const storageGlobals = globalThis as StorageGlobals
|
|
32
|
-
const collectionCache: Map<string,
|
|
126
|
+
const collectionCache: Map<string, LRUMap<string, string>> =
|
|
33
127
|
storageGlobals[collectionCacheKey]
|
|
34
|
-
?? (storageGlobals[collectionCacheKey] = new Map<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):
|
|
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
|
|
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):
|
|
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('-
|
|
46
|
-
if (enabledTools.includes('process')) lines.push('-
|
|
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('-
|
|
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('-
|
|
51
|
-
if (enabledTools.includes('web_search')) lines.push('-
|
|
52
|
-
if (enabledTools.includes('web_fetch')) lines.push('-
|
|
53
|
-
if (enabledTools.includes('browser')) lines.push('-
|
|
54
|
-
if (enabledTools.includes('claude_code')) lines.push('-
|
|
55
|
-
if (enabledTools.includes('codex_cli')) lines.push('-
|
|
56
|
-
if (enabledTools.includes('opencode_cli')) lines.push('-
|
|
57
|
-
if (enabledTools.includes('memory')) lines.push('-
|
|
58
|
-
if (enabledTools.includes('sandbox')) lines.push('-
|
|
59
|
-
if (enabledTools.includes('manage_agents')) lines.push('-
|
|
60
|
-
if (enabledTools.includes('manage_tasks')) lines.push('-
|
|
61
|
-
if (enabledTools.includes('manage_schedules')) lines.push('-
|
|
62
|
-
if (enabledTools.includes('manage_documents')) lines.push('-
|
|
63
|
-
if (enabledTools.includes('manage_webhooks')) lines.push('-
|
|
64
|
-
if (enabledTools.includes('manage_skills')) lines.push('-
|
|
65
|
-
if (enabledTools.includes('manage_connectors')) lines.push('-
|
|
66
|
-
if (enabledTools.includes('manage_sessions')) lines.push('-
|
|
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('-
|
|
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('-
|
|
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('-
|
|
75
|
-
if (enabledTools.includes('manage_chatrooms')) lines.push('-
|
|
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
|
-
'##
|
|
96
|
-
'
|
|
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 ? '
|
|
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('
|
|
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
|
|
343
|
-
'
|
|
364
|
+
'## My Memory',
|
|
365
|
+
'I have long-term memory that persists across conversations. I use it naturally — I don\'t wait to be asked to remember things.',
|
|
344
366
|
'',
|
|
345
|
-
'**
|
|
346
|
-
'-
|
|
347
|
-
'-
|
|
348
|
-
'-
|
|
349
|
-
'-
|
|
350
|
-
'-
|
|
351
|
-
'-
|
|
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
|
-
'**
|
|
354
|
-
'-
|
|
355
|
-
'-
|
|
356
|
-
'-
|
|
357
|
-
'-
|
|
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
|
-
'**
|
|
360
|
-
'-
|
|
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
|
-
'-
|
|
363
|
-
'- When
|
|
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
|
|
372
|
-
'
|
|
373
|
-
'
|
|
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(
|
|
446
|
+
const enabledSet = new Set(sessionToolsWithImplicitProcess)
|
|
395
447
|
const allToolIds = [
|
|
396
|
-
'shell', 'files', '
|
|
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
|
-
`##
|
|
407
|
-
'If
|
|
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
|
|
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:
|
|
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,
|
|
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
|
|
652
|
-
|
|
653
|
-
||
|
|
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 =
|
|
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)
|