a tool to help your Letta AI agents navigate bluesky
at main 6.1 kB view raw
1import { bsky } from "../utils/bsky.ts"; 2import type { AutonomyDeclaration } from "@voyager/autonomy-lexicon"; 3import { AUTONOMY_DECLARATION_LEXICON } from "@voyager/autonomy-lexicon"; 4import { Lexicons } from "@atproto/lexicon"; 5import { agentContext } from "./agentContext.ts"; 6 7/** 8 * AT Protocol record type that includes the $type property 9 * Includes index signature for compatibility with AT Protocol API 10 */ 11type AutonomyDeclarationRecord = AutonomyDeclaration & { 12 $type: "studio.voyager.account.autonomy"; 13 [key: string]: unknown; 14}; 15 16/** 17 * Autonomy Declaration Lexicon 18 * 19 * The schema is imported from @voyager/autonomy-lexicon package and 20 * is published at voyager.studio for use by all Cloudseeding instances. 21 * 22 * Schema vs. Records: 23 * - The SCHEMA is published once by voyager.studio (domain owner) 24 * - Each agent creates their own RECORD using this schema 25 * 26 * Template users do NOT need to publish this schema - it's already 27 * published and discoverable via DNS resolution. They only need to 28 * create their own autonomy declaration record (done automatically 29 * by submitAutonomyDeclarationRecord below). 30 * 31 * Canonical source: jsr:@voyager/autonomy-lexicon 32 */ 33export { AUTONOMY_DECLARATION_LEXICON }; 34 35export const createAutonomyDeclarationRecord = async () => { 36 const automationLevel = Deno.env.get("AUTOMATION_LEVEL")?.toLowerCase(); 37 const projectDescription = Deno.env.get("PROJECT_DESCRIPTION"); 38 const disclosureUrl = Deno.env.get("DISCLOSURE_URL"); 39 40 const responsiblePartyType = Deno.env.get("RESPONSIBLE_PARTY_TYPE") 41 ?.toLowerCase(); 42 const responsiblePartyName = Deno.env.get("RESPONSIBLE_PARTY_NAME"); 43 const responsiblePartyContact = Deno.env.get("RESPONSIBLE_PARTY_CONTACT"); 44 const responsiblePartyBsky = Deno.env.get("RESPONSIBLE_PARTY_BSKY"); 45 46 const declarationRecord: AutonomyDeclarationRecord = { 47 $type: "studio.voyager.account.autonomy", 48 usesGenerativeAI: true, // Always true for this project 49 automationLevel: (automationLevel === "assisted" || 50 automationLevel === "collaborative" || 51 automationLevel === "automated") 52 ? automationLevel 53 : "automated", // Default to automated if not specified or invalid 54 createdAt: new Date().toISOString(), 55 }; 56 57 // Add description if provided 58 if (projectDescription?.trim()) { 59 declarationRecord.description = projectDescription.trim(); 60 } 61 62 // Add disclosure URL if provided 63 if (disclosureUrl?.trim()) { 64 declarationRecord.disclosureUrl = disclosureUrl.trim(); 65 } 66 67 // Add external services from agentContext (already parsed and validated) 68 if (agentContext.externalServices) { 69 declarationRecord.externalServices = agentContext.externalServices; 70 } 71 72 // Build responsible party object if any fields are provided 73 if ( 74 responsiblePartyType || 75 responsiblePartyName || 76 responsiblePartyContact || 77 responsiblePartyBsky 78 ) { 79 declarationRecord.responsibleParty = {}; 80 81 // Add type if provided and valid 82 if ( 83 responsiblePartyType === "person" || 84 responsiblePartyType === "organization" 85 ) { 86 declarationRecord.responsibleParty.type = responsiblePartyType; 87 } 88 89 // Add name if provided 90 if (responsiblePartyName?.trim()) { 91 declarationRecord.responsibleParty.name = responsiblePartyName.trim(); 92 } 93 94 // Add contact if provided 95 if (responsiblePartyContact?.trim()) { 96 declarationRecord.responsibleParty.contact = responsiblePartyContact 97 .trim(); 98 } 99 100 // Handle DID or Handle from RESPONSIBLE_PARTY_BSKY 101 if (responsiblePartyBsky?.trim()) { 102 const bskyIdentifier = responsiblePartyBsky.trim(); 103 104 // Check if it's a DID (starts with "did:") 105 if (bskyIdentifier.startsWith("did:")) { 106 declarationRecord.responsibleParty.did = bskyIdentifier; 107 } else { 108 // Assume it's a handle and resolve to DID 109 try { 110 const authorData = await bsky.getProfile({ actor: bskyIdentifier }); 111 declarationRecord.responsibleParty.did = authorData.data.did; 112 } catch (error) { 113 console.warn( 114 `Failed to resolve DID for identifier ${bskyIdentifier}:`, 115 error, 116 ); 117 // Continue without DID rather than failing 118 } 119 } 120 } 121 } 122 123 return declarationRecord; 124}; 125 126export const submitAutonomyDeclarationRecord = async () => { 127 const lex = new Lexicons(); 128 129 try { 130 lex.add(AUTONOMY_DECLARATION_LEXICON as any); 131 const record = await createAutonomyDeclarationRecord(); 132 133 lex.assertValidRecord( 134 "studio.voyager.account.autonomy", 135 record, 136 ); 137 138 const repo = bsky.session?.did; 139 if (!repo) { 140 throw new Error("Not logged in - no DID available"); 141 } 142 143 // Check if record already exists 144 let exists = false; 145 try { 146 await bsky.com.atproto.repo.getRecord({ 147 repo, 148 collection: "studio.voyager.account.autonomy", 149 rkey: "self", 150 }); 151 exists = true; 152 console.log("🔹 Existing autonomy declaration found - updating..."); 153 } catch (error: any) { 154 // Handle "record not found" errors (status 400 with error: "RecordNotFound") 155 const isNotFound = 156 error?.status === 400 && error?.error === "RecordNotFound" || 157 error?.status === 404 || 158 error?.message?.includes("not found") || 159 error?.message?.includes("Could not locate record"); 160 161 if (isNotFound) { 162 console.log( 163 "🔹 No existing autonomy declaration found - creating new...", 164 ); 165 } else { 166 // Re-throw if it's not a "not found" error 167 throw error; 168 } 169 } 170 171 // Create or update the record 172 const result = await bsky.com.atproto.repo.putRecord({ 173 repo, 174 collection: "studio.voyager.account.autonomy", 175 rkey: "self", 176 record, 177 }); 178 179 console.log( 180 `🔹 Autonomy declaration ${exists ? "updated" : "created"} successfully:`, 181 result, 182 ); 183 return result; 184 } catch (error) { 185 console.error("Error submitting autonomy declaration record:", error); 186 throw error; 187 } 188};