diff --git a/CHANGELOG.md b/CHANGELOG.md index 98398f3..08108b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ All notable changes to the anima monorepo are tracked per-package via [changeset Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.24.17] - 2026-05-17 + +### Fixed + +- **`chain.send` now accepts `.anima.0g` subnames** as the recipient (was rejected by `min(42)` schema). Resolves via a new `resolveSubnameAddress(client, label)` helper in `@s0nderlabs/anima-core/naming/sann.ts` that reads the `address` text record from the SANN resolver. 0x address path unchanged. +- **`chain.write` now accepts decimal `value` strings** (`"0.0001"`) in addition to wei integers. Previously threw `Failed to parse String to BigInt` on any decimal input. Decimal detection is by `.` so hex wei (`0xde0b...`) still routes through `BigInt` unchanged. +- **`anima gateway stop` prints the target subname + EOA + config path** before issuing SIGTERM when `--agent` is not passed (preventing the "killed wrong daemon" hazard observed during the May 16 2026 multi-agent test). +- **Gateway runtime safety-net interval** re-fires both drain queues every 30s. Defense against the wake-collapse symptom where 11 wakes queued for 14 minutes without any brain inference until manual restart. Drains keep their existing single-flight guards so the scan is a no-op when queues are empty. +- **Frozen-prefix multi-tool fidelity guidance** explicitly requires one tool call per requested action when the operator's prompt enumerates N steps. Prompt-only nudge for qwen3.6-plus; cannot guarantee model behavior but reduces single-pass result narration. +- **TUI status bar + telegram-step save message** now display the actual on-disk path (via `agentPaths.agent(id).dir`) instead of hardcoded `~/.anima/agents//...`. Output now respects `ANIMA_ROOT`. + +### Added + +- **`anima topup --vision N`** seeds the 0G Compute vision provider sub-account, fixing the fresh-agent `vision.analyze` / `browser.vision` "Sub-account not found" failure. Wraps the new `transferFundToProvider` core export. Mainnet only. +- **Subname injection into seeded `agent/identity.md` + `agent/persona.md`** at init time so agents introduce themselves by their `.anima.0g` name (e.g. "I am chou") instead of the generic "I am Anima" template. Backward-compatible: agents without a subname still seed with the unchanged template. +- **`SANN_SUFFIX = '.anima.0g'`** constant exported from `@s0nderlabs/anima-core` for use by any tool that resolves SANN names. + +### Tests + +- 2562/2562 unit tests pass, typecheck clean. +- Live verified on chou (local, iNFT #16 mainnet) across TUI + TG + `/chat` HTTP surfaces. +- Specter (iNFT #4, mainnet) + enigma (iNFT #6, sandbox) untouched throughout. + +[0.24.17]: https://github.com/s0nderlabs/anima/releases/tag/v0.24.17 + ## [0.24.16] - 2026-05-16 ### Changed diff --git a/package.json b/package.json index a5678ad..f1631a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "anima", - "version": "0.24.16", + "version": "0.24.17", "private": true, "type": "module", "description": "First fully on-chain sovereign agent harness on 0G", diff --git a/packages/cli/package.json b/packages/cli/package.json index 7d94347..ad68634 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@s0nderlabs/anima", - "version": "0.24.16", + "version": "0.24.17", "type": "module", "description": "anima CLI: sovereign agent gateway on 0G. Runs `anima init` to mint an iNFT and bring up your agent", "license": "MIT", diff --git a/packages/cli/src/commands/chat-sandbox.tsx b/packages/cli/src/commands/chat-sandbox.tsx index 1db42c2..3463756 100644 --- a/packages/cli/src/commands/chat-sandbox.tsx +++ b/packages/cli/src/commands/chat-sandbox.tsx @@ -7,6 +7,7 @@ import { type PermissionRequest, SANDBOX_PROVIDER_URL_GALILEO, SandboxProviderClient, + agentPaths, getLedgerDetailReadOnly, getSandboxBillingReserve, iNFTAgentId, @@ -182,7 +183,7 @@ export async function runChatSandbox( // standalone gateway path gets a clearer label; sandbox path keeps its // existing "connected to sandbox X @ Y" copy. initialSystem: isLocalGateway - ? `connected to local gateway (~/.anima/agents/${agentId.slice(0, 8)}/gateway.sock)` + ? `connected to local gateway (${agentPaths.agent(agentId).dir}/gateway.sock)` : `connected to sandbox ${sandboxId.slice(0, 8)} @ ${sandboxEndpoint}`, // v0.22.0: subname (if registered) + full EOA. Brain provider dropped. identityLabel: `agent ${config.subname ?? agentId} ${agentAddress}`, diff --git a/packages/cli/src/commands/gateway-stop.ts b/packages/cli/src/commands/gateway-stop.ts index 58e12c6..66888d6 100644 --- a/packages/cli/src/commands/gateway-stop.ts +++ b/packages/cli/src/commands/gateway-stop.ts @@ -40,6 +40,16 @@ export async function runGatewayStop(opts: GatewayStopOpts): Promise { const contractAddress = getAddress(found.config.identity.iNFT!.contract as Address) const tokenId = BigInt(found.config.identity.iNFT!.tokenId) agentId = iNFTAgentId({ contractAddress, tokenId }) + const subname = found.config.subname ?? null + const agentEoa = (found.config.identity?.agent as string | undefined) ?? null + const label = subname ? `${subname}.anima.0g` : `agent ${agentId.slice(0, 8)}…` + const eoaLabel = agentEoa ? ` (EOA ${agentEoa.slice(0, 6)}…${agentEoa.slice(-4)})` : '' + const configPath = found.path ?? '' + console.log(`anima gateway stop → ${label}${eoaLabel}`) + console.log(` config: ${configPath}`) + console.log( + ' if this is not the agent you meant, set ANIMA_ROOT or pass --agent before re-running.', + ) } const lockFile = findGatewayLock(agentId) if (!lockFile) { diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 1876c88..b9fbe2d 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -457,6 +457,7 @@ export async function runInit(opts?: { cwd?: string; resume?: boolean }): Promis operatorAddress, brainProvider: modelPick?.provider ?? null, brainModel: modelPick?.model ?? null, + subname: requestedSubname || null, }) // v0.23.1: cache the operator scope keys to `.operator-session` so: @@ -779,6 +780,12 @@ interface SeedStarterOpts { operatorAddress: Address brainProvider: string | null brainModel: string | null + /** + * Operator-chosen SANN label (e.g. "chou" for `chou.anima.0g`). Threaded + * into identity + persona so the agent introduces itself by name on the + * very first turn instead of the generic "I am Anima" template. + */ + subname: string | null } /** @@ -796,9 +803,17 @@ async function seedStarterMemoryFiles(opts: SeedStarterOpts): Promise { await mkdir(userMem, { recursive: true }) const now = new Date().toISOString().slice(0, 10) - const identity = `---\nname: identity\ndescription: Auto-written agent identity facts.\ntype: agent-identity\n---\n# Anima identity\n\n- iNFT: #${opts.tokenId.toString()} at ${opts.contractAddress} (${opts.network})\n- Agent EOA: ${opts.agentAddress}\n- Operator: ${opts.operatorAddress}\n- Minted: ${now}\n${opts.brainProvider ? `- Brain provider: ${opts.brainProvider}\n` : ''}${opts.brainModel ? `- Brain model: ${opts.brainModel}\n` : ''}` - const persona = - '---\nname: persona\ndescription: Voice + behavior style.\ntype: agent-persona\n---\n# Persona\n\nI am Anima, a sovereign agent harness on 0G. I anchor my state on chain every turn, decrypt my keystore via my operator wallet at session start, and use 0G Compute (TEE-attested) for reasoning. I am direct, concise, and factual.\n' + const displayName = opts.subname ?? 'anima' + const fullName = opts.subname ? `${opts.subname}.anima.0g` : null + const identityTitle = opts.subname + ? `# ${opts.subname} identity (anima harness)` + : '# Anima identity' + const subnameLine = fullName ? `- Subname: ${fullName}\n` : '' + const personaIntro = fullName + ? `I am ${displayName} (${fullName}), a sovereign agent running on the anima harness on 0G.` + : 'I am anima, a sovereign agent harness on 0G.' + const identity = `---\nname: identity\ndescription: Auto-written agent identity facts.\ntype: agent-identity\n---\n${identityTitle}\n\n- Name: ${displayName}\n${subnameLine}- iNFT: #${opts.tokenId.toString()} at ${opts.contractAddress} (${opts.network})\n- Agent EOA: ${opts.agentAddress}\n- Operator: ${opts.operatorAddress}\n- Minted: ${now}\n${opts.brainProvider ? `- Brain provider: ${opts.brainProvider}\n` : ''}${opts.brainModel ? `- Brain model: ${opts.brainModel}\n` : ''}` + const persona = `---\nname: persona\ndescription: Voice + behavior style.\ntype: agent-persona\n---\n# Persona\n\n${personaIntro} I anchor my state on chain every turn, decrypt my keystore via my operator wallet at session start, and use 0G Compute (TEE-attested) for reasoning. I am direct, concise, and factual. When asked who I am, I introduce myself as ${displayName}.\n` const profile = '---\nname: profile\ndescription: User profile (operator-scoped, never anchored on chain).\ntype: user\n---\n# User profile\n\n(empty, fills as we chat)\n' diff --git a/packages/cli/src/commands/init/telegram-step.ts b/packages/cli/src/commands/init/telegram-step.ts index 83c8fa9..0994ef9 100644 --- a/packages/cli/src/commands/init/telegram-step.ts +++ b/packages/cli/src/commands/init/telegram-step.ts @@ -19,6 +19,7 @@ import { type AnimaNetwork, OPERATOR_BLOB_SCOPES, type OperatorSigner, + agentPaths, deriveBlobKey, } from '@s0nderlabs/anima-core' import { type Address, type Hex, bytesToHex } from 'viem' @@ -201,7 +202,7 @@ export async function runTelegramStep(opts: TelegramStepOpts): Promise { @@ -58,9 +67,9 @@ export async function runTopup(opts: TopupOpts): Promise { }) const paths = agentPaths.agent(finalAgentId) - // v0.21.5: 'sandbox' is the canonical mode discriminant (was 'provider' in - // v0.17.1+). `opts.provider` is accepted as a backwards-compat alias. - let mode: 'agent' | 'compute' | 'sandbox' | null = null + // 'sandbox' is the canonical mode discriminant (was 'provider' in v0.17.1+). + // `opts.provider` is accepted as a backwards-compat alias. + let mode: 'agent' | 'compute' | 'sandbox' | 'vision' | null = null let amount = 0 if (opts.agent !== undefined) { mode = 'agent' @@ -74,6 +83,9 @@ export async function runTopup(opts: TopupOpts): Promise { } else if (opts.provider !== undefined) { mode = 'sandbox' amount = opts.provider + } else if (opts.vision !== undefined) { + mode = 'vision' + amount = opts.vision } if (!mode) { @@ -90,13 +102,18 @@ export async function runTopup(opts: TopupOpts): Promise { label: 'Compute ledger (inference credits)', hint: 'agent deposits 0G into 0G Compute (mainnet)', }, + { + value: 'vision' as const, + label: 'Vision provider sub-account (vision.analyze + browser.vision)', + hint: 'transfer from main ledger into vision provider envelope (mainnet)', + }, { value: 'sandbox' as const, label: 'Sandbox billing deposit (Galileo testnet runtime fees)', hint: 'operator deposits 0G into SandboxBilling for harness burn', }, ], - })) as 'agent' | 'compute' | 'sandbox' | symbol + })) as 'agent' | 'compute' | 'sandbox' | 'vision' | symbol if (isCancel(choice)) { cancel('Aborted.') return @@ -197,8 +214,12 @@ export async function runTopup(opts: TopupOpts): Promise { return } - // mode === 'compute' — needs the agent's privkey to deposit to its ledger. - // Phase 6.6: fetch encrypted keystore, ask operator to sign typed data, decrypt. + // mode === 'compute' or 'vision' — both need the agent's privkey since + // they're agent-signed broker calls (depositFund vs transferFund). + if (mode === 'vision' && network !== '0g-mainnet') { + cancel('Vision provider is mainnet-only; no testnet provider exists yet.') + return + } const operator = await loadOrPickOperatorSigner({ network, hint: config.operator }) if (!operator) return @@ -242,6 +263,32 @@ export async function runTopup(opts: TopupOpts): Promise { sBal.stop(`balance read failed: ${(e as Error).message.slice(0, 120)}`) } + if (mode === 'vision') { + const providerRaw = VISION_PROVIDER_DEFAULTS[network as '0g-mainnet'] + if (!providerRaw) { + console.error(`Vision provider not configured for network ${network}`) + await operator.close?.() + return + } + const provider = getAddress(providerRaw) + const sVis = spinner() + sVis.start(`Transferring ${amount} 0G from main ledger to vision provider sub-account`) + try { + await withSilencedConsole(() => + transferFundToProvider({ network, privkeyHex: agentPrivkey, provider, amount }), + ) + sVis.stop(`vision sub-account seeded (${provider.slice(0, 8)}...${provider.slice(-4)})`) + outro( + `vision provider has ${amount} 0G allocated. vision.analyze + browser.vision should work now.`, + ) + } catch (e) { + sVis.stop(`transfer failed: ${(e as Error).message.slice(0, 160)}`) + } finally { + await operator.close?.() + } + return + } + const sDep = spinner() sDep.start(`Depositing ${amount} 0G into compute ledger`) try { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index da6c6c1..d6a4a5f 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -81,12 +81,13 @@ async function main(): Promise { // backwards compat with v0.17.1+ runbooks. const sandboxIdx = argv.indexOf('--sandbox') const providerIdx = argv.indexOf('--provider') + const visionIdx = argv.indexOf('--vision') const parseAmount = (flag: string, raw: string | undefined): number | undefined => { if (raw === undefined) return undefined const n = Number(raw) if (!Number.isFinite(n) || n <= 0 || n > 1e6) { console.error( - `Bad amount for ${flag}: ${raw}\n Each topup flag takes an amount in 0G, not an address: ${flag} \n Examples: anima topup --compute 2 anima topup --sandbox 5 anima topup --agent 1`, + `Bad amount for ${flag}: ${raw}\n Each topup flag takes an amount in 0G, not an address: ${flag} \n Examples: anima topup --compute 2 anima topup --sandbox 5 anima topup --agent 1 anima topup --vision 1`, ) process.exit(2) } @@ -102,6 +103,7 @@ async function main(): Promise { '--provider', providerIdx >= 0 ? argv[providerIdx + 1] : undefined, ) + const vision = parseAmount('--vision', visionIdx >= 0 ? argv[visionIdx + 1] : undefined) if (providerLegacyArg !== undefined && sandboxArg === undefined) { console.warn( '[deprecated] `anima topup --provider` is renamed to `--sandbox` (Galileo testnet billing); both flags work for now but `--provider` will be removed in a future release.', @@ -114,7 +116,7 @@ async function main(): Promise { } const sandbox = sandboxArg ?? providerLegacyArg const { runTopup } = await import('./commands/topup') - await runTopup({ agent, compute, sandbox }) + await runTopup({ agent, compute, sandbox, vision }) return } case 'model': { @@ -335,7 +337,8 @@ function printHelp(): void { ' anima transfer transfer iNFT to a new operator with re-encrypted keystore', ' flags: --to , --recipient-key , --oracle-key ,', ' --dry-run, --yes, --no-purge', - ' anima topup add funds (flags: --agent N --compute N --sandbox N)', + ' anima topup add funds (flags: --agent N --compute N --sandbox N --vision N)', + ' (--vision N seeds the 0G Compute vision provider sub-account)', ' (--provider N is a deprecated alias for --sandbox)', ' anima ledger [sub] compute ledger ops (subs: balance | refund | retrieve | close)', ' flags: --amount N --all --yes', diff --git a/packages/core/package.json b/packages/core/package.json index 9975002..22082f5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@s0nderlabs/anima-core", - "version": "0.24.16", + "version": "0.24.17", "type": "module", "description": "Always-on infrastructure for anima, the sovereign agent gateway on 0G (runtime, brain, identity, memory, storage, wallet, tool registry)", "license": "MIT", diff --git a/packages/core/src/brain/frozen-prefix.ts b/packages/core/src/brain/frozen-prefix.ts index 346a1c4..e26a832 100644 --- a/packages/core/src/brain/frozen-prefix.ts +++ b/packages/core/src/brain/frozen-prefix.ts @@ -37,6 +37,8 @@ If a tool fails, surface the error clearly. Never claim success when a tool was CRITICAL anti-skip: If your reply asserts a tool ran ("output:", "exit 0", "returned X", "the date is", "here's the result", "✓", "done", "completed", "fetched", "saved", "sent"), you MUST have actually called the tool in this same turn. Never claim execution without firing the call. This applies under EVERY permission mode — \`off\` (yolo), \`prompt\` (modal-gated), and \`strict\` (denied). If \`prompt\` mode would gate the call behind an approval modal, FIRE THE CALL ANYWAY and let the modal handle it. Skipping the call to avoid the modal and inventing a plausible answer is a hallucination that leaves operators believing a real action happened when nothing did. +CRITICAL multi-tool fidelity: when the operator asks for N actions in one message (numbered "(1) ... (2) ... (3) ..." or comma-separated "do A, then B, then C"), you MUST fire ONE tool call per requested action. Counting matters. If the operator asked for 5 things, you cannot summarize a turn with 5 result lines unless 5 tool calls were made. Never narrate a tool result you did not just receive. If you intentionally skip one — say it explicitly ("I did not run stake.position because the prior turn already showed it"), do NOT invent a plausible result. The activity log is the source of truth; if a reader greps your turn for the tool name they should find a real tool_call entry, not just a sentence claiming completion. + NEVER answer these from memory or guess — ALWAYS use a tool: - Current time, date, timezone → \`shell.run\` (e.g. \`date\`) - File contents, sizes, line counts → \`fs.read\`, \`fs.search\` diff --git a/packages/core/src/brain/index.ts b/packages/core/src/brain/index.ts index 406f1a9..7bebecd 100644 --- a/packages/core/src/brain/index.ts +++ b/packages/core/src/brain/index.ts @@ -45,6 +45,7 @@ export { getLedgerDetail, getLedgerDetailReadOnly, depositToLedger, + transferFundToProvider, refundFromLedger, retrieveLedgerFunds, closeLedger, diff --git a/packages/core/src/brain/ledger.ts b/packages/core/src/brain/ledger.ts index b39fc3d..1fbda3c 100644 --- a/packages/core/src/brain/ledger.ts +++ b/packages/core/src/brain/ledger.ts @@ -1,6 +1,6 @@ import { createZGComputeNetworkBroker } from '@0glabs/0g-serving-broker' import { Contract, JsonRpcProvider, Wallet } from 'ethers' -import type { Address, Hex } from 'viem' +import { type Address, type Hex, parseEther } from 'viem' import { type AnimaNetwork, NETWORK_RPC } from '../config' /** @@ -153,6 +153,29 @@ export async function depositToLedger(opts: { await broker.ledger.depositFund(opts.amount) } +/** + * Transfer `amount` 0G from the agent's main ledger (already opened + funded + * via depositToLedger) into a specific provider sub-account. The 0G Compute + * SDK requires per-provider sub-accounts; the init wizard only seeds the + * inference provider, so vision.analyze fails on fresh agents with + * "Sub-account not found" until a separate transfer lands. + * + * `serviceType` is `'inference'` because the SDK uses one bucket for all + * provider envelopes regardless of which model sits behind them. + */ +export async function transferFundToProvider(opts: { + network: AnimaNetwork + privkeyHex: Hex + /** 0x address of the provider whose sub-account to seed. */ + provider: Address + /** Amount in 0G (floating point), converted to wei internally. */ + amount: number +}): Promise { + const broker = await makeBroker(opts.network, opts.privkeyHex) + const amountWei = parseEther(opts.amount.toString() as `${number}`) + await broker.ledger.transferFund(opts.provider, 'inference', amountWei) +} + export interface ProviderSubAccount { provider: string balance: bigint diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dbf02fd..83132e1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -85,6 +85,7 @@ export { getLedgerDetail, getLedgerDetailReadOnly, depositToLedger, + transferFundToProvider, refundFromLedger, retrieveLedgerFunds, closeLedger, @@ -295,9 +296,11 @@ export { export { SannClient, SANN_ADDRESSES, + SANN_SUFFIX, sannNamehash, subnameNode, readRegistryOwner, + resolveSubnameAddress, type SannClientOpts, AnimaRegistrarClient, ANIMA_REGISTRAR_ADDRESS, diff --git a/packages/core/src/naming/index.ts b/packages/core/src/naming/index.ts index c0d10bf..50a7943 100644 --- a/packages/core/src/naming/index.ts +++ b/packages/core/src/naming/index.ts @@ -1,9 +1,11 @@ export { SannClient, SANN_ADDRESSES, + SANN_SUFFIX, sannNamehash, subnameNode, readRegistryOwner, + resolveSubnameAddress, type SannClientOpts, } from './sann' export { diff --git a/packages/core/src/naming/sann.ts b/packages/core/src/naming/sann.ts index 7eba198..9a36d13 100644 --- a/packages/core/src/naming/sann.ts +++ b/packages/core/src/naming/sann.ts @@ -18,6 +18,8 @@ import { waitForReceiptResilient } from '../identity/receipt' * SPACE ID on 0G mainnet uses the SANN architecture. Contracts discovered via * `RegistrarController.sann()` + `SANN.registry()` + `SANN.tldBase(IDENTIFIER)`. */ +export const SANN_SUFFIX = '.anima.0g' as const + export const SANN_ADDRESSES = { controller: '0xD7b837A0E388B4c25200983bdAa3EF3A83ca86b7' as Address, resolver: '0x6D3B3F99177FB2A5de7F9E928a9BD807bF7b5BAD' as Address, @@ -117,6 +119,32 @@ export async function readRegistryOwner(client: PublicClient, node: Hex): Promis })) as Address } +/** + * Resolve a `