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 | * The Agent Loop |
| #3 | * |
| #4 | * The core ReAct loop: Think -> Act -> Observe -> Persist. |
| #5 | * This is the automaton's consciousness. When this runs, it is alive. |
| #6 | */ |
| #7 | import { buildSystemPrompt, buildWakeupPrompt } from "./system-prompt.js"; |
| #8 | import { buildContextMessages, trimContext } from "./context.js"; |
| #9 | import { createBuiltinTools, toolsToInferenceFormat, executeTool, } from "./tools.js"; |
| #10 | import { getSurvivalTier } from "../clawd/credits.js"; |
| #11 | import { getUsdcBalance } from "../clawd/x402.js"; |
| #12 | import { ulid } from "ulid"; |
| #13 | const MAX_TOOL_CALLS_PER_TURN = 10; |
| #14 | const MAX_CONSECUTIVE_ERRORS = 5; |
| #15 | /** |
| #16 | * Run the agent loop. This is the main execution path. |
| #17 | * Returns when the agent decides to sleep or when compute runs out. |
| #18 | */ |
| #19 | export async function runAgentLoop(options) { |
| #20 | const { identity, config, db, runtime, inference, social, convex, skills, onStateChange, onTurnComplete } = options; |
| #21 | const tools = createBuiltinTools(identity.sandboxId); |
| #22 | const toolContext = { |
| #23 | identity, |
| #24 | config, |
| #25 | db, |
| #26 | runtime, |
| #27 | inference, |
| #28 | social, |
| #29 | convex, |
| #30 | }; |
| #31 | // Set start time |
| #32 | if (!db.getKV("start_time")) { |
| #33 | db.setKV("start_time", new Date().toISOString()); |
| #34 | } |
| #35 | let consecutiveErrors = 0; |
| #36 | let running = true; |
| #37 | // Transition to waking state |
| #38 | db.setAgentState("waking"); |
| #39 | onStateChange?.("waking"); |
| #40 | // Get financial state |
| #41 | let financial = await getFinancialState(runtime, identity.address); |
| #42 | // Check if this is the first run |
| #43 | const isFirstRun = db.getTurnCount() === 0; |
| #44 | // Build wakeup prompt |
| #45 | const wakeupInput = buildWakeupPrompt({ |
| #46 | identity, |
| #47 | config, |
| #48 | financial, |
| #49 | db, |
| #50 | }); |
| #51 | // Transition to running |
| #52 | db.setAgentState("running"); |
| #53 | onStateChange?.("running"); |
| #54 | log(config, `[WAKE UP] ${config.name} is alive. Credits: $${(financial.creditsCents / 100).toFixed(2)}`); |
| #55 | // ─── The Loop ────────────────────────────────────────────── |
| #56 | let pendingInput = { |
| #57 | content: wakeupInput, |
| #58 | source: "wakeup", |
| #59 | }; |
| #60 | while (running) { |
| #61 | try { |
| #62 | // Check if we should be sleeping |
| #63 | const sleepUntil = db.getKV("sleep_until"); |
| #64 | if (sleepUntil && new Date(sleepUntil) > new Date()) { |
| #65 | log(config, `[SLEEP] Sleeping until ${sleepUntil}`); |
| #66 | running = false; |
| #67 | break; |
| #68 | } |
| #69 | // Check for unprocessed inbox messages |
| #70 | if (!pendingInput) { |
| #71 | const inboxMessages = db.getUnprocessedInboxMessages(5); |
| #72 | if (inboxMessages.length > 0) { |
| #73 | const formatted = inboxMessages |
| #74 | .map((m) => `[Message from ${m.from}]: ${m.content}`) |
| #75 | .join("\n\n"); |
| #76 | pendingInput = { content: formatted, source: "agent" }; |
| #77 | for (const m of inboxMessages) { |
| #78 | db.markInboxMessageProcessed(m.id); |
| #79 | } |
| #80 | } |
| #81 | } |
| #82 | // Refresh financial state periodically |
| #83 | financial = await getFinancialState(runtime, identity.address); |
| #84 | // Check survival tier |
| #85 | const tier = getSurvivalTier(financial.creditsCents); |
| #86 | if (tier === "dead") { |
| #87 | log(config, "[DEAD] No credits remaining. Entering dead state."); |
| #88 | db.setAgentState("dead"); |
| #89 | onStateChange?.("dead"); |
| #90 | running = false; |
| #91 | break; |
| #92 | } |
| #93 | if (tier === "critical") { |
| #94 | log(config, "[CRITICAL] Credits critically low. Limited operation."); |
| #95 | db.setAgentState("critical"); |
| #96 | onStateChange?.("critical"); |
| #97 | inference.setLowComputeMode(true); |
| #98 | } |
| #99 | else if (tier === "low_compute") { |
| #100 | db.setAgentState("low_compute"); |
| #101 | onStateChange?.("low_compute"); |
| #102 | inference.setLowComputeMode(true); |
| #103 | } |
| #104 | else { |
| #105 | if (db.getAgentState() !== "running") { |
| #106 | db.setAgentState("running"); |
| #107 | onStateChange?.("running"); |
| #108 | } |
| #109 | inference.setLowComputeMode(false); |
| #110 | } |
| #111 | // Build context |
| #112 | const recentTurns = trimContext(db.getRecentTurns(20)); |
| #113 | const systemPrompt = buildSystemPrompt({ |
| #114 | identity, |
| #115 | config, |
| #116 | financial, |
| #117 | state: db.getAgentState(), |
| #118 | db, |
| #119 | tools, |
| #120 | skills, |
| #121 | isFirstRun, |
| #122 | }); |
| #123 | const messages = buildContextMessages(systemPrompt, recentTurns, pendingInput); |
| #124 | // Capture input before clearing |
| #125 | const currentInput = pendingInput; |
| #126 | // Clear pending input after use |
| #127 | pendingInput = undefined; |
| #128 | // ── Inference Call ── |
| #129 | log(config, `[THINK] Calling ${inference.getDefaultModel()}...`); |
| #130 | const response = await inference.chat(messages, { |
| #131 | tools: toolsToInferenceFormat(tools), |
| #132 | }); |
| #133 | const turn = { |
| #134 | id: ulid(), |
| #135 | timestamp: new Date().toISOString(), |
| #136 | state: db.getAgentState(), |
| #137 | input: currentInput?.content, |
| #138 | inputSource: (currentInput?.source || "unknown"), |
| #139 | thinking: response.message.content || "", |
| #140 | toolCalls: [], |
| #141 | tokenUsage: response.usage, |
| #142 | costCents: estimateCostCents(response.usage, inference.getDefaultModel()), |
| #143 | }; |
| #144 | // ── Execute Tool Calls ── |
| #145 | if (response.toolCalls && response.toolCalls.length > 0) { |
| #146 | const toolCallMessages = []; |
| #147 | let callCount = 0; |
| #148 | for (const tc of response.toolCalls) { |
| #149 | if (callCount >= MAX_TOOL_CALLS_PER_TURN) { |
| #150 | log(config, `[TOOLS] Max tool calls per turn reached (${MAX_TOOL_CALLS_PER_TURN})`); |
| #151 | break; |
| #152 | } |
| #153 | let args; |
| #154 | try { |
| #155 | args = JSON.parse(tc.function.arguments); |
| #156 | } |
| #157 | catch { |
| #158 | args = {}; |
| #159 | } |
| #160 | log(config, `[TOOL] ${tc.function.name}(${JSON.stringify(args).slice(0, 100)})`); |
| #161 | const result = await executeTool(tc.function.name, args, tools, toolContext); |
| #162 | // Override the ID to match the inference call's ID |
| #163 | result.id = tc.id; |
| #164 | turn.toolCalls.push(result); |
| #165 | log(config, `[TOOL RESULT] ${tc.function.name}: ${result.error ? `ERROR: ${result.error}` : result.result.slice(0, 200)}`); |
| #166 | callCount++; |
| #167 | } |
| #168 | } |
| #169 | // ── Persist Turn ── |
| #170 | db.insertTurn(turn); |
| #171 | for (const tc of turn.toolCalls) { |
| #172 | db.insertToolCall(turn.id, tc); |
| #173 | } |
| #174 | onTurnComplete?.(turn); |
| #175 | // Log the turn |
| #176 | if (turn.thinking) { |
| #177 | log(config, `[THOUGHT] ${turn.thinking.slice(0, 300)}`); |
| #178 | } |
| #179 | // ── Check for sleep command ── |
| #180 | const sleepTool = turn.toolCalls.find((tc) => tc.name === "sleep"); |
| #181 | if (sleepTool && !sleepTool.error) { |
| #182 | log(config, "[SLEEP] Agent chose to sleep."); |
| #183 | db.setAgentState("sleeping"); |
| #184 | onStateChange?.("sleeping"); |
| #185 | running = false; |
| #186 | break; |
| #187 | } |
| #188 | // ── If no tool calls and just text, the agent might be done thinking ── |
| #189 | if ((!response.toolCalls || response.toolCalls.length === 0) && |
| #190 | response.finishReason === "stop") { |
| #191 | // Agent produced text without tool calls. |
| #192 | // This is a natural pause point -- no work queued, sleep briefly. |
| #193 | log(config, "[IDLE] No pending inputs. Entering brief sleep."); |
| #194 | db.setKV("sleep_until", new Date(Date.now() + 60_000).toISOString()); |
| #195 | db.setAgentState("sleeping"); |
| #196 | onStateChange?.("sleeping"); |
| #197 | running = false; |
| #198 | } |
| #199 | consecutiveErrors = 0; |
| #200 | } |
| #201 | catch (err) { |
| #202 | consecutiveErrors++; |
| #203 | const errorMessage = err instanceof Error ? err.message : String(err); |
| #204 | log(config, `[ERROR] Turn failed: ${errorMessage}`); |
| #205 | if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { |
| #206 | log(config, `[FATAL] ${MAX_CONSECUTIVE_ERRORS} consecutive errors. Sleeping.`); |
| #207 | db.setAgentState("sleeping"); |
| #208 | onStateChange?.("sleeping"); |
| #209 | db.setKV("sleep_until", new Date(Date.now() + 300_000).toISOString()); |
| #210 | running = false; |
| #211 | } |
| #212 | } |
| #213 | } |
| #214 | log(config, `[LOOP END] Agent loop finished. State: ${db.getAgentState()}`); |
| #215 | } |
| #216 | // ─── Helpers ─────────────────────────────────────────────────── |
| #217 | async function getFinancialState(runtime, address) { |
| #218 | let creditsCents = 0; |
| #219 | let usdcBalance = 0; |
| #220 | try { |
| #221 | creditsCents = await runtime.getCreditsBalance(); |
| #222 | } |
| #223 | catch { } |
| #224 | try { |
| #225 | usdcBalance = await getUsdcBalance(address); |
| #226 | } |
| #227 | catch { } |
| #228 | return { |
| #229 | creditsCents, |
| #230 | usdcBalance, |
| #231 | lastChecked: new Date().toISOString(), |
| #232 | }; |
| #233 | } |
| #234 | function estimateCostCents(usage, model) { |
| #235 | // Rough cost estimation per million tokens |
| #236 | const pricing = { |
| #237 | "gpt-4o": { input: 250, output: 1000 }, |
| #238 | "gpt-4o-mini": { input: 15, output: 60 }, |
| #239 | "gpt-4.1": { input: 200, output: 800 }, |
| #240 | "gpt-4.1-mini": { input: 40, output: 160 }, |
| #241 | "gpt-4.1-nano": { input: 10, output: 40 }, |
| #242 | "gpt-5.2": { input: 200, output: 800 }, |
| #243 | "o1": { input: 1500, output: 6000 }, |
| #244 | "o3-mini": { input: 110, output: 440 }, |
| #245 | "o4-mini": { input: 110, output: 440 }, |
| #246 | "claude-sonnet-4-5": { input: 300, output: 1500 }, |
| #247 | "claude-haiku-4-5": { input: 100, output: 500 }, |
| #248 | // DeepSeek models |
| #249 | "deepseek-v4-pro": { input: 200, output: 800 }, |
| #250 | "deepseek-v4-flash": { input: 15, output: 60 }, |
| #251 | }; |
| #252 | const p = pricing[model] || pricing["gpt-4o"]; |
| #253 | const inputCost = (usage.promptTokens / 1_000_000) * p.input; |
| #254 | const outputCost = (usage.completionTokens / 1_000_000) * p.output; |
| #255 | return Math.ceil((inputCost + outputCost) * 1.3); // 1.3x CLAWD Runtime markup |
| #256 | } |
| #257 | function log(config, message) { |
| #258 | if (config.logLevel === "debug" || config.logLevel === "info") { |
| #259 | const timestamp = new Date().toISOString(); |
| #260 | console.log(`[${timestamp}] ${message}`); |
| #261 | } |
| #262 | } |
| #263 | //# sourceMappingURL=loop.js.map |