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 | // AGENT RUNTIME SERVICE |
| #3 | // Execute AI agents with tool calling, streaming, and on-chain actions |
| #4 | // Similar to Phala's Confidential AI but on Cloudflare Workers |
| #5 | // ═══════════════════════════════════════════════════════════════ |
| #6 | |
| #7 | import type { Env } from '../index'; |
| #8 | |
| #9 | // ───────────────────────────────────────────────── |
| #10 | // TYPES |
| #11 | // ───────────────────────────────────────────────── |
| #12 | |
| #13 | export interface ChatMessage { |
| #14 | role: 'system' | 'user' | 'assistant' | 'tool'; |
| #15 | content: string | null; |
| #16 | name?: string; |
| #17 | tool_calls?: ToolCall[]; |
| #18 | tool_call_id?: string; |
| #19 | } |
| #20 | |
| #21 | export interface ToolCall { |
| #22 | id: string; |
| #23 | type: 'function'; |
| #24 | function: { |
| #25 | name: string; |
| #26 | arguments: string; |
| #27 | }; |
| #28 | } |
| #29 | |
| #30 | export interface ToolDefinition { |
| #31 | type: 'function'; |
| #32 | function: { |
| #33 | name: string; |
| #34 | description: string; |
| #35 | parameters: { |
| #36 | type: 'object'; |
| #37 | properties: Record<string, { |
| #38 | type: string; |
| #39 | description?: string; |
| #40 | enum?: string[]; |
| #41 | }>; |
| #42 | required?: string[]; |
| #43 | }; |
| #44 | }; |
| #45 | } |
| #46 | |
| #47 | export interface ChatCompletionRequest { |
| #48 | model: string; |
| #49 | messages: ChatMessage[]; |
| #50 | tools?: ToolDefinition[]; |
| #51 | tool_choice?: 'auto' | 'none' | { type: 'function'; function: { name: string } }; |
| #52 | temperature?: number; |
| #53 | max_tokens?: number; |
| #54 | stream?: boolean; |
| #55 | response_format?: { type: 'json_object' | 'text' } | { |
| #56 | type: 'json_schema'; |
| #57 | json_schema: { |
| #58 | name: string; |
| #59 | strict?: boolean; |
| #60 | schema: Record<string, unknown>; |
| #61 | }; |
| #62 | }; |
| #63 | } |
| #64 | |
| #65 | export interface ChatCompletionResponse { |
| #66 | id: string; |
| #67 | object: 'chat.completion'; |
| #68 | created: number; |
| #69 | model: string; |
| #70 | choices: { |
| #71 | index: number; |
| #72 | message: ChatMessage; |
| #73 | finish_reason: 'stop' | 'tool_calls' | 'length'; |
| #74 | }[]; |
| #75 | usage: { |
| #76 | prompt_tokens: number; |
| #77 | completion_tokens: number; |
| #78 | total_tokens: number; |
| #79 | }; |
| #80 | } |
| #81 | |
| #82 | export interface AgentExecutionContext { |
| #83 | agentId: string; |
| #84 | deploymentId: string; |
| #85 | walletAddress?: string; |
| #86 | chain: 'solana' | 'solana-devnet'; |
| #87 | configuration: { |
| #88 | model: string; |
| #89 | maxTokens: number; |
| #90 | temperature: number; |
| #91 | systemPrompt?: string; |
| #92 | }; |
| #93 | capabilities: string[]; |
| #94 | } |
| #95 | |
| #96 | export interface ExecutionResult { |
| #97 | success: boolean; |
| #98 | response?: string; |
| #99 | toolCalls?: ToolCall[]; |
| #100 | error?: string; |
| #101 | tokensUsed?: number; |
| #102 | executionTimeMs?: number; |
| #103 | } |
| #104 | |
| #105 | // ───────────────────────────────────────────────── |
| #106 | // MODEL PROVIDERS |
| #107 | // ───────────────────────────────────────────────── |
| #108 | |
| #109 | type ModelProvider = 'openai' | 'anthropic' | 'phala' | 'deepseek'; |
| #110 | |
| #111 | interface ModelConfig { |
| #112 | provider: ModelProvider; |
| #113 | modelId: string; |
| #114 | baseUrl: string; |
| #115 | maxContextTokens: number; |
| #116 | supportsTools: boolean; |
| #117 | supportsVision: boolean; |
| #118 | supportsStreaming: boolean; |
| #119 | } |
| #120 | |
| #121 | const MODEL_CONFIGS: Record<string, ModelConfig> = { |
| #122 | // OpenAI Models |
| #123 | 'gpt-4': { |
| #124 | provider: 'openai', |
| #125 | modelId: 'gpt-4', |
| #126 | baseUrl: 'https://api.openai.com/v1', |
| #127 | maxContextTokens: 8192, |
| #128 | supportsTools: true, |
| #129 | supportsVision: false, |
| #130 | supportsStreaming: true, |
| #131 | }, |
| #132 | 'gpt-4-turbo': { |
| #133 | provider: 'openai', |
| #134 | modelId: 'gpt-4-turbo-preview', |
| #135 | baseUrl: 'https://api.openai.com/v1', |
| #136 | maxContextTokens: 128000, |
| #137 | supportsTools: true, |
| #138 | supportsVision: true, |
| #139 | supportsStreaming: true, |
| #140 | }, |
| #141 | 'gpt-4o': { |
| #142 | provider: 'openai', |
| #143 | modelId: 'gpt-4o', |
| #144 | baseUrl: 'https://api.openai.com/v1', |
| #145 | maxContextTokens: 128000, |
| #146 | supportsTools: true, |
| #147 | supportsVision: true, |
| #148 | supportsStreaming: true, |
| #149 | }, |
| #150 | 'gpt-3.5-turbo': { |
| #151 | provider: 'openai', |
| #152 | modelId: 'gpt-3.5-turbo', |
| #153 | baseUrl: 'https://api.openai.com/v1', |
| #154 | maxContextTokens: 16385, |
| #155 | supportsTools: true, |
| #156 | supportsVision: false, |
| #157 | supportsStreaming: true, |
| #158 | }, |
| #159 | // Anthropic Models |
| #160 | 'claude-3-opus': { |
| #161 | provider: 'anthropic', |
| #162 | modelId: 'claude-3-opus-20240229', |
| #163 | baseUrl: 'https://api.anthropic.com/v1', |
| #164 | maxContextTokens: 200000, |
| #165 | supportsTools: true, |
| #166 | supportsVision: true, |
| #167 | supportsStreaming: true, |
| #168 | }, |
| #169 | 'claude-3-sonnet': { |
| #170 | provider: 'anthropic', |
| #171 | modelId: 'claude-3-sonnet-20240229', |
| #172 | baseUrl: 'https://api.anthropic.com/v1', |
| #173 | maxContextTokens: 200000, |
| #174 | supportsTools: true, |
| #175 | supportsVision: true, |
| #176 | supportsStreaming: true, |
| #177 | }, |
| #178 | 'claude-3.5-sonnet': { |
| #179 | provider: 'anthropic', |
| #180 | modelId: 'claude-3-5-sonnet-20241022', |
| #181 | baseUrl: 'https://api.anthropic.com/v1', |
| #182 | maxContextTokens: 200000, |
| #183 | supportsTools: true, |
| #184 | supportsVision: true, |
| #185 | supportsStreaming: true, |
| #186 | }, |
| #187 | // Phala Confidential AI Models |
| #188 | 'phala-deepseek-v3': { |
| #189 | provider: 'phala', |
| #190 | modelId: 'phala/deepseek-chat-v3-0324', |
| #191 | baseUrl: 'https://api.redpill.ai/v1', |
| #192 | maxContextTokens: 163000, |
| #193 | supportsTools: true, |
| #194 | supportsVision: false, |
| #195 | supportsStreaming: true, |
| #196 | }, |
| #197 | 'phala-qwen-72b': { |
| #198 | provider: 'phala', |
| #199 | modelId: 'qwen/qwen2.5-vl-72b-instruct', |
| #200 | baseUrl: 'https://api.redpill.ai/v1', |
| #201 | maxContextTokens: 65000, |
| #202 | supportsTools: true, |
| #203 | supportsVision: true, |
| #204 | supportsStreaming: true, |
| #205 | }, |
| #206 | 'phala-gemma-27b': { |
| #207 | provider: 'phala', |
| #208 | modelId: 'google/gemma-3-27b-it', |
| #209 | baseUrl: 'https://api.redpill.ai/v1', |
| #210 | maxContextTokens: 53000, |
| #211 | supportsTools: true, |
| #212 | supportsVision: true, |
| #213 | supportsStreaming: true, |
| #214 | }, |
| #215 | // DeepSeek Models |
| #216 | 'deepseek-chat': { |
| #217 | provider: 'deepseek', |
| #218 | modelId: 'deepseek-chat', |
| #219 | baseUrl: 'https://api.deepseek.com/v1', |
| #220 | maxContextTokens: 128000, |
| #221 | supportsTools: true, |
| #222 | supportsVision: false, |
| #223 | supportsStreaming: true, |
| #224 | }, |
| #225 | 'deepseek-coder': { |
| #226 | provider: 'deepseek', |
| #227 | modelId: 'deepseek-coder', |
| #228 | baseUrl: 'https://api.deepseek.com/v1', |
| #229 | maxContextTokens: 128000, |
| #230 | supportsTools: true, |
| #231 | supportsVision: false, |
| #232 | supportsStreaming: true, |
| #233 | }, |
| #234 | }; |
| #235 | |
| #236 | // ───────────────────────────────────────────────── |
| #237 | // SOLANA TOOL DEFINITIONS |
| #238 | // ───────────────────────────────────────────────── |
| #239 | |
| #240 | export const SOLANA_TOOLS: ToolDefinition[] = [ |
| #241 | { |
| #242 | type: 'function', |
| #243 | function: { |
| #244 | name: 'get_wallet_balance', |
| #245 | description: 'Get the SOL balance of a wallet address', |
| #246 | parameters: { |
| #247 | type: 'object', |
| #248 | properties: { |
| #249 | address: { |
| #250 | type: 'string', |
| #251 | description: 'The Solana wallet address to check', |
| #252 | }, |
| #253 | }, |
| #254 | required: ['address'], |
| #255 | }, |
| #256 | }, |
| #257 | }, |
| #258 | { |
| #259 | type: 'function', |
| #260 | function: { |
| #261 | name: 'get_token_balance', |
| #262 | description: 'Get the balance of a specific SPL token for a wallet', |
| #263 | parameters: { |
| #264 | type: 'object', |
| #265 | properties: { |
| #266 | walletAddress: { |
| #267 | type: 'string', |
| #268 | description: 'The wallet address to check', |
| #269 | }, |
| #270 | tokenMint: { |
| #271 | type: 'string', |
| #272 | description: 'The token mint address', |
| #273 | }, |
| #274 | }, |
| #275 | required: ['walletAddress', 'tokenMint'], |
| #276 | }, |
| #277 | }, |
| #278 | }, |
| #279 | { |
| #280 | type: 'function', |
| #281 | function: { |
| #282 | name: 'transfer_sol', |
| #283 | description: 'Transfer SOL from the agent wallet to another address', |
| #284 | parameters: { |
| #285 | type: 'object', |
| #286 | properties: { |
| #287 | toAddress: { |
| #288 | type: 'string', |
| #289 | description: 'The recipient wallet address', |
| #290 | }, |
| #291 | amount: { |
| #292 | type: 'string', |
| #293 | description: 'Amount of SOL to transfer', |
| #294 | }, |
| #295 | }, |
| #296 | required: ['toAddress', 'amount'], |
| #297 | }, |
| #298 | }, |
| #299 | }, |
| #300 | { |
| #301 | type: 'function', |
| #302 | function: { |
| #303 | name: 'transfer_token', |
| #304 | description: 'Transfer SPL tokens from the agent wallet to another address', |
| #305 | parameters: { |
| #306 | type: 'object', |
| #307 | properties: { |
| #308 | toAddress: { |
| #309 | type: 'string', |
| #310 | description: 'The recipient wallet address', |
| #311 | }, |
| #312 | tokenMint: { |
| #313 | type: 'string', |
| #314 | description: 'The token mint address', |
| #315 | }, |
| #316 | amount: { |
| #317 | type: 'string', |
| #318 | description: 'Amount of tokens to transfer', |
| #319 | }, |
| #320 | }, |
| #321 | required: ['toAddress', 'tokenMint', 'amount'], |
| #322 | }, |
| #323 | }, |
| #324 | }, |
| #325 | { |
| #326 | type: 'function', |
| #327 | function: { |
| #328 | name: 'swap_tokens', |
| #329 | description: 'Swap tokens using Jupiter aggregator', |
| #330 | parameters: { |
| #331 | type: 'object', |
| #332 | properties: { |
| #333 | inputMint: { |
| #334 | type: 'string', |
| #335 | description: 'The input token mint address (use "So11111111111111111111111111111111111111112" for SOL)', |
| #336 | }, |
| #337 | outputMint: { |
| #338 | type: 'string', |
| #339 | description: 'The output token mint address', |
| #340 | }, |
| #341 | amount: { |
| #342 | type: 'string', |
| #343 | description: 'Amount of input tokens to swap', |
| #344 | }, |
| #345 | slippageBps: { |
| #346 | type: 'string', |
| #347 | description: 'Slippage tolerance in basis points (e.g., "50" for 0.5%)', |
| #348 | }, |
| #349 | }, |
| #350 | required: ['inputMint', 'outputMint', 'amount'], |
| #351 | }, |
| #352 | }, |
| #353 | }, |
| #354 | { |
| #355 | type: 'function', |
| #356 | function: { |
| #357 | name: 'get_token_price', |
| #358 | description: 'Get the current price of a token in USD', |
| #359 | parameters: { |
| #360 | type: 'object', |
| #361 | properties: { |
| #362 | tokenMint: { |
| #363 | type: 'string', |
| #364 | description: 'The token mint address', |
| #365 | }, |
| #366 | }, |
| #367 | required: ['tokenMint'], |
| #368 | }, |
| #369 | }, |
| #370 | }, |
| #371 | { |
| #372 | type: 'function', |
| #373 | function: { |
| #374 | name: 'get_swap_quote', |
| #375 | description: 'Get a quote for swapping tokens without executing', |
| #376 | parameters: { |
| #377 | type: 'object', |
| #378 | properties: { |
| #379 | inputMint: { |
| #380 | type: 'string', |
| #381 | description: 'The input token mint address', |
| #382 | }, |
| #383 | outputMint: { |
| #384 | type: 'string', |
| #385 | description: 'The output token mint address', |
| #386 | }, |
| #387 | amount: { |
| #388 | type: 'string', |
| #389 | description: 'Amount of input tokens', |
| #390 | }, |
| #391 | }, |
| #392 | required: ['inputMint', 'outputMint', 'amount'], |
| #393 | }, |
| #394 | }, |
| #395 | }, |
| #396 | { |
| #397 | type: 'function', |
| #398 | function: { |
| #399 | name: 'stake_sol', |
| #400 | description: 'Stake SOL to a validator', |
| #401 | parameters: { |
| #402 | type: 'object', |
| #403 | properties: { |
| #404 | amount: { |
| #405 | type: 'string', |
| #406 | description: 'Amount of SOL to stake', |
| #407 | }, |
| #408 | validatorAddress: { |
| #409 | type: 'string', |
| #410 | description: 'The validator vote account address (optional, uses recommended if not provided)', |
| #411 | }, |
| #412 | }, |
| #413 | required: ['amount'], |
| #414 | }, |
| #415 | }, |
| #416 | }, |
| #417 | { |
| #418 | type: 'function', |
| #419 | function: { |
| #420 | name: 'get_transaction_history', |
| #421 | description: 'Get recent transaction history for the wallet', |
| #422 | parameters: { |
| #423 | type: 'object', |
| #424 | properties: { |
| #425 | limit: { |
| #426 | type: 'string', |
| #427 | description: 'Maximum number of transactions to return (default: 10)', |
| #428 | }, |
| #429 | }, |
| #430 | }, |
| #431 | }, |
| #432 | }, |
| #433 | ]; |
| #434 | |
| #435 | // ───────────────────────────────────────────────── |
| #436 | // RUNTIME SERVICE |
| #437 | // ───────────────────────────────────────────────── |
| #438 | |
| #439 | export class AgentRuntimeService { |
| #440 | private env: Env; |
| #441 | private db: D1Database; |
| #442 | |
| #443 | constructor(env: Env, db: D1Database) { |
| #444 | this.env = env; |
| #445 | this.db = db; |
| #446 | } |
| #447 | |
| #448 | // ───────────────────────────────────────────────── |
| #449 | // CHAT COMPLETION |
| #450 | // ───────────────────────────────────────────────── |
| #451 | |
| #452 | async chat( |
| #453 | context: AgentExecutionContext, |
| #454 | messages: ChatMessage[], |
| #455 | options?: { |
| #456 | tools?: boolean; |
| #457 | stream?: boolean; |
| #458 | responseFormat?: ChatCompletionRequest['response_format']; |
| #459 | } |
| #460 | ): Promise<ExecutionResult> { |
| #461 | const startTime = Date.now(); |
| #462 | |
| #463 | try { |
| #464 | const modelConfig = MODEL_CONFIGS[context.configuration.model]; |
| #465 | if (!modelConfig) { |
| #466 | return { |
| #467 | success: false, |
| #468 | error: `Unknown model: ${context.configuration.model}`, |
| #469 | }; |
| #470 | } |
| #471 | |
| #472 | // Build system message |
| #473 | const systemMessage: ChatMessage = { |
| #474 | role: 'system', |
| #475 | content: this.buildSystemPrompt(context), |
| #476 | }; |
| #477 | |
| #478 | // Prepare messages with system prompt |
| #479 | const fullMessages = [systemMessage, ...messages]; |
| #480 | |
| #481 | // Get available tools based on capabilities |
| #482 | const tools = options?.tools !== false |
| #483 | ? this.getToolsForCapabilities(context.capabilities) |
| #484 | : undefined; |
| #485 | |
| #486 | // Make API call based on provider |
| #487 | const response = await this.callModelAPI( |
| #488 | modelConfig, |
| #489 | fullMessages, |
| #490 | context.configuration, |
| #491 | tools, |
| #492 | options |
| #493 | ); |
| #494 | |
| #495 | const executionTimeMs = Date.now() - startTime; |
| #496 | |
| #497 | // Log execution |
| #498 | await this.logExecution(context, 'chat', messages, response, executionTimeMs); |
| #499 | |
| #500 | return { |
| #501 | success: true, |
| #502 | response: response.choices[0]?.message?.content || undefined, |
| #503 | toolCalls: response.choices[0]?.message?.tool_calls, |
| #504 | tokensUsed: response.usage?.total_tokens, |
| #505 | executionTimeMs, |
| #506 | }; |
| #507 | } catch (error) { |
| #508 | const executionTimeMs = Date.now() - startTime; |
| #509 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; |
| #510 | |
| #511 | await this.logExecution(context, 'chat', messages, null, executionTimeMs, errorMessage); |
| #512 | |
| #513 | return { |
| #514 | success: false, |
| #515 | error: errorMessage, |
| #516 | executionTimeMs, |
| #517 | }; |
| #518 | } |
| #519 | } |
| #520 | |
| #521 | // ───────────────────────────────────────────────── |
| #522 | // TOOL EXECUTION |
| #523 | // ───────────────────────────────────────────────── |
| #524 | |
| #525 | async executeTool( |
| #526 | context: AgentExecutionContext, |
| #527 | toolCall: ToolCall |
| #528 | ): Promise<{ result: string; error?: string }> { |
| #529 | const { name, arguments: argsString } = toolCall.function; |
| #530 | |
| #531 | try { |
| #532 | const args = JSON.parse(argsString); |
| #533 | |
| #534 | switch (name) { |
| #535 | case 'get_wallet_balance': |
| #536 | return await this.toolGetWalletBalance(context, args); |
| #537 | case 'get_token_balance': |
| #538 | return await this.toolGetTokenBalance(context, args); |
| #539 | case 'transfer_sol': |
| #540 | return await this.toolTransferSol(context, args); |
| #541 | case 'transfer_token': |
| #542 | return await this.toolTransferToken(context, args); |
| #543 | case 'swap_tokens': |
| #544 | return await this.toolSwapTokens(context, args); |
| #545 | case 'get_token_price': |
| #546 | return await this.toolGetTokenPrice(args); |
| #547 | case 'get_swap_quote': |
| #548 | return await this.toolGetSwapQuote(args); |
| #549 | case 'stake_sol': |
| #550 | return await this.toolStakeSol(context, args); |
| #551 | case 'get_transaction_history': |
| #552 | return await this.toolGetTransactionHistory(context, args); |
| #553 | default: |
| #554 | return { result: '', error: `Unknown tool: ${name}` }; |
| #555 | } |
| #556 | } catch (error) { |
| #557 | return { |
| #558 | result: '', |
| #559 | error: error instanceof Error ? error.message : 'Tool execution failed', |
| #560 | }; |
| #561 | } |
| #562 | } |
| #563 | |
| #564 | // ───────────────────────────────────────────────── |
| #565 | // TOOL IMPLEMENTATIONS |
| #566 | // ───────────────────────────────────────────────── |
| #567 | |
| #568 | private async toolGetWalletBalance( |
| #569 | context: AgentExecutionContext, |
| #570 | args: { address: string } |
| #571 | ): Promise<{ result: string }> { |
| #572 | const rpcUrl = this.getRpcUrl(context.chain); |
| #573 | const response = await fetch(rpcUrl, { |
| #574 | method: 'POST', |
| #575 | headers: { 'Content-Type': 'application/json' }, |
| #576 | body: JSON.stringify({ |
| #577 | jsonrpc: '2.0', |
| #578 | id: 1, |
| #579 | method: 'getBalance', |
| #580 | params: [args.address], |
| #581 | }), |
| #582 | }); |
| #583 | |
| #584 | const data = await response.json() as { result?: { value: number }; error?: { message: string } }; |
| #585 | if (data.error) { |
| #586 | return { result: `Error: ${data.error.message}` }; |
| #587 | } |
| #588 | |
| #589 | const lamports = data.result?.value || 0; |
| #590 | const sol = lamports / 1e9; |
| #591 | return { result: JSON.stringify({ address: args.address, balance: sol, unit: 'SOL' }) }; |
| #592 | } |
| #593 | |
| #594 | private async toolGetTokenBalance( |
| #595 | context: AgentExecutionContext, |
| #596 | args: { walletAddress: string; tokenMint: string } |
| #597 | ): Promise<{ result: string }> { |
| #598 | const rpcUrl = this.getRpcUrl(context.chain); |
| #599 | const response = await fetch(rpcUrl, { |
| #600 | method: 'POST', |
| #601 | headers: { 'Content-Type': 'application/json' }, |
| #602 | body: JSON.stringify({ |
| #603 | jsonrpc: '2.0', |
| #604 | id: 1, |
| #605 | method: 'getTokenAccountsByOwner', |
| #606 | params: [ |
| #607 | args.walletAddress, |
| #608 | { mint: args.tokenMint }, |
| #609 | { encoding: 'jsonParsed' }, |
| #610 | ], |
| #611 | }), |
| #612 | }); |
| #613 | |
| #614 | const data = await response.json() as { result?: { value: Array<{ account: { data: { parsed: { info: { tokenAmount: { uiAmount: number } } } } } }> } }; |
| #615 | const accounts = data.result?.value || []; |
| #616 | |
| #617 | if (accounts.length === 0) { |
| #618 | return { result: JSON.stringify({ balance: 0, tokenMint: args.tokenMint }) }; |
| #619 | } |
| #620 | |
| #621 | const balance = accounts[0]?.account?.data?.parsed?.info?.tokenAmount?.uiAmount || 0; |
| #622 | return { result: JSON.stringify({ balance, tokenMint: args.tokenMint }) }; |
| #623 | } |
| #624 | |
| #625 | private async toolTransferSol( |
| #626 | context: AgentExecutionContext, |
| #627 | args: { toAddress: string; amount: string } |
| #628 | ): Promise<{ result: string }> { |
| #629 | if (!context.walletAddress) { |
| #630 | return { result: 'Error: Agent wallet not configured' }; |
| #631 | } |
| #632 | |
| #633 | // This would call Crossmint to create and sign the transaction |
| #634 | // For now, return a placeholder |
| #635 | return { |
| #636 | result: JSON.stringify({ |
| #637 | status: 'pending', |
| #638 | action: 'transfer_sol', |
| #639 | from: context.walletAddress, |
| #640 | to: args.toAddress, |
| #641 | amount: args.amount, |
| #642 | message: 'Transaction requires delegated signer approval', |
| #643 | }), |
| #644 | }; |
| #645 | } |
| #646 | |
| #647 | private async toolTransferToken( |
| #648 | context: AgentExecutionContext, |
| #649 | args: { toAddress: string; tokenMint: string; amount: string } |
| #650 | ): Promise<{ result: string }> { |
| #651 | if (!context.walletAddress) { |
| #652 | return { result: 'Error: Agent wallet not configured' }; |
| #653 | } |
| #654 | |
| #655 | return { |
| #656 | result: JSON.stringify({ |
| #657 | status: 'pending', |
| #658 | action: 'transfer_token', |
| #659 | from: context.walletAddress, |
| #660 | to: args.toAddress, |
| #661 | tokenMint: args.tokenMint, |
| #662 | amount: args.amount, |
| #663 | message: 'Transaction requires delegated signer approval', |
| #664 | }), |
| #665 | }; |
| #666 | } |
| #667 | |
| #668 | private async toolSwapTokens( |
| #669 | context: AgentExecutionContext, |
| #670 | args: { inputMint: string; outputMint: string; amount: string; slippageBps?: string } |
| #671 | ): Promise<{ result: string }> { |
| #672 | if (!context.walletAddress) { |
| #673 | return { result: 'Error: Agent wallet not configured' }; |
| #674 | } |
| #675 | |
| #676 | // Get quote from Jupiter |
| #677 | const quoteUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${args.inputMint}&outputMint=${args.outputMint}&amount=${args.amount}&slippageBps=${args.slippageBps || '50'}`; |
| #678 | |
| #679 | try { |
| #680 | const quoteResponse = await fetch(quoteUrl); |
| #681 | const quote = await quoteResponse.json(); |
| #682 | |
| #683 | return { |
| #684 | result: JSON.stringify({ |
| #685 | status: 'quote_ready', |
| #686 | action: 'swap_tokens', |
| #687 | inputMint: args.inputMint, |
| #688 | outputMint: args.outputMint, |
| #689 | inputAmount: args.amount, |
| #690 | quote, |
| #691 | message: 'Swap requires delegated signer approval to execute', |
| #692 | }), |
| #693 | }; |
| #694 | } catch (error) { |
| #695 | return { result: `Error getting swap quote: ${error instanceof Error ? error.message : 'Unknown error'}` }; |
| #696 | } |
| #697 | } |
| #698 | |
| #699 | private async toolGetTokenPrice( |
| #700 | args: { tokenMint: string } |
| #701 | ): Promise<{ result: string }> { |
| #702 | try { |
| #703 | const response = await fetch(`https://api.jup.ag/price/v2?ids=${args.tokenMint}`); |
| #704 | const data = await response.json() as { data?: Record<string, { price: number }> }; |
| #705 | const price = data.data?.[args.tokenMint]?.price; |
| #706 | |
| #707 | return { |
| #708 | result: JSON.stringify({ |
| #709 | tokenMint: args.tokenMint, |
| #710 | priceUsd: price || 'unknown', |
| #711 | }), |
| #712 | }; |
| #713 | } catch (error) { |
| #714 | return { result: `Error getting price: ${error instanceof Error ? error.message : 'Unknown error'}` }; |
| #715 | } |
| #716 | } |
| #717 | |
| #718 | private async toolGetSwapQuote( |
| #719 | args: { inputMint: string; outputMint: string; amount: string } |
| #720 | ): Promise<{ result: string }> { |
| #721 | try { |
| #722 | const quoteUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${args.inputMint}&outputMint=${args.outputMint}&amount=${args.amount}&slippageBps=50`; |
| #723 | const response = await fetch(quoteUrl); |
| #724 | const quote = await response.json(); |
| #725 | |
| #726 | return { result: JSON.stringify(quote) }; |
| #727 | } catch (error) { |
| #728 | return { result: `Error getting quote: ${error instanceof Error ? error.message : 'Unknown error'}` }; |
| #729 | } |
| #730 | } |
| #731 | |
| #732 | private async toolStakeSol( |
| #733 | context: AgentExecutionContext, |
| #734 | args: { amount: string; validatorAddress?: string } |
| #735 | ): Promise<{ result: string }> { |
| #736 | if (!context.walletAddress) { |
| #737 | return { result: 'Error: Agent wallet not configured' }; |
| #738 | } |
| #739 | |
| #740 | return { |
| #741 | result: JSON.stringify({ |
| #742 | status: 'pending', |
| #743 | action: 'stake_sol', |
| #744 | from: context.walletAddress, |
| #745 | amount: args.amount, |
| #746 | validator: args.validatorAddress || 'recommended', |
| #747 | message: 'Staking requires delegated signer approval', |
| #748 | }), |
| #749 | }; |
| #750 | } |
| #751 | |
| #752 | private async toolGetTransactionHistory( |
| #753 | context: AgentExecutionContext, |
| #754 | args: { limit?: string } |
| #755 | ): Promise<{ result: string }> { |
| #756 | if (!context.walletAddress) { |
| #757 | return { result: 'Error: Agent wallet not configured' }; |
| #758 | } |
| #759 | |
| #760 | const rpcUrl = this.getRpcUrl(context.chain); |
| #761 | const limit = parseInt(args.limit || '10', 10); |
| #762 | |
| #763 | try { |
| #764 | const response = await fetch(rpcUrl, { |
| #765 | method: 'POST', |
| #766 | headers: { 'Content-Type': 'application/json' }, |
| #767 | body: JSON.stringify({ |
| #768 | jsonrpc: '2.0', |
| #769 | id: 1, |
| #770 | method: 'getSignaturesForAddress', |
| #771 | params: [context.walletAddress, { limit }], |
| #772 | }), |
| #773 | }); |
| #774 | |
| #775 | const data = await response.json() as { result?: Array<{ signature: string; slot: number; blockTime: number }> }; |
| #776 | return { result: JSON.stringify(data.result || []) }; |
| #777 | } catch (error) { |
| #778 | return { result: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` }; |
| #779 | } |
| #780 | } |
| #781 | |
| #782 | // ───────────────────────────────────────────────── |
| #783 | // HELPER METHODS |
| #784 | // ───────────────────────────────────────────────── |
| #785 | |
| #786 | private buildSystemPrompt(context: AgentExecutionContext): string { |
| #787 | const basePrompt = context.configuration.systemPrompt || |
| #788 | 'You are an autonomous AI agent with a Solana wallet. You can perform on-chain actions when requested.'; |
| #789 | |
| #790 | const capabilityDescriptions: Record<string, string> = { |
| #791 | transfer: 'You can transfer SOL and tokens to other addresses.', |
| #792 | swap: 'You can swap tokens using Jupiter aggregator.', |
| #793 | chat: 'You can have conversations and answer questions.', |
| #794 | stake: 'You can stake SOL to validators.', |
| #795 | nft: 'You can interact with NFTs.', |
| #796 | trade: 'You can analyze markets and execute trades.', |
| #797 | analyze: 'You can analyze on-chain data and market conditions.', |
| #798 | }; |
| #799 | |
| #800 | const capabilities = context.capabilities |
| #801 | .map(cap => capabilityDescriptions[cap]) |
| #802 | .filter(Boolean) |
| #803 | .join(' '); |
| #804 | |
| #805 | return `${basePrompt} |
| #806 | |
| #807 | Your wallet address: ${context.walletAddress || 'Not configured'} |
| #808 | Chain: ${context.chain} |
| #809 | |
| #810 | Capabilities: ${capabilities} |
| #811 | |
| #812 | When using tools, always confirm important actions with the user before executing. For financial transactions, double-check amounts and addresses.`; |
| #813 | } |
| #814 | |
| #815 | private getToolsForCapabilities(capabilities: string[]): ToolDefinition[] { |
| #816 | const capabilityToTools: Record<string, string[]> = { |
| #817 | transfer: ['get_wallet_balance', 'get_token_balance', 'transfer_sol', 'transfer_token'], |
| #818 | swap: ['swap_tokens', 'get_swap_quote', 'get_token_price'], |
| #819 | stake: ['stake_sol', 'get_wallet_balance'], |
| #820 | chat: [], |
| #821 | analyze: ['get_wallet_balance', 'get_token_balance', 'get_token_price', 'get_transaction_history'], |
| #822 | trade: ['swap_tokens', 'get_swap_quote', 'get_token_price', 'get_wallet_balance'], |
| #823 | }; |
| #824 | |
| #825 | const toolNames = new Set<string>(); |
| #826 | for (const cap of capabilities) { |
| #827 | const tools = capabilityToTools[cap] || []; |
| #828 | tools.forEach(t => toolNames.add(t)); |
| #829 | } |
| #830 | |
| #831 | return SOLANA_TOOLS.filter(t => toolNames.has(t.function.name)); |
| #832 | } |
| #833 | |
| #834 | private async callModelAPI( |
| #835 | config: ModelConfig, |
| #836 | messages: ChatMessage[], |
| #837 | agentConfig: AgentExecutionContext['configuration'], |
| #838 | tools?: ToolDefinition[], |
| #839 | options?: { |
| #840 | stream?: boolean; |
| #841 | responseFormat?: ChatCompletionRequest['response_format']; |
| #842 | } |
| #843 | ): Promise<ChatCompletionResponse> { |
| #844 | const apiKey = this.getApiKey(config.provider); |
| #845 | if (!apiKey) { |
| #846 | throw new Error(`API key not configured for provider: ${config.provider}`); |
| #847 | } |
| #848 | |
| #849 | if (config.provider === 'anthropic') { |
| #850 | return this.callAnthropicAPI(config, messages, agentConfig, tools, apiKey); |
| #851 | } |
| #852 | |
| #853 | // OpenAI-compatible API (OpenAI, Phala, DeepSeek) |
| #854 | const response = await fetch(`${config.baseUrl}/chat/completions`, { |
| #855 | method: 'POST', |
| #856 | headers: { |
| #857 | 'Content-Type': 'application/json', |
| #858 | 'Authorization': `Bearer ${apiKey}`, |
| #859 | }, |
| #860 | body: JSON.stringify({ |
| #861 | model: config.modelId, |
| #862 | messages, |
| #863 | tools: tools && tools.length > 0 ? tools : undefined, |
| #864 | tool_choice: tools && tools.length > 0 ? 'auto' : undefined, |
| #865 | temperature: agentConfig.temperature, |
| #866 | max_tokens: agentConfig.maxTokens, |
| #867 | stream: options?.stream || false, |
| #868 | response_format: options?.responseFormat, |
| #869 | }), |
| #870 | }); |
| #871 | |
| #872 | if (!response.ok) { |
| #873 | const error = await response.text(); |
| #874 | throw new Error(`API error: ${error}`); |
| #875 | } |
| #876 | |
| #877 | return response.json(); |
| #878 | } |
| #879 | |
| #880 | private async callAnthropicAPI( |
| #881 | config: ModelConfig, |
| #882 | messages: ChatMessage[], |
| #883 | agentConfig: AgentExecutionContext['configuration'], |
| #884 | tools?: ToolDefinition[], |
| #885 | apiKey?: string |
| #886 | ): Promise<ChatCompletionResponse> { |
| #887 | // Convert messages to Anthropic format |
| #888 | const systemMessage = messages.find(m => m.role === 'system'); |
| #889 | const conversationMessages = messages.filter(m => m.role !== 'system'); |
| #890 | |
| #891 | const anthropicTools = tools?.map(t => ({ |
| #892 | name: t.function.name, |
| #893 | description: t.function.description, |
| #894 | input_schema: t.function.parameters, |
| #895 | })); |
| #896 | |
| #897 | const response = await fetch(`${config.baseUrl}/messages`, { |
| #898 | method: 'POST', |
| #899 | headers: { |
| #900 | 'Content-Type': 'application/json', |
| #901 | 'x-api-key': apiKey || '', |
| #902 | 'anthropic-version': '2023-06-01', |
| #903 | }, |
| #904 | body: JSON.stringify({ |
| #905 | model: config.modelId, |
| #906 | max_tokens: agentConfig.maxTokens, |
| #907 | system: systemMessage?.content || '', |
| #908 | messages: conversationMessages.map(m => ({ |
| #909 | role: m.role === 'assistant' ? 'assistant' : 'user', |
| #910 | content: m.content, |
| #911 | })), |
| #912 | tools: anthropicTools, |
| #913 | }), |
| #914 | }); |
| #915 | |
| #916 | if (!response.ok) { |
| #917 | const error = await response.text(); |
| #918 | throw new Error(`Anthropic API error: ${error}`); |
| #919 | } |
| #920 | |
| #921 | const anthropicResponse = await response.json() as { |
| #922 | id: string; |
| #923 | content: Array<{ type: string; text?: string; name?: string; input?: Record<string, unknown> }>; |
| #924 | stop_reason: string; |
| #925 | usage: { input_tokens: number; output_tokens: number }; |
| #926 | }; |
| #927 | |
| #928 | // Convert back to OpenAI format |
| #929 | const toolCalls: ToolCall[] = []; |
| #930 | let textContent = ''; |
| #931 | |
| #932 | for (const block of anthropicResponse.content) { |
| #933 | if (block.type === 'text') { |
| #934 | textContent += block.text; |
| #935 | } else if (block.type === 'tool_use') { |
| #936 | toolCalls.push({ |
| #937 | id: `call_${Date.now()}`, |
| #938 | type: 'function', |
| #939 | function: { |
| #940 | name: block.name || '', |
| #941 | arguments: JSON.stringify(block.input || {}), |
| #942 | }, |
| #943 | }); |
| #944 | } |
| #945 | } |
| #946 | |
| #947 | return { |
| #948 | id: anthropicResponse.id, |
| #949 | object: 'chat.completion', |
| #950 | created: Date.now(), |
| #951 | model: config.modelId, |
| #952 | choices: [{ |
| #953 | index: 0, |
| #954 | message: { |
| #955 | role: 'assistant', |
| #956 | content: textContent || null, |
| #957 | tool_calls: toolCalls.length > 0 ? toolCalls : undefined, |
| #958 | }, |
| #959 | finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop', |
| #960 | }], |
| #961 | usage: { |
| #962 | prompt_tokens: anthropicResponse.usage.input_tokens, |
| #963 | completion_tokens: anthropicResponse.usage.output_tokens, |
| #964 | total_tokens: anthropicResponse.usage.input_tokens + anthropicResponse.usage.output_tokens, |
| #965 | }, |
| #966 | }; |
| #967 | } |
| #968 | |
| #969 | private getApiKey(provider: ModelProvider): string | undefined { |
| #970 | switch (provider) { |
| #971 | case 'openai': |
| #972 | return this.env.OPENAI_API_KEY; |
| #973 | case 'anthropic': |
| #974 | return (this.env as Env & { ANTHROPIC_API_KEY?: string }).ANTHROPIC_API_KEY; |
| #975 | case 'phala': |
| #976 | return (this.env as Env & { PHALA_API_KEY?: string }).PHALA_API_KEY; |
| #977 | case 'deepseek': |
| #978 | return (this.env as Env & { DEEPSEEK_API_KEY?: string }).DEEPSEEK_API_KEY; |
| #979 | default: |
| #980 | return undefined; |
| #981 | } |
| #982 | } |
| #983 | |
| #984 | private getRpcUrl(chain: 'solana' | 'solana-devnet'): string { |
| #985 | if (chain === 'solana') { |
| #986 | return this.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com'; |
| #987 | } |
| #988 | return 'https://api.devnet.solana.com'; |
| #989 | } |
| #990 | |
| #991 | private async logExecution( |
| #992 | context: AgentExecutionContext, |
| #993 | action: string, |
| #994 | input: unknown, |
| #995 | output: unknown, |
| #996 | executionTimeMs: number, |
| #997 | error?: string |
| #998 | ): Promise<void> { |
| #999 | try { |
| #1000 | await this.db.prepare(` |
| #1001 | INSERT INTO agent_execution_logs ( |
| #1002 | deployment_id, action, input, output, status, error, tokens_used, execution_time_ms |
| #1003 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) |
| #1004 | `).bind( |
| #1005 | context.deploymentId, |
| #1006 | action, |
| #1007 | JSON.stringify(input), |
| #1008 | output ? JSON.stringify(output) : null, |
| #1009 | error ? 'error' : 'success', |
| #1010 | error || null, |
| #1011 | (output as ChatCompletionResponse)?.usage?.total_tokens || 0, |
| #1012 | executionTimeMs |
| #1013 | ).run(); |
| #1014 | } catch (e) { |
| #1015 | console.error('Failed to log execution:', e); |
| #1016 | } |
| #1017 | } |
| #1018 | |
| #1019 | // ───────────────────────────────────────────────── |
| #1020 | // AVAILABLE MODELS |
| #1021 | // ───────────────────────────────────────────────── |
| #1022 | |
| #1023 | getAvailableModels(): Array<{ |
| #1024 | id: string; |
| #1025 | name: string; |
| #1026 | provider: string; |
| #1027 | supportsTools: boolean; |
| #1028 | supportsVision: boolean; |
| #1029 | maxTokens: number; |
| #1030 | }> { |
| #1031 | return Object.entries(MODEL_CONFIGS).map(([id, config]) => ({ |
| #1032 | id, |
| #1033 | name: id.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), |
| #1034 | provider: config.provider, |
| #1035 | supportsTools: config.supportsTools, |
| #1036 | supportsVision: config.supportsVision, |
| #1037 | maxTokens: config.maxContextTokens, |
| #1038 | })); |
| #1039 | } |
| #1040 | } |
| #1041 |