@swarmclawai/swarmclaw 1.2.0 → 1.2.1
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 +10 -0
- package/package.json +4 -1
- package/src/app/api/chats/[id]/deploy/route.ts +11 -6
- package/src/app/api/chats/[id]/devserver/route.ts +5 -2
- package/src/app/api/chats/[id]/messages/route.ts +7 -1
- package/src/app/api/credentials/[id]/route.ts +4 -1
- package/src/app/api/extensions/marketplace/route.ts +5 -2
- package/src/app/api/memory/maintenance/route.ts +5 -2
- package/src/app/api/preview-server/route.ts +14 -11
- package/src/app/api/system/status/route.ts +11 -0
- package/src/app/api/upload/route.ts +4 -1
- package/src/cli/index.js +7 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-files-editor.tsx +44 -32
- package/src/components/agents/personality-builder.tsx +13 -7
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +1 -0
- package/src/components/chat/message-list.tsx +25 -39
- package/src/components/chat/swarm-status-card.tsx +10 -3
- package/src/components/layout/daemon-indicator.tsx +7 -8
- package/src/components/layout/update-banner.tsx +8 -13
- package/src/components/logs/log-list.tsx +1 -1
- package/src/components/memory/memory-card.tsx +3 -1
- package/src/components/org-chart/org-chart-view.tsx +4 -0
- package/src/components/projects/project-list.tsx +4 -2
- package/src/components/projects/tabs/overview-tab.tsx +3 -2
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +12 -6
- package/src/components/shared/dir-browser.tsx +22 -18
- package/src/components/skills/skill-sheet.tsx +2 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +1 -1
- package/src/hooks/use-openclaw-gateway.ts +46 -27
- package/src/instrumentation.ts +10 -7
- package/src/lib/chat/chat.ts +18 -2
- package/src/lib/providers/anthropic.ts +6 -3
- package/src/lib/providers/claude-cli.ts +9 -3
- package/src/lib/providers/cli-utils.ts +15 -0
- package/src/lib/providers/codex-cli.ts +9 -3
- package/src/lib/providers/gemini-cli.ts +6 -2
- package/src/lib/providers/index.ts +4 -1
- package/src/lib/providers/ollama.ts +5 -2
- package/src/lib/providers/openai.ts +8 -5
- package/src/lib/providers/opencode-cli.ts +6 -2
- package/src/lib/server/agents/agent-registry.ts +20 -3
- package/src/lib/server/agents/main-agent-loop.ts +4 -3
- package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
- package/src/lib/server/chat-execution/chat-execution.ts +14 -2
- package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
- package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
- package/src/lib/server/chat-execution/message-classifier.ts +5 -2
- package/src/lib/server/chat-execution/post-stream-finalization.ts +4 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +11 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +52 -9
- package/src/lib/server/chat-execution/response-completeness.ts +5 -2
- package/src/lib/server/chat-execution/stream-agent-chat.ts +42 -12
- package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
- package/src/lib/server/connectors/bluebubbles.ts +7 -4
- package/src/lib/server/connectors/connector-inbound.ts +16 -13
- package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
- package/src/lib/server/connectors/connector-outbound.ts +6 -3
- package/src/lib/server/connectors/discord.ts +10 -7
- package/src/lib/server/connectors/email.ts +17 -14
- package/src/lib/server/connectors/googlechat.ts +7 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
- package/src/lib/server/connectors/matrix.ts +6 -3
- package/src/lib/server/connectors/openclaw.ts +20 -17
- package/src/lib/server/connectors/outbox.ts +4 -1
- package/src/lib/server/connectors/runtime-state.ts +19 -0
- package/src/lib/server/connectors/session-consolidation.ts +5 -2
- package/src/lib/server/connectors/signal.ts +9 -6
- package/src/lib/server/connectors/slack.ts +13 -10
- package/src/lib/server/connectors/teams.ts +8 -5
- package/src/lib/server/connectors/telegram.ts +15 -12
- package/src/lib/server/connectors/whatsapp.ts +32 -29
- package/src/lib/server/embeddings.ts +4 -1
- package/src/lib/server/link-understanding.ts +4 -1
- package/src/lib/server/memory/memory-abstract.ts +59 -0
- package/src/lib/server/memory/memory-db.ts +40 -14
- package/src/lib/server/missions/mission-service.ts +6 -3
- package/src/lib/server/openclaw/gateway.ts +8 -5
- package/src/lib/server/project-utils.ts +13 -0
- package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
- package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
- package/src/lib/server/provider-health.ts +18 -0
- package/src/lib/server/query-expansion.ts +4 -1
- package/src/lib/server/runtime/alert-dispatch.ts +7 -6
- package/src/lib/server/runtime/daemon-state.ts +189 -50
- package/src/lib/server/runtime/heartbeat-service.ts +23 -0
- package/src/lib/server/runtime/idle-window.ts +4 -1
- package/src/lib/server/runtime/perf.ts +4 -1
- package/src/lib/server/runtime/process-manager.ts +7 -4
- package/src/lib/server/runtime/queue.ts +31 -28
- package/src/lib/server/runtime/scheduler.ts +9 -6
- package/src/lib/server/runtime/session-run-manager.ts +3 -0
- package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
- package/src/lib/server/sandbox/novnc-auth.ts +10 -0
- package/src/lib/server/session-tools/context.ts +14 -0
- package/src/lib/server/session-tools/discovery.ts +9 -6
- package/src/lib/server/session-tools/index.ts +3 -1
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/subagent.ts +23 -2
- package/src/lib/server/session-tools/wallet.ts +4 -1
- package/src/lib/server/skills/clawhub-client.ts +4 -1
- package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
- package/src/lib/server/skills/skill-eligibility.ts +6 -0
- package/src/lib/server/solana.ts +6 -0
- package/src/lib/server/storage-auth.ts +5 -5
- package/src/lib/server/storage-normalization.ts +4 -0
- package/src/lib/server/storage.ts +19 -8
- package/src/lib/server/tasks/task-followups.ts +4 -1
- package/src/lib/server/tool-loop-detection.ts +8 -3
- package/src/lib/server/tool-planning.ts +226 -0
- package/src/lib/server/tool-retry.ts +4 -3
- package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
- package/src/lib/server/ws-hub.ts +5 -2
- package/src/lib/strip-internal-metadata.test.ts +44 -4
- package/src/lib/strip-internal-metadata.ts +20 -6
- package/src/stores/use-approval-store.ts +7 -1
- package/src/stores/use-chat-store.ts +5 -1
- package/src/types/index.ts +6 -0
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
SkillRequirements,
|
|
10
10
|
SkillSecuritySummary,
|
|
11
11
|
} from '@/types'
|
|
12
|
-
import { dedup } from '@/lib/shared-utils'
|
|
12
|
+
import { dedup, hmrSingleton } from '@/lib/shared-utils'
|
|
13
13
|
import { expandExtensionIds, getExtensionAliases, normalizeExtensionId } from '@/lib/server/tool-aliases'
|
|
14
14
|
import { loadLearnedSkills, loadSettings, loadSkills } from '@/lib/server/storage'
|
|
15
15
|
import { cosineSimilarity, getEmbedding } from '@/lib/server/embeddings'
|
|
@@ -160,7 +160,8 @@ const SOURCE_PRIORITY: Record<RuntimeSkillSource, number> = {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
const DEFAULT_RUNTIME_SKILL_TOP_K = 8
|
|
163
|
-
const
|
|
163
|
+
const SKILL_EMBEDDING_CACHE_MAX = 200
|
|
164
|
+
const embeddingCache = hmrSingleton('__swarmclaw_skill_embedding_cache__', () => new Map<string, Promise<number[] | null>>())
|
|
164
165
|
|
|
165
166
|
function normalizeKey(value: string | null | undefined): string {
|
|
166
167
|
return String(value || '')
|
|
@@ -859,6 +860,11 @@ async function getCachedSkillEmbedding(
|
|
|
859
860
|
const key = getSkillEmbeddingCacheKey(skill)
|
|
860
861
|
let cached = embeddingCache.get(key)
|
|
861
862
|
if (!cached) {
|
|
863
|
+
// FIFO eviction when cache exceeds cap
|
|
864
|
+
if (embeddingCache.size >= SKILL_EMBEDDING_CACHE_MAX) {
|
|
865
|
+
const firstKey = embeddingCache.keys().next().value
|
|
866
|
+
if (firstKey !== undefined) embeddingCache.delete(firstKey)
|
|
867
|
+
}
|
|
862
868
|
cached = embeddingResolver(buildSkillEmbeddingText(skill))
|
|
863
869
|
embeddingCache.set(key, cached)
|
|
864
870
|
}
|
|
@@ -10,11 +10,17 @@ export interface SkillEligibilityResult {
|
|
|
10
10
|
reasons: string[]
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const BINARY_CACHE_MAX = 200
|
|
13
14
|
const binaryCache = new Map<string, boolean>()
|
|
14
15
|
|
|
15
16
|
function hasBinary(name: string): boolean {
|
|
16
17
|
const cached = binaryCache.get(name)
|
|
17
18
|
if (cached !== undefined) return cached
|
|
19
|
+
// FIFO eviction at cap
|
|
20
|
+
if (binaryCache.size >= BINARY_CACHE_MAX) {
|
|
21
|
+
const firstKey = binaryCache.keys().next().value
|
|
22
|
+
if (firstKey !== undefined) binaryCache.delete(firstKey)
|
|
23
|
+
}
|
|
18
24
|
try {
|
|
19
25
|
execSync(`which ${name}`, { stdio: 'ignore', timeout: 2000 })
|
|
20
26
|
binaryCache.set(name, true)
|
package/src/lib/server/solana.ts
CHANGED
|
@@ -129,11 +129,17 @@ export function getSolanaExplorerUrl(cluster: SolanaCluster | string | null | un
|
|
|
129
129
|
return `https://explorer.solana.com/${prefix}/${value}${clusterSuffix}`
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
const CONNECTION_CACHE_MAX = 50
|
|
132
133
|
const connectionCache = new Map<string, Connection>()
|
|
133
134
|
|
|
134
135
|
function getCachedConnection(url: string): Connection {
|
|
135
136
|
let conn = connectionCache.get(url)
|
|
136
137
|
if (!conn) {
|
|
138
|
+
// FIFO eviction if cache exceeds cap
|
|
139
|
+
if (connectionCache.size >= CONNECTION_CACHE_MAX) {
|
|
140
|
+
const firstKey = connectionCache.keys().next().value
|
|
141
|
+
if (firstKey !== undefined) connectionCache.delete(firstKey)
|
|
142
|
+
}
|
|
137
143
|
conn = new Connection(url, {
|
|
138
144
|
commitment: 'confirmed',
|
|
139
145
|
disableRetryOnRateLimit: true,
|
|
@@ -3,6 +3,9 @@ import path from 'path'
|
|
|
3
3
|
import crypto from 'crypto'
|
|
4
4
|
|
|
5
5
|
import { DATA_DIR, IS_BUILD_BOOTSTRAP } from './data-dir'
|
|
6
|
+
import { log } from '@/lib/server/logger'
|
|
7
|
+
|
|
8
|
+
const TAG = 'storage-auth'
|
|
6
9
|
|
|
7
10
|
// --- .env loading ---
|
|
8
11
|
function loadEnv() {
|
|
@@ -24,7 +27,7 @@ if (!IS_BUILD_BOOTSTRAP && !process.env.CREDENTIAL_SECRET) {
|
|
|
24
27
|
const envPath = path.join(process.cwd(), '.env.local')
|
|
25
28
|
fs.appendFileSync(envPath, `\nCREDENTIAL_SECRET=${secret}\n`)
|
|
26
29
|
process.env.CREDENTIAL_SECRET = secret
|
|
27
|
-
|
|
30
|
+
log.info(TAG, 'Generated CREDENTIAL_SECRET in .env.local')
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
// Auto-generate ACCESS_KEY if missing (used for simple auth)
|
|
@@ -35,10 +38,7 @@ if (!IS_BUILD_BOOTSTRAP && !process.env.ACCESS_KEY) {
|
|
|
35
38
|
fs.appendFileSync(envPath, `\nACCESS_KEY=${key}\n`)
|
|
36
39
|
process.env.ACCESS_KEY = key
|
|
37
40
|
fs.writeFileSync(SETUP_FLAG, key)
|
|
38
|
-
|
|
39
|
-
console.log(` ACCESS KEY: ${key}`)
|
|
40
|
-
console.log(` Use this key to connect from the browser.`)
|
|
41
|
-
console.log(`${'='.repeat(50)}\n`)
|
|
41
|
+
log.info(TAG, `ACCESS KEY: ${key} — Use this key to connect from the browser.`)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export function getAccessKey(): string {
|
|
@@ -526,5 +526,9 @@ function normalizeStoredRecordInner(
|
|
|
526
526
|
if ('missionSummary' in session) delete session.missionSummary
|
|
527
527
|
// Default geminiSessionId for new field
|
|
528
528
|
if (session.geminiSessionId === undefined) session.geminiSessionId = null
|
|
529
|
+
// Default injectedMemoryIds for proactive recall dedup
|
|
530
|
+
if (!session.injectedMemoryIds || typeof session.injectedMemoryIds !== 'object') {
|
|
531
|
+
session.injectedMemoryIds = {}
|
|
532
|
+
}
|
|
529
533
|
return session
|
|
530
534
|
}
|
|
@@ -6,7 +6,10 @@ import type { ChildProcess } from 'node:child_process'
|
|
|
6
6
|
import Database from 'better-sqlite3'
|
|
7
7
|
|
|
8
8
|
import { perf } from '@/lib/server/runtime/perf'
|
|
9
|
+
import { log } from '@/lib/server/logger'
|
|
9
10
|
import { notify } from '@/lib/server/ws-hub'
|
|
11
|
+
|
|
12
|
+
const TAG = 'storage'
|
|
10
13
|
import { DATA_DIR, IS_BUILD_BOOTSTRAP, WORKSPACE_DIR } from './data-dir'
|
|
11
14
|
import { normalizeHeartbeatSettingFields } from '@/lib/runtime/heartbeat-defaults'
|
|
12
15
|
import { normalizeRuntimeSettingFields } from '@/lib/runtime/runtime-loop'
|
|
@@ -264,8 +267,8 @@ function saveCollection(table: string, data: Record<string, unknown>) {
|
|
|
264
267
|
// partial collection instead of a full load-modify-save. This prevents
|
|
265
268
|
// accidental data wipes (e.g. tests calling saveCredentials with 1 item).
|
|
266
269
|
if (toDelete.length > 0 && next.size > 0 && toDelete.length > next.size) {
|
|
267
|
-
|
|
268
|
-
`
|
|
270
|
+
log.error(TAG,
|
|
271
|
+
`BLOCKED destructive saveCollection("${table}"): ` +
|
|
269
272
|
`would delete ${toDelete.length} rows but only upsert ${next.size}. ` +
|
|
270
273
|
`Use deleteCollectionItem() for explicit deletes or load-modify-save to update.`,
|
|
271
274
|
)
|
|
@@ -537,7 +540,7 @@ const MIGRATION_FLAG = path.join(DATA_DIR, '.sqlite_migrated')
|
|
|
537
540
|
function migrateFromJson() {
|
|
538
541
|
if (fs.existsSync(MIGRATION_FLAG)) return
|
|
539
542
|
|
|
540
|
-
|
|
543
|
+
log.info(TAG, 'Migrating from JSON files to SQLite...')
|
|
541
544
|
|
|
542
545
|
const transaction = db.transaction(() => {
|
|
543
546
|
for (const [table, jsonPath] of Object.entries(JSON_FILES)) {
|
|
@@ -549,7 +552,7 @@ function migrateFromJson() {
|
|
|
549
552
|
for (const [id, val] of Object.entries(data)) {
|
|
550
553
|
ins.run(id, JSON.stringify(val))
|
|
551
554
|
}
|
|
552
|
-
|
|
555
|
+
log.info(TAG, `Migrated ${table}: ${Object.keys(data).length} records`)
|
|
553
556
|
}
|
|
554
557
|
} catch { /* skip malformed files */ }
|
|
555
558
|
}
|
|
@@ -562,7 +565,7 @@ function migrateFromJson() {
|
|
|
562
565
|
const data = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
|
|
563
566
|
if (data && Object.keys(data).length > 0) {
|
|
564
567
|
saveSingleton('settings', data)
|
|
565
|
-
|
|
568
|
+
log.info(TAG, 'Migrated settings')
|
|
566
569
|
}
|
|
567
570
|
} catch { /* skip */ }
|
|
568
571
|
}
|
|
@@ -574,7 +577,7 @@ function migrateFromJson() {
|
|
|
574
577
|
const data = JSON.parse(fs.readFileSync(queuePath, 'utf8'))
|
|
575
578
|
if (Array.isArray(data) && data.length > 0) {
|
|
576
579
|
saveSingleton('queue', data)
|
|
577
|
-
|
|
580
|
+
log.info(TAG, `Migrated queue: ${data.length} items`)
|
|
578
581
|
}
|
|
579
582
|
} catch { /* skip */ }
|
|
580
583
|
}
|
|
@@ -592,14 +595,14 @@ function migrateFromJson() {
|
|
|
592
595
|
}
|
|
593
596
|
}
|
|
594
597
|
}
|
|
595
|
-
|
|
598
|
+
log.info(TAG, 'Migrated usage records')
|
|
596
599
|
} catch { /* skip */ }
|
|
597
600
|
}
|
|
598
601
|
})
|
|
599
602
|
|
|
600
603
|
transaction()
|
|
601
604
|
fs.writeFileSync(MIGRATION_FLAG, new Date().toISOString())
|
|
602
|
-
|
|
605
|
+
log.info(TAG, 'Migration complete. JSON files preserved as backup.')
|
|
603
606
|
}
|
|
604
607
|
|
|
605
608
|
if (!IS_BUILD_BOOTSTRAP) {
|
|
@@ -1400,6 +1403,14 @@ export function appendUsage(sessionId: string, record: unknown) {
|
|
|
1400
1403
|
ins.run(sessionId, JSON.stringify(record))
|
|
1401
1404
|
}
|
|
1402
1405
|
|
|
1406
|
+
export function pruneOldUsage(maxAgeMs: number): number {
|
|
1407
|
+
const cutoff = Date.now() - maxAgeMs
|
|
1408
|
+
const result = db.prepare(
|
|
1409
|
+
`DELETE FROM usage WHERE CAST(COALESCE(json_extract(data, '$.timestamp'), 0) AS INTEGER) < ?`
|
|
1410
|
+
).run(cutoff)
|
|
1411
|
+
return result.changes
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1403
1414
|
// --- Connectors ---
|
|
1404
1415
|
const connectorsStore = createCollectionStore('connectors', { ttlMs: 30_000 })
|
|
1405
1416
|
export const loadConnectors = connectorsStore.load
|
|
@@ -7,6 +7,9 @@ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
|
7
7
|
import { loadConnectors, loadSessions, UPLOAD_DIR } from '@/lib/server/storage'
|
|
8
8
|
import { errorMessage } from '@/lib/shared-utils'
|
|
9
9
|
import { isMainSession } from '@/lib/server/agents/main-agent-loop'
|
|
10
|
+
import { log } from '@/lib/server/logger'
|
|
11
|
+
|
|
12
|
+
const TAG = 'task-followups'
|
|
10
13
|
|
|
11
14
|
export { normalizeWhatsappTarget }
|
|
12
15
|
|
|
@@ -507,7 +510,7 @@ export async function notifyConnectorTaskFollowups(params: {
|
|
|
507
510
|
})
|
|
508
511
|
} catch (err: unknown) {
|
|
509
512
|
const errMsg = errorMessage(err)
|
|
510
|
-
|
|
513
|
+
log.warn(TAG, `Failed task follow-up send on connector ${target.connectorId}: ${errMsg}`)
|
|
511
514
|
}
|
|
512
515
|
}
|
|
513
516
|
}
|
|
@@ -71,9 +71,9 @@ const DEFAULT_THRESHOLDS: LoopDetectionThresholds = {
|
|
|
71
71
|
pollCritical: 8,
|
|
72
72
|
pingPongWarn: 3,
|
|
73
73
|
pingPongCritical: 5,
|
|
74
|
-
circuitBreaker:
|
|
75
|
-
toolFrequencyWarn:
|
|
76
|
-
toolFrequencyCritical:
|
|
74
|
+
circuitBreaker: 15,
|
|
75
|
+
toolFrequencyWarn: 12,
|
|
76
|
+
toolFrequencyCritical: 25,
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// ---------------------------------------------------------------------------
|
|
@@ -182,6 +182,11 @@ export class ToolLoopTracker {
|
|
|
182
182
|
this.keyCount.clear()
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/** Partial reset: clear per-tool frequency counts but preserve circuit breaker and repeat history. */
|
|
186
|
+
resetFrequencyCounts(): void {
|
|
187
|
+
this.nameCount.clear()
|
|
188
|
+
}
|
|
189
|
+
|
|
185
190
|
/** Get the full call history (for diagnostics). */
|
|
186
191
|
getHistory(): ReadonlyArray<ToolCallRecord> {
|
|
187
192
|
return this.history
|
|
@@ -39,6 +39,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
39
39
|
capabilities: ['artifact.files'],
|
|
40
40
|
disciplineGuidance: [
|
|
41
41
|
'For `files`, include an explicit action whenever possible. Common patterns: `{"action":"list","dirPath":"."}`, `{"action":"read","filePath":"path/to/file.md"}`, and `{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}`.',
|
|
42
|
+
'Prefer a single write call with multiple files over writing one file at a time.',
|
|
42
43
|
],
|
|
43
44
|
requestMatchers: [],
|
|
44
45
|
},
|
|
@@ -49,6 +50,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
49
50
|
capabilities: ['runtime.shell'],
|
|
50
51
|
disciplineGuidance: [
|
|
51
52
|
'For `shell`, use `{"action":"execute","command":"..."}` for commands and `{"action":"status","processId":"..."}` or `{"action":"log","processId":"..."}` for long-lived processes.',
|
|
53
|
+
'Chain related commands in a single shell call using && to reduce round-trips. Avoid running the same build or test command repeatedly — if it fails, diagnose the error before retrying.',
|
|
52
54
|
],
|
|
53
55
|
requestMatchers: [],
|
|
54
56
|
},
|
|
@@ -59,6 +61,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
59
61
|
capabilities: [TOOL_CAPABILITY.researchSearch],
|
|
60
62
|
disciplineGuidance: [
|
|
61
63
|
'For `web_search`, use `{"query":"..."}` to research fresh information. For current events, breaking news, or "latest" requests, start with `web_search` before summarizing.',
|
|
64
|
+
'Gather 2-3 key sources, then synthesize. Do not search-read-search-read in a loop.',
|
|
62
65
|
],
|
|
63
66
|
requestMatchers: [
|
|
64
67
|
{
|
|
@@ -73,6 +76,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
73
76
|
capabilities: [TOOL_CAPABILITY.researchFetch],
|
|
74
77
|
disciplineGuidance: [
|
|
75
78
|
'For `web_fetch`, use `{"url":"https://..."}` to read a specific page or article after you know the URL.',
|
|
79
|
+
'Fetch the pages you need, then synthesize. Do not fetch-read-fetch-read in a loop.',
|
|
76
80
|
],
|
|
77
81
|
requestMatchers: [
|
|
78
82
|
{
|
|
@@ -91,6 +95,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
91
95
|
'For `browser`, when the task includes a literal URL, pass that exact URL string to `{"action":"navigate","url":"..."}`. Do not invent placeholder URLs like `[Your URL]`, `Example_URL`, or `MockMailPage_URL`.',
|
|
92
96
|
'For `browser` form work, prefer `{"action":"fill_form","fields":[{"element":"#email","value":"user@example.com"},{"element":"#password","value":"..."}]}`. A shorthand `form` object keyed by input id/name also works for simple forms.',
|
|
93
97
|
'Use `browser` when the user asks for screenshots, visual proof, page capture, PDFs, or a rendered view of a page. `navigate` alone is not a screenshot.',
|
|
98
|
+
'Limit browser navigations to what is needed. Each navigation is expensive. Plan your browser session: list the pages you need, visit each once, extract what you need.',
|
|
94
99
|
],
|
|
95
100
|
requestMatchers: [
|
|
96
101
|
{
|
|
@@ -117,6 +122,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
117
122
|
'For outbound delivery, inspect available channels with `connector_message_tool` using `{"action":"list_running"}` before claiming something cannot be sent.',
|
|
118
123
|
'Use `connector_message_tool` with `{"action":"send","message":"...","mediaPath":"..."}` for text/media and `{"action":"send_voice_note","voiceText":"..."}` for voice notes.',
|
|
119
124
|
'If no channel or recipient is configured, explain that connector/channel setup is missing rather than claiming the capability does not exist.',
|
|
125
|
+
'Check channel availability once with `list_running`, then send. Do not re-list channels between each message.',
|
|
120
126
|
],
|
|
121
127
|
requestMatchers: [
|
|
122
128
|
{
|
|
@@ -140,6 +146,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
140
146
|
capabilities: ['network.http'],
|
|
141
147
|
disciplineGuidance: [
|
|
142
148
|
'For `http_request`, send exact literal URLs from the task or from prior tool results. Keep JSON request bodies as raw JSON strings.',
|
|
149
|
+
'If an API call fails, inspect the error before retrying with the same request. Do not retry the same failing call in a loop.',
|
|
143
150
|
],
|
|
144
151
|
requestMatchers: [],
|
|
145
152
|
},
|
|
@@ -150,6 +157,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
150
157
|
capabilities: ['delivery.email'],
|
|
151
158
|
disciplineGuidance: [
|
|
152
159
|
'For `email`, send mail with `{"action":"send","to":"user@example.com","subject":"...","body":"..."}`. If delivery depends on SMTP setup, check `{"action":"status"}` before claiming success.',
|
|
160
|
+
'Compose the full message in one send call. Do not send partial drafts followed by corrections.',
|
|
153
161
|
],
|
|
154
162
|
requestMatchers: [],
|
|
155
163
|
},
|
|
@@ -162,6 +170,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
162
170
|
'For `google_workspace`, pass exact `gws` arguments in `{"args":[...]}` form. Prefer list/get/read commands first to confirm IDs and current state before mutating Drive, Docs, Sheets, Gmail, Calendar, or Chat resources.',
|
|
163
171
|
'Use `params` and `jsonInput` for `--params` / `--json` payloads instead of packing raw JSON blobs into the `args` array.',
|
|
164
172
|
'Do not call interactive `gws auth login` or `gws auth setup` from the agent. Use the extension settings or a pre-authenticated `gws` install.',
|
|
173
|
+
'Confirm resource IDs with a single list/get call before mutating. Do not repeatedly list the same resources between edits.',
|
|
165
174
|
],
|
|
166
175
|
requestMatchers: [
|
|
167
176
|
{
|
|
@@ -179,6 +188,223 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
179
188
|
'For `ask_human`, when a workflow needs a code, approval, or out-of-band value from a person, do not guess or keep re-submitting blank forms. Use `{"action":"request_input","question":"..."}` and, for durable pauses, `{"action":"wait_for_reply","correlationId":"..."}`.',
|
|
180
189
|
'Reuse the same `correlationId` from `request_input` when you call `wait_for_reply`. Once the durable wait returns active, stop the turn immediately and wait for the reply instead of calling `request_input` again.',
|
|
181
190
|
'Do not ask the same pending human question twice before the durable wait resumes unless the question materially changes.',
|
|
191
|
+
'Batch related questions into a single request rather than asking one question at a time.',
|
|
192
|
+
],
|
|
193
|
+
requestMatchers: [],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
|
|
197
|
+
// --- Internal platform tools ---
|
|
198
|
+
|
|
199
|
+
manage_agents: [
|
|
200
|
+
{
|
|
201
|
+
toolName: 'manage_agents',
|
|
202
|
+
capabilities: ['platform.agents'],
|
|
203
|
+
disciplineGuidance: [
|
|
204
|
+
'List agents once at the start of a task, then work with specific agent IDs. Do not re-list between each action.',
|
|
205
|
+
],
|
|
206
|
+
requestMatchers: [],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
manage_projects: [
|
|
210
|
+
{
|
|
211
|
+
toolName: 'manage_projects',
|
|
212
|
+
capabilities: ['platform.projects'],
|
|
213
|
+
disciplineGuidance: [
|
|
214
|
+
'List projects once to orient, then operate on specific project IDs. Do not re-list after each update.',
|
|
215
|
+
],
|
|
216
|
+
requestMatchers: [],
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
manage_tasks: [
|
|
220
|
+
{
|
|
221
|
+
toolName: 'manage_tasks',
|
|
222
|
+
capabilities: ['platform.tasks'],
|
|
223
|
+
disciplineGuidance: [
|
|
224
|
+
'Read the task list once, make your changes, then move on. Do not re-read the task list after every update.',
|
|
225
|
+
],
|
|
226
|
+
requestMatchers: [],
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
manage_schedules: [
|
|
230
|
+
{
|
|
231
|
+
toolName: 'manage_schedules',
|
|
232
|
+
capabilities: ['platform.schedules'],
|
|
233
|
+
disciplineGuidance: [
|
|
234
|
+
'List schedules once to check current state. Do not re-list after each modification.',
|
|
235
|
+
],
|
|
236
|
+
requestMatchers: [],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
manage_skills: [
|
|
240
|
+
{
|
|
241
|
+
toolName: 'manage_skills',
|
|
242
|
+
capabilities: ['platform.skills'],
|
|
243
|
+
disciplineGuidance: [
|
|
244
|
+
'Use `recommend_for_task` to find a relevant skill efficiently. Do not repeatedly list or search skills between each action.',
|
|
245
|
+
],
|
|
246
|
+
requestMatchers: [],
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
manage_webhooks: [
|
|
250
|
+
{
|
|
251
|
+
toolName: 'manage_webhooks',
|
|
252
|
+
capabilities: ['platform.webhooks'],
|
|
253
|
+
disciplineGuidance: [
|
|
254
|
+
'List webhooks once for current state. Do not re-list after each change.',
|
|
255
|
+
],
|
|
256
|
+
requestMatchers: [],
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
manage_secrets: [
|
|
260
|
+
{
|
|
261
|
+
toolName: 'manage_secrets',
|
|
262
|
+
capabilities: ['platform.secrets'],
|
|
263
|
+
disciplineGuidance: [
|
|
264
|
+
'Store secrets directly. Use the `check` action (not `list`) to verify if a credential already exists before requesting a new one.',
|
|
265
|
+
],
|
|
266
|
+
requestMatchers: [],
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
manage_chatrooms: [
|
|
270
|
+
{
|
|
271
|
+
toolName: 'manage_chatrooms',
|
|
272
|
+
capabilities: ['platform.chatrooms'],
|
|
273
|
+
disciplineGuidance: [
|
|
274
|
+
'List chatrooms once to orient, then operate on specific IDs. Do not re-list after each message or update.',
|
|
275
|
+
],
|
|
276
|
+
requestMatchers: [],
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
manage_protocols: [
|
|
280
|
+
{
|
|
281
|
+
toolName: 'manage_protocols',
|
|
282
|
+
capabilities: ['platform.protocols'],
|
|
283
|
+
disciplineGuidance: [
|
|
284
|
+
'Read the protocol definition once, then execute steps. Do not re-read the protocol between each step.',
|
|
285
|
+
],
|
|
286
|
+
requestMatchers: [],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
manage_platform: [
|
|
290
|
+
{
|
|
291
|
+
toolName: 'manage_platform',
|
|
292
|
+
capabilities: ['platform.umbrella'],
|
|
293
|
+
disciplineGuidance: [
|
|
294
|
+
'Prefer the direct `manage_*` tools (manage_agents, manage_tasks, etc.) when they are enabled. Use `manage_platform` only as a fallback when the specific tool is not available.',
|
|
295
|
+
],
|
|
296
|
+
requestMatchers: [],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
spawn_subagent: [
|
|
300
|
+
{
|
|
301
|
+
toolName: 'spawn_subagent',
|
|
302
|
+
capabilities: ['delegation.subagent'],
|
|
303
|
+
disciplineGuidance: [
|
|
304
|
+
'Use `waitForCompletion: true` (the default) or `wait`/`wait_all` actions to await results. Do not poll `status` in a loop.',
|
|
305
|
+
'Batch related delegations — spawn multiple subagents at once if tasks are independent.',
|
|
306
|
+
'For multi-step or cross-domain work, delegate to a subagent rather than attempting everything in one long tool chain.',
|
|
307
|
+
],
|
|
308
|
+
requestMatchers: [],
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
delegate: [
|
|
312
|
+
{
|
|
313
|
+
toolName: 'delegate',
|
|
314
|
+
capabilities: ['delegation.cli'],
|
|
315
|
+
disciplineGuidance: [
|
|
316
|
+
'Give the delegate a complete task description in one call. Do not send incremental instructions across multiple delegation calls.',
|
|
317
|
+
],
|
|
318
|
+
requestMatchers: [],
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
manage_sessions: [
|
|
322
|
+
{
|
|
323
|
+
toolName: 'sessions_tool',
|
|
324
|
+
capabilities: ['platform.sessions'],
|
|
325
|
+
disciplineGuidance: [
|
|
326
|
+
'Check session identity once at the start. Do not re-query session info between each action.',
|
|
327
|
+
],
|
|
328
|
+
requestMatchers: [],
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
memory: [
|
|
332
|
+
{
|
|
333
|
+
toolName: 'memory_tool',
|
|
334
|
+
capabilities: ['memory.search', 'memory.store'],
|
|
335
|
+
disciplineGuidance: [
|
|
336
|
+
'Search memory once with a good query, then use the results. Do not run multiple overlapping searches for the same topic.',
|
|
337
|
+
'For stores and updates, write once with complete content. Do not read-back immediately after writing to confirm.',
|
|
338
|
+
],
|
|
339
|
+
requestMatchers: [],
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
context_mgmt: [
|
|
343
|
+
{
|
|
344
|
+
toolName: 'context_status',
|
|
345
|
+
capabilities: ['context.management'],
|
|
346
|
+
disciplineGuidance: [
|
|
347
|
+
'Check context status only when you suspect you are running low. Do not check after every tool call.',
|
|
348
|
+
],
|
|
349
|
+
requestMatchers: [],
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
monitor: [
|
|
353
|
+
{
|
|
354
|
+
toolName: 'monitor_tool',
|
|
355
|
+
capabilities: ['monitoring.watch'],
|
|
356
|
+
disciplineGuidance: [
|
|
357
|
+
'Prefer `wait_until`, `wait_for_http`, `wait_for_file`, or other `wait_for_*` shortcut actions — they create a durable wait that resumes your turn automatically. Avoid creating a watch with `create_watch` then polling `get_watch` in a loop.',
|
|
358
|
+
],
|
|
359
|
+
requestMatchers: [],
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
wallet: [
|
|
363
|
+
{
|
|
364
|
+
toolName: 'wallet_tool',
|
|
365
|
+
capabilities: [TOOL_CAPABILITY.walletInspect, TOOL_CAPABILITY.walletExecute],
|
|
366
|
+
disciplineGuidance: [
|
|
367
|
+
'Inspect wallet state once, then act. Use `simulate_transaction` to validate before executing. Do not re-inspect balances between each operation unless the operation changes them.',
|
|
368
|
+
],
|
|
369
|
+
requestMatchers: [],
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
image_gen: [
|
|
373
|
+
{
|
|
374
|
+
toolName: 'generate_image',
|
|
375
|
+
capabilities: ['media.image_generation'],
|
|
376
|
+
disciplineGuidance: [
|
|
377
|
+
'Describe the image fully in one generation call. Do not generate multiple variations unless the user asks for options.',
|
|
378
|
+
],
|
|
379
|
+
requestMatchers: [],
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
replicate: [
|
|
383
|
+
{
|
|
384
|
+
toolName: 'replicate',
|
|
385
|
+
capabilities: ['media.replicate'],
|
|
386
|
+
disciplineGuidance: [
|
|
387
|
+
'Submit the job with complete parameters in one call. Use `wait: true` for synchronous completion. If running async, let the built-in polling handle it — do not add your own polling loop on top.',
|
|
388
|
+
],
|
|
389
|
+
requestMatchers: [],
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
schedule_wake: [
|
|
393
|
+
{
|
|
394
|
+
toolName: 'schedule_wake',
|
|
395
|
+
capabilities: ['runtime.schedule'],
|
|
396
|
+
disciplineGuidance: [
|
|
397
|
+
'Schedule the wake once with the correct time. Do not reschedule repeatedly to adjust by small increments.',
|
|
398
|
+
],
|
|
399
|
+
requestMatchers: [],
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
mailbox: [
|
|
403
|
+
{
|
|
404
|
+
toolName: 'mailbox',
|
|
405
|
+
capabilities: ['delivery.mailbox'],
|
|
406
|
+
disciplineGuidance: [
|
|
407
|
+
'Use `search_messages` for targeted retrieval instead of listing all messages. Do not poll the inbox in a loop waiting for replies.',
|
|
182
408
|
],
|
|
183
409
|
requestMatchers: [],
|
|
184
410
|
},
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { sleep, jitteredBackoff } from '@/lib/shared-utils'
|
|
6
|
+
import { log } from '@/lib/server/logger'
|
|
7
|
+
|
|
8
|
+
const TAG = 'tool-retry'
|
|
6
9
|
|
|
7
10
|
export interface RetryOptions {
|
|
8
11
|
maxAttempts?: number
|
|
@@ -51,9 +54,7 @@ export async function withRetry<TArgs>(
|
|
|
51
54
|
if (attempt < maxAttempts && isRetryableError(lastResult, retryable)) {
|
|
52
55
|
await opts?.onRetry?.(attempt, lastResult)
|
|
53
56
|
const delay = jitteredBackoff(backoffMs, attempt - 1, backoffMs * 16)
|
|
54
|
-
|
|
55
|
-
`[tool-retry] Attempt ${attempt}/${maxAttempts} matched retryable pattern, retrying in ${delay}ms`,
|
|
56
|
-
)
|
|
57
|
+
log.warn(TAG, `Attempt ${attempt}/${maxAttempts} matched retryable pattern, retrying in ${delay}ms`)
|
|
57
58
|
await sleep(delay)
|
|
58
59
|
continue
|
|
59
60
|
}
|
|
@@ -84,9 +84,37 @@ export interface GetWalletPortfolioOptions {
|
|
|
84
84
|
allowStale?: boolean
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
const PORTFOLIO_CACHE_MAX = 200
|
|
87
88
|
const portfolioCache = new Map<string, WalletPortfolioCacheEntry>()
|
|
88
89
|
const evmContractDiscoveryCache = new Map<string, EvmContractDiscoveryCacheEntry>()
|
|
89
90
|
|
|
91
|
+
function pruneExpiredPortfolioCaches(): void {
|
|
92
|
+
const now = Date.now()
|
|
93
|
+
for (const [key, entry] of portfolioCache) {
|
|
94
|
+
if (entry.expiresAt <= now) portfolioCache.delete(key)
|
|
95
|
+
}
|
|
96
|
+
for (const [key, entry] of evmContractDiscoveryCache) {
|
|
97
|
+
if (entry.expiresAt <= now) evmContractDiscoveryCache.delete(key)
|
|
98
|
+
}
|
|
99
|
+
// Hard cap as safety net
|
|
100
|
+
if (portfolioCache.size > PORTFOLIO_CACHE_MAX) {
|
|
101
|
+
const excess = portfolioCache.size - PORTFOLIO_CACHE_MAX
|
|
102
|
+
const iter = portfolioCache.keys()
|
|
103
|
+
for (let i = 0; i < excess; i++) {
|
|
104
|
+
const k = iter.next().value
|
|
105
|
+
if (k !== undefined) portfolioCache.delete(k)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (evmContractDiscoveryCache.size > PORTFOLIO_CACHE_MAX) {
|
|
109
|
+
const excess = evmContractDiscoveryCache.size - PORTFOLIO_CACHE_MAX
|
|
110
|
+
const iter = evmContractDiscoveryCache.keys()
|
|
111
|
+
for (let i = 0; i < excess; i++) {
|
|
112
|
+
const k = iter.next().value
|
|
113
|
+
if (k !== undefined) evmContractDiscoveryCache.delete(k)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
const KNOWN_SOLANA_TOKENS: Record<string, { symbol: string; name: string }> = {
|
|
91
119
|
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: { symbol: 'USDC', name: 'USD Coin' },
|
|
92
120
|
}
|
|
@@ -721,6 +749,7 @@ export async function getWalletPortfolio(wallet: AgentWallet, options?: GetWalle
|
|
|
721
749
|
label: `wallet portfolio ${wallet.id}`,
|
|
722
750
|
})
|
|
723
751
|
|
|
752
|
+
pruneExpiredPortfolioCaches()
|
|
724
753
|
portfolioCache.set(cacheKey, {
|
|
725
754
|
expiresAt: Date.now() + PORTFOLIO_CACHE_TTL_MS,
|
|
726
755
|
portfolio,
|
package/src/lib/server/ws-hub.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { WebSocketServer, WebSocket } from 'ws'
|
|
|
2
2
|
import type { IncomingMessage } from 'http'
|
|
3
3
|
import { validateAccessKey } from './storage'
|
|
4
4
|
import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
|
|
5
|
+
import { log } from '@/lib/server/logger'
|
|
6
|
+
|
|
7
|
+
const TAG = 'ws-hub'
|
|
5
8
|
|
|
6
9
|
interface WsClient {
|
|
7
10
|
ws: WebSocket
|
|
@@ -67,10 +70,10 @@ export function initWsServer() {
|
|
|
67
70
|
})
|
|
68
71
|
|
|
69
72
|
wss.on('error', (err) => {
|
|
70
|
-
|
|
73
|
+
log.error(TAG, 'WebSocket server error:', err.message)
|
|
71
74
|
})
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
log.info(TAG, `WebSocket server listening on port ${port}`)
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
export function closeWsServer(): Promise<void> {
|