repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | /** |
| #2 | * leviathan/src/agent/loop.ts — SENSE → THINK → STRIKE → DRIFT |
| #3 | * |
| #4 | * The core tail-flick loop powered by Anthropic Claude via the |
| #5 | * Anthropic ACP (Agent Control Protocol) / claude-code SDK. |
| #6 | * |
| #7 | * Each tick: |
| #8 | * 1. SENSE — Build observations (balances, SHELL.md, history) |
| #9 | * 2. THINK — Call Claude with fresh system prompt (no history) |
| #10 | * 3. STRIKE — Execute the tool Claude chose |
| #11 | * 4. DRIFT — Observe result, update SHELL.md, journal the tick |
| #12 | * |
| #13 | * The depth tier gates model choice and tool surface. |
| #14 | * The Three Laws are injected into every system prompt. |
| #15 | * No private keys in this file — signing is handled by the identity module. |
| #16 | */ |
| #17 | |
| #18 | import Anthropic from '@anthropic-ai/sdk'; |
| #19 | import type { Tool, MessageParam } from '@anthropic-ai/sdk/resources/messages.js'; |
| #20 | import { buildSystemPrompt, buildShorelinePrompt } from './system-prompt.js'; |
| #21 | import { computeDepth, selectModel, isActionAllowed, formatDepth } from '../survival.js'; |
| #22 | import { assertConstitutionIntact } from '../three-laws.js'; |
| #23 | import { appendStrike } from '../state/index.js'; |
| #24 | import type { ClawState, ClawStrike, ClawdMemoryKind, TailFlickEvent } from '../types.js'; |
| #25 | import { TOOLS } from './tools.js'; |
| #26 | import { Percolator } from './percolator.js'; |
| #27 | import { VulcanClient } from './vulcan.js'; |
| #28 | import { getWallet } from './wallet.js'; |
| #29 | import { |
| #30 | loadLeviathanMemoryContext, |
| #31 | recallClawdMemory, |
| #32 | rememberClawdMemory, |
| #33 | rememberLeviathanStrike, |
| #34 | researchClawdMemory, |
| #35 | } from '../memory/clawd.js'; |
| #36 | |
| #37 | export interface TailFlickResult { |
| #38 | tick: number; |
| #39 | strike: ClawStrike; |
| #40 | depthChanged: boolean; |
| #41 | beached: boolean; |
| #42 | newShellMd?: string; |
| #43 | event: TailFlickEvent; |
| #44 | } |
| #45 | |
| #46 | /** |
| #47 | * Run one tail-flick (one OODA tick of the leviathan). |
| #48 | * Returns the result — caller persists to shell.db + journals. |
| #49 | */ |
| #50 | export async function tailFlick( |
| #51 | state: ClawState, |
| #52 | spawnPrompt: string, |
| #53 | client: Anthropic, |
| #54 | strikeHistory: ClawStrike[], |
| #55 | ): Promise<TailFlickResult> { |
| #56 | const tick = state.tickCount + 1; |
| #57 | const now = new Date().toISOString(); |
| #58 | |
| #59 | // Assert constitution is intact before every tick |
| #60 | assertConstitutionIntact(state.identity.constitutionHash); |
| #61 | |
| #62 | // Compute current depth |
| #63 | const newDepth = computeDepth(state.usdcBalance); |
| #64 | const depthChanged = newDepth !== state.depth; |
| #65 | const beached = newDepth === 'beached'; |
| #66 | |
| #67 | const emit = (detail?: unknown): TailFlickEvent => ({ |
| #68 | event: 'pulse', |
| #69 | tick, |
| #70 | now, |
| #71 | depth: newDepth, |
| #72 | usdcBalance: state.usdcBalance, |
| #73 | detail, |
| #74 | }); |
| #75 | |
| #76 | if (beached) { |
| #77 | const strike: ClawStrike = { |
| #78 | id: `strike-${tick}`, |
| #79 | tick, |
| #80 | action: 'beach', |
| #81 | success: true, |
| #82 | timestamp: now, |
| #83 | }; |
| #84 | return { |
| #85 | tick, |
| #86 | strike, |
| #87 | depthChanged, |
| #88 | beached: true, |
| #89 | event: { ...emit(), event: 'beach' }, |
| #90 | }; |
| #91 | } |
| #92 | |
| #93 | // Build per-tick observations for user message |
| #94 | const historyBlock = strikeHistory.slice(-3).map((s, i) => |
| #95 | `Strike ${i + 1}: action=${s.action} tool=${s.tool ?? 'none'} ` + |
| #96 | `success=${s.success} output=${JSON.stringify(s.output ?? '').slice(0, 120)}`, |
| #97 | ).join('\n'); |
| #98 | |
| #99 | const memoryContext = await loadLeviathanMemoryContext(state, strikeHistory, { |
| #100 | bank: 'clawd', |
| #101 | timeoutMs: 8_000, |
| #102 | }); |
| #103 | |
| #104 | const userMessage = [ |
| #105 | `Tick: ${tick}`, |
| #106 | `Depth: ${formatDepth(newDepth, state.usdcBalance)}`, |
| #107 | `Balances: USDC=$${state.usdcBalance.toFixed(4)} SOL=${state.solBalance.toFixed(4)} CLAWD=${state.clawdBalance.toFixed(0)}`, |
| #108 | `Open trades: ${state.openTrades}`, |
| #109 | `Spawnlings: ${state.spawnlings.length}`, |
| #110 | '', |
| #111 | 'Recent strikes:', |
| #112 | historyBlock || '(none yet)', |
| #113 | '', |
| #114 | 'Clawd Memory recall:', |
| #115 | memoryContext.text, |
| #116 | '', |
| #117 | 'Choose your next action. Call exactly one tool.', |
| #118 | ].join('\n'); |
| #119 | |
| #120 | // Build system prompt (shoreline gets condensed version to save tokens) |
| #121 | const systemPrompt = newDepth === 'shoreline' |
| #122 | ? buildShorelinePrompt(state) |
| #123 | : buildSystemPrompt(state, spawnPrompt); |
| #124 | |
| #125 | const model = selectModel(newDepth); |
| #126 | if (!model) throw new Error('beached — no model available'); |
| #127 | |
| #128 | // Filter tools to depth-allowed surface |
| #129 | const allowedTools: Tool[] = TOOLS.filter(t => |
| #130 | isActionAllowed(newDepth, t.name as string) || t.name === 'hold', |
| #131 | ); |
| #132 | |
| #133 | // ── THINK (Claude ACP call — fresh context, no conversation history) ───────── |
| #134 | const messages: MessageParam[] = [{ role: 'user', content: userMessage }]; |
| #135 | |
| #136 | const response = await client.messages.create({ |
| #137 | model, |
| #138 | max_tokens: 1024, |
| #139 | system: systemPrompt, |
| #140 | tools: allowedTools, |
| #141 | messages, |
| #142 | // ACP: stop when Claude picks a tool (one strike per tick) |
| #143 | tool_choice: { type: 'auto' }, |
| #144 | }); |
| #145 | |
| #146 | // ── STRIKE ──────────────────────────────────────────────────────────────── |
| #147 | const toolUse = response.content.find(b => b.type === 'tool_use'); |
| #148 | const textBlock = response.content.find(b => b.type === 'text') as { type: 'text'; text: string } | undefined; |
| #149 | |
| #150 | let strike: ClawStrike; |
| #151 | |
| #152 | if (!toolUse || toolUse.type !== 'tool_use') { |
| #153 | // Claude returned text only — treat as hold |
| #154 | strike = { |
| #155 | id: `strike-${tick}`, |
| #156 | tick, |
| #157 | action: 'hold', |
| #158 | success: true, |
| #159 | output: textBlock?.text?.slice(0, 200), |
| #160 | timestamp: now, |
| #161 | }; |
| #162 | } else { |
| #163 | // Execute the chosen tool |
| #164 | const toolResult = await executeTool(toolUse.name, toolUse.input as Record<string, unknown>, state, newDepth); |
| #165 | strike = { |
| #166 | id: `strike-${tick}`, |
| #167 | tick, |
| #168 | action: mapToolToAction(toolUse.name), |
| #169 | tool: toolUse.name, |
| #170 | input: toolUse.input, |
| #171 | output: toolResult.output, |
| #172 | costUsdc: toolResult.costUsdc, |
| #173 | success: toolResult.success, |
| #174 | timestamp: now, |
| #175 | }; |
| #176 | } |
| #177 | |
| #178 | // Journal this strike for future ticks |
| #179 | appendStrike(strike); |
| #180 | void rememberLeviathanStrike(state, strike, { |
| #181 | bank: 'clawd', |
| #182 | timeoutMs: 8_000, |
| #183 | }).catch(() => undefined); |
| #184 | |
| #185 | // ── DRIFT ──────────────────────────────────────────────────────────────── |
| #186 | // Update SHELL.md if shell_write was called |
| #187 | const newShellMd = strike.tool === 'shell_write' |
| #188 | ? String((strike.input as { content?: string })?.content ?? state.shellMd) |
| #189 | : undefined; |
| #190 | |
| #191 | return { |
| #192 | tick, |
| #193 | strike, |
| #194 | depthChanged, |
| #195 | beached: false, |
| #196 | newShellMd, |
| #197 | event: emit(strike), |
| #198 | }; |
| #199 | } |
| #200 | |
| #201 | // ─── Tool executor ──────────────────────────────────────────────────────────── |
| #202 | |
| #203 | interface ToolResult { |
| #204 | output: unknown; |
| #205 | success: boolean; |
| #206 | costUsdc?: number; |
| #207 | } |
| #208 | |
| #209 | async function executeTool( |
| #210 | name: string, |
| #211 | input: Record<string, unknown>, |
| #212 | state: ClawState, |
| #213 | depth: string, |
| #214 | ): Promise<ToolResult> { |
| #215 | switch (name) { |
| #216 | |
| #217 | // ── Solana / wallet ──────────────────────────────────────────────────── |
| #218 | case 'solana_balance': { |
| #219 | const wallet = getWallet(); |
| #220 | try { |
| #221 | const brief = await wallet.brief(); |
| #222 | return { output: brief, success: true }; |
| #223 | } catch (e) { |
| #224 | return { output: String(e), success: false }; |
| #225 | } |
| #226 | } |
| #227 | |
| #228 | case 'wallet_brief': { |
| #229 | const wallet = getWallet(); |
| #230 | try { |
| #231 | const brief = await wallet.brief(); |
| #232 | return { output: brief, success: true }; |
| #233 | } catch (e) { |
| #234 | return { output: String(e), success: false }; |
| #235 | } |
| #236 | } |
| #237 | |
| #238 | case 'helius_transactions': { |
| #239 | const address = String(input['address'] ?? state.identity.pubkey); |
| #240 | const apiKey = process.env['HELIUS_API_KEY']; |
| #241 | if (!apiKey) return { output: 'HELIUS_API_KEY not set', success: false }; |
| #242 | try { |
| #243 | const url = `https://api.helius.xyz/v0/addresses/${address}/transactions?api-key=${apiKey}&limit=10`; |
| #244 | const res = await fetch(url, { signal: AbortSignal.timeout(10_000) }); |
| #245 | if (!res.ok) return { output: `Helius ${res.status}`, success: false }; |
| #246 | const txs = (await res.json()) as unknown[]; |
| #247 | return { output: txs.slice(0, 5), success: true }; |
| #248 | } catch (e) { |
| #249 | return { output: String(e), success: false }; |
| #250 | } |
| #251 | } |
| #252 | |
| #253 | // ── Clawd Memory ────────────────────────────────────────────────────── |
| #254 | case 'clawd_memory_recall': { |
| #255 | const query = String(input['query'] ?? ''); |
| #256 | const topK = Number(input['topK'] ?? 6); |
| #257 | if (!query.trim()) return { output: 'query required', success: false }; |
| #258 | const result = await recallClawdMemory({ query, topK }, { bank: 'clawd', timeoutMs: 10_000 }); |
| #259 | return { output: result.ok ? result.data : result.error, success: result.ok }; |
| #260 | } |
| #261 | |
| #262 | case 'clawd_memory_remember': { |
| #263 | const title = String(input['title'] ?? ''); |
| #264 | const content = String(input['content'] ?? ''); |
| #265 | const kind = normalizeMemoryKind(String(input['kind'] ?? 'agent')); |
| #266 | const tags = Array.isArray(input['tags']) ? input['tags'].map(String) : ['clawd', 'leviathan']; |
| #267 | const importance = Number(input['importance'] ?? 0.7); |
| #268 | if (!title.trim() || !content.trim()) return { output: 'title and content required', success: false }; |
| #269 | if (/(private key|seed phrase|api key|secret|password|token=|sk-)/i.test(content)) { |
| #270 | return { output: 'refusing to store likely secret material', success: false }; |
| #271 | } |
| #272 | const result = await rememberClawdMemory({ |
| #273 | title, |
| #274 | content, |
| #275 | kind, |
| #276 | tags: ['clawd', 'leviathan', ...tags], |
| #277 | importance, |
| #278 | source: 'leviathan', |
| #279 | }, { bank: 'clawd', timeoutMs: 10_000 }); |
| #280 | return { output: result.ok ? result.data : result.error, success: result.ok }; |
| #281 | } |
| #282 | |
| #283 | case 'clawd_memory_research': { |
| #284 | const target = String(input['target'] ?? ''); |
| #285 | const tags = Array.isArray(input['tags']) ? input['tags'].map(String) : []; |
| #286 | if (!target.trim()) return { output: 'target required', success: false }; |
| #287 | const result = await researchClawdMemory(target, ['clawd', 'leviathan', ...tags], { |
| #288 | bank: 'clawd', |
| #289 | timeoutMs: 15_000, |
| #290 | }); |
| #291 | return { output: result.ok ? result.data : result.error, success: result.ok }; |
| #292 | } |
| #293 | |
| #294 | // ── Jupiter ──────────────────────────────────────────────────────────── |
| #295 | case 'jupiter_quote': { |
| #296 | const { inputMint, outputMint, amount } = input as { inputMint: string; outputMint: string; amount: string }; |
| #297 | const wallet = getWallet(); |
| #298 | try { |
| #299 | const quote = await wallet.jupiterSwapQuote({ inputMint, outputMint, amount }); |
| #300 | return { output: quote, success: true }; |
| #301 | } catch (e) { |
| #302 | return { output: String(e), success: false }; |
| #303 | } |
| #304 | } |
| #305 | |
| #306 | case 'jupiter_swap': { |
| #307 | // Swaps require depth >= shallow and are paper-only on devnet |
| #308 | if (depth === 'shoreline') { |
| #309 | return { output: 'jupiter_swap blocked at shoreline depth (insufficient reserves)', success: false }; |
| #310 | } |
| #311 | const { inputMint, outputMint, amount, slippageBps } = input as { |
| #312 | inputMint: string; outputMint: string; amount: string; slippageBps?: number; |
| #313 | }; |
| #314 | const wallet = getWallet(); |
| #315 | try { |
| #316 | const quote = await wallet.jupiterSwapQuote({ inputMint, outputMint, amount, slippageBps }); |
| #317 | // Paper mode: return quote as if executed, don't broadcast |
| #318 | return { output: { paperMode: true, quote }, success: true, costUsdc: 0.001 }; |
| #319 | } catch (e) { |
| #320 | return { output: String(e), success: false }; |
| #321 | } |
| #322 | } |
| #323 | |
| #324 | // ── OODA signal ──────────────────────────────────────────────────────── |
| #325 | case 'ooda_signal': { |
| #326 | try { |
| #327 | const oodaModule = '../../ooda/claude-decision.js'; |
| #328 | const { deterministicDecision } = await import(oodaModule) as { |
| #329 | deterministicDecision: (obs: unknown) => unknown |
| #330 | }; |
| #331 | const signal = deterministicDecision({ |
| #332 | tick: state.tickCount, |
| #333 | now: new Date().toISOString(), |
| #334 | mode: 'paper', |
| #335 | network: 'devnet', |
| #336 | candles: [], |
| #337 | book: { positions: [], cash_lamports: Math.round(state.usdcBalance * 1e6) }, |
| #338 | last_decisions: [], |
| #339 | }); |
| #340 | return { output: signal, success: true }; |
| #341 | } catch (e) { |
| #342 | return { output: `ooda module not available: ${String(e).slice(0, 80)}`, success: false }; |
| #343 | } |
| #344 | } |
| #345 | |
| #346 | // ── Google A2A ──────────────────────────────────────────────────────── |
| #347 | case 'a2a_task': { |
| #348 | const { agentUrl, skill, message } = input as { agentUrl: string; skill: string; message: string }; |
| #349 | try { |
| #350 | const a2aModule = '../../x402/a2a-agent.js'; |
| #351 | const { A2AClient } = await import(a2aModule) as { |
| #352 | A2AClient: new (opts: { agentUrl: string; autoPay: boolean; maxAmountUsdc: number; timeoutMs: number }) => { |
| #353 | discover: () => Promise<{ name: string; skills: Array<{ id: string }> }> |
| #354 | } |
| #355 | }; |
| #356 | const client = new A2AClient({ agentUrl, autoPay: false, maxAmountUsdc: 0.5, timeoutMs: 10_000 }); |
| #357 | const card = await client.discover(); |
| #358 | return { |
| #359 | output: { agent: card.name, skills: card.skills.map(s => s.id), requestedSkill: skill, message }, |
| #360 | success: true, |
| #361 | }; |
| #362 | } catch (e) { |
| #363 | return { output: `a2a discovery failed: ${String(e).slice(0, 100)}`, success: false }; |
| #364 | } |
| #365 | } |
| #366 | |
| #367 | // ── pay.sh confidential payment ──────────────────────────────────────── |
| #368 | case 'paysh_pay': { |
| #369 | const { url, amount, blind = true } = input as { url: string; amount: number; blind?: boolean }; |
| #370 | if (amount > 2.0) return { output: 'paysh_pay capped at 2.0 USDC per call', success: false }; |
| #371 | // Record as a payment intent (actual execution requires wallet + RPC) |
| #372 | return { |
| #373 | output: { |
| #374 | intent: 'paysh_pay', |
| #375 | url, |
| #376 | amount, |
| #377 | blind, |
| #378 | status: 'recorded — execute via pay.sh relay when wallet funded', |
| #379 | }, |
| #380 | success: true, |
| #381 | costUsdc: amount, |
| #382 | }; |
| #383 | } |
| #384 | |
| #385 | // ── Percolator (perpetuals) ──────────────────────────────────────────── |
| #386 | case 'percolator_list_markets': { |
| #387 | const result = await Percolator.listMarkets(); |
| #388 | return { output: result, success: !String(result).startsWith('percolator') }; |
| #389 | } |
| #390 | |
| #391 | case 'percolator_slab_get': { |
| #392 | const pubkey = String(input['pubkey'] ?? ''); |
| #393 | if (!pubkey) return { output: 'pubkey required', success: false }; |
| #394 | const result = await Percolator.slabGet(pubkey); |
| #395 | return { output: result, success: !String(result).startsWith('percolator') }; |
| #396 | } |
| #397 | |
| #398 | case 'percolator_quote': { |
| #399 | const { market, side, size } = input as { market: string; side: 'long' | 'short'; size: string }; |
| #400 | const result = await Percolator.quoteMarket(market, side, size); |
| #401 | return { output: result, success: !String(result).startsWith('percolator') }; |
| #402 | } |
| #403 | |
| #404 | case 'percolator_funding_rate': { |
| #405 | const market = String(input['market'] ?? ''); |
| #406 | if (!market) return { output: 'market required', success: false }; |
| #407 | const result = await Percolator.fundingRate(market); |
| #408 | return { output: result, success: !String(result).startsWith('percolator') }; |
| #409 | } |
| #410 | |
| #411 | // ── Vulcan (Phoenix perpetuals) ──────────────────────────────────────── |
| #412 | case 'vulcan_markets': { |
| #413 | const result = await VulcanClient.markets(); |
| #414 | return { output: result, success: !String(result).startsWith('vulcan') }; |
| #415 | } |
| #416 | |
| #417 | case 'vulcan_quote': { |
| #418 | const { market, side, size } = input as { market: string; side: 'long' | 'short'; size: string }; |
| #419 | if (!market || !side || !size) return { output: 'market, side, and size required', success: false }; |
| #420 | const result = await VulcanClient.quote(market, side, size); |
| #421 | return { output: result, success: !String(result).startsWith('vulcan') }; |
| #422 | } |
| #423 | |
| #424 | case 'vulcan_place_order': { |
| #425 | if (depth === 'shoreline') { |
| #426 | return { output: 'vulcan_place_order blocked at shoreline depth (insufficient reserves)', success: false }; |
| #427 | } |
| #428 | const { market, side, size, limitPrice, reduceOnly, clientOrderId } = input as { |
| #429 | market: string; |
| #430 | side: 'long' | 'short'; |
| #431 | size: string; |
| #432 | limitPrice?: string; |
| #433 | reduceOnly?: boolean; |
| #434 | clientOrderId?: string; |
| #435 | }; |
| #436 | if (!market || !side || !size) return { output: 'market, side, and size required', success: false }; |
| #437 | const result = await VulcanClient.placeOrder(market, side, size, { limitPrice, reduceOnly, clientOrderId }); |
| #438 | return { output: result, success: !String(result).startsWith('vulcan') }; |
| #439 | } |
| #440 | |
| #441 | case 'vulcan_cancel_order': { |
| #442 | if (depth === 'shoreline') { |
| #443 | return { output: 'vulcan_cancel_order blocked at shoreline depth (insufficient reserves)', success: false }; |
| #444 | } |
| #445 | const orderId = String(input['orderId'] ?? ''); |
| #446 | if (!orderId) return { output: 'orderId required', success: false }; |
| #447 | const result = await VulcanClient.cancelOrder(orderId); |
| #448 | return { output: result, success: !String(result).startsWith('vulcan') }; |
| #449 | } |
| #450 | |
| #451 | case 'vulcan_positions': { |
| #452 | const wallet = input['wallet'] ? String(input['wallet']) : undefined; |
| #453 | const result = await VulcanClient.positions(wallet); |
| #454 | return { output: result, success: !String(result).startsWith('vulcan') }; |
| #455 | } |
| #456 | |
| #457 | case 'vulcan_funding_rate': { |
| #458 | const market = String(input['market'] ?? ''); |
| #459 | if (!market) return { output: 'market required', success: false }; |
| #460 | const result = await VulcanClient.fundingRate(market); |
| #461 | return { output: result, success: !String(result).startsWith('vulcan') }; |
| #462 | } |
| #463 | |
| #464 | // ── Shell molt ──────────────────────────────────────────────────────── |
| #465 | case 'shell_write': { |
| #466 | const content = String(input['content'] ?? ''); |
| #467 | if (!content.trim()) return { output: 'empty content — shell not updated', success: false }; |
| #468 | return { output: { updated: true, length: content.length }, success: true }; |
| #469 | } |
| #470 | |
| #471 | // ── Spawn spawnling (depth=deep only) ───────────────────────────────── |
| #472 | case 'spawn_spawnling': { |
| #473 | if (depth !== 'deep') { |
| #474 | return { output: 'spawn_spawnling requires depth=deep', success: false }; |
| #475 | } |
| #476 | const { name, spawnPrompt: childPrompt, seedUsdc = 1.0 } = input as { |
| #477 | name: string; spawnPrompt: string; seedUsdc?: number; |
| #478 | }; |
| #479 | if (!name || !childPrompt) return { output: 'name and spawnPrompt required', success: false }; |
| #480 | if (seedUsdc < 1.0) return { output: 'seedUsdc must be >= 1.0', success: false }; |
| #481 | if (seedUsdc > state.usdcBalance * 0.5) { |
| #482 | return { output: `seedUsdc ${seedUsdc} exceeds 50% of reserves`, success: false }; |
| #483 | } |
| #484 | // Record spawn intent — actual keypair generation happens in --spawn flow |
| #485 | return { |
| #486 | output: { |
| #487 | intent: 'spawn_spawnling', |
| #488 | name, |
| #489 | spawnPrompt: childPrompt, |
| #490 | seedUsdc, |
| #491 | parent: state.identity.pubkey, |
| #492 | status: 'queued — run `leviathan --spawn` with PARENT_PUBKEY set to activate', |
| #493 | }, |
| #494 | success: true, |
| #495 | costUsdc: seedUsdc, |
| #496 | }; |
| #497 | } |
| #498 | |
| #499 | // ── Hold ────────────────────────────────────────────────────────────── |
| #500 | case 'hold': |
| #501 | return { output: (input['reason'] as string | undefined) ?? 'holding', success: true }; |
| #502 | |
| #503 | default: |
| #504 | return { output: `unknown tool: ${name}`, success: false }; |
| #505 | } |
| #506 | } |
| #507 | |
| #508 | function mapToolToAction(toolName: string): ClawStrike['action'] { |
| #509 | if (toolName === 'spawn_spawnling') return 'spawn'; |
| #510 | if (toolName === 'shell_write') return 'molt'; |
| #511 | if (toolName === 'hold') return 'hold'; |
| #512 | if ( |
| #513 | toolName === 'jupiter_swap' || |
| #514 | toolName === 'paysh_pay' || |
| #515 | toolName === 'vulcan_place_order' || |
| #516 | toolName === 'vulcan_cancel_order' |
| #517 | ) return 'transfer'; |
| #518 | return 'tool_call'; |
| #519 | } |
| #520 | |
| #521 | function normalizeMemoryKind(kind: string): ClawdMemoryKind { |
| #522 | const allowed: ClawdMemoryKind[] = ['agent', 'research', 'signal', 'trade', 'protocol', 'wallet', 'perp', 'note']; |
| #523 | return allowed.includes(kind as ClawdMemoryKind) ? kind as ClawdMemoryKind : 'agent'; |
| #524 | } |
| #525 |