a tool to help your Letta AI agents navigate bluesky
1import { bsky } from "../utils/bsky.ts";
2import type { AutonomyDeclarationRecord } from "./types.ts";
3import { Lexicons } from "@atproto/lexicon";
4
5export const AUTONOMY_DECLARATION_LEXICON = {
6 "lexicon": 1,
7 "id": "studio.voyager.account.autonomy",
8 "defs": {
9 "main": {
10 "type": "record",
11 "key": "literal:self",
12 "record": {
13 "type": "object",
14 "properties": {
15 "automationLevel": {
16 "type": "string",
17 "knownValues": [
18 "human",
19 "assisted",
20 "collaborative",
21 "automated",
22 ],
23 "description":
24 "Level of automation in account management and content creation",
25 },
26 "usesGenerativeAI": {
27 "type": "boolean",
28 "description":
29 "Whether this account uses generative AI (LLMs, image generation, etc.) to create content",
30 },
31 "description": {
32 "type": "string",
33 "maxGraphemes": 300,
34 "description":
35 "Plain language explanation of how this account is automated and what it does",
36 },
37 "responsibleParty": {
38 "type": "object",
39 "properties": {
40 "type": {
41 "type": "string",
42 "knownValues": [
43 "person",
44 "organization",
45 ],
46 "description":
47 "Whether the responsible party is a person or organization",
48 },
49 "name": {
50 "type": "string",
51 "maxGraphemes": 100,
52 "description": "Name of the person or organization responsible",
53 },
54 "contact": {
55 "type": "string",
56 "maxLength": 300,
57 "description":
58 "Contact information (email, URL, handle, or DID)",
59 },
60 "did": {
61 "type": "string",
62 "format": "did",
63 "description":
64 "DID of the responsible party if they have an ATProto identity",
65 },
66 },
67 "description":
68 "Information about who is accountable for this account's automated behavior",
69 },
70 "disclosureUrl": {
71 "type": "string",
72 "format": "uri",
73 "description":
74 "URL with additional information about this account's automation",
75 },
76 "createdAt": {
77 "type": "string",
78 "format": "datetime",
79 "description": "Timestamp when this declaration was created",
80 },
81 },
82 "required": [
83 "createdAt",
84 ],
85 },
86 "description":
87 "Declaration of automation and AI usage for transparency and accountability",
88 },
89 },
90};
91
92export const createAutonomyDeclarationRecord = async () => {
93 const automationLevel = Deno.env.get("AUTOMATION_LEVEL")?.toLowerCase();
94 const projectDescription = Deno.env.get("PROJECT_DESCRIPTION");
95 const disclosureUrl = Deno.env.get("DISCLOSURE_URL");
96
97 const responsiblePartyType = Deno.env.get("RESPONSIBLE_PARTY_TYPE")
98 ?.toLowerCase();
99 const responsiblePartyName = Deno.env.get("RESPONSIBLE_PARTY_NAME");
100 const responsiblePartyContact = Deno.env.get("RESPONSIBLE_PARTY_CONTACT");
101 const responsiblePartyBsky = Deno.env.get("RESPONSIBLE_PARTY_BSKY");
102
103 const declarationRecord: AutonomyDeclarationRecord = {
104 $type: "studio.voyager.account.autonomy",
105 usesGenerativeAI: true, // Always true for this project
106 automationLevel: (automationLevel === "assisted" ||
107 automationLevel === "collaborative" ||
108 automationLevel === "automated")
109 ? automationLevel
110 : "automated", // Default to automated if not specified or invalid
111 createdAt: new Date().toISOString(),
112 };
113
114 // Add description if provided
115 if (projectDescription?.trim()) {
116 declarationRecord.description = projectDescription.trim();
117 }
118
119 // Add disclosure URL if provided
120 if (disclosureUrl?.trim()) {
121 declarationRecord.disclosureUrl = disclosureUrl.trim();
122 }
123
124 // Build responsible party object if any fields are provided
125 if (
126 responsiblePartyType ||
127 responsiblePartyName ||
128 responsiblePartyContact ||
129 responsiblePartyBsky
130 ) {
131 declarationRecord.responsibleParty = {};
132
133 // Add type if provided and valid
134 if (
135 responsiblePartyType === "person" ||
136 responsiblePartyType === "organization"
137 ) {
138 declarationRecord.responsibleParty.type = responsiblePartyType;
139 }
140
141 // Add name if provided
142 if (responsiblePartyName?.trim()) {
143 declarationRecord.responsibleParty.name = responsiblePartyName.trim();
144 }
145
146 // Add contact if provided
147 if (responsiblePartyContact?.trim()) {
148 declarationRecord.responsibleParty.contact = responsiblePartyContact
149 .trim();
150 }
151
152 // Handle DID or Handle from RESPONSIBLE_PARTY_BSKY
153 if (responsiblePartyBsky?.trim()) {
154 const bskyIdentifier = responsiblePartyBsky.trim();
155
156 // Check if it's a DID (starts with "did:")
157 if (bskyIdentifier.startsWith("did:")) {
158 declarationRecord.responsibleParty.did = bskyIdentifier;
159 } else {
160 // Assume it's a handle and resolve to DID
161 try {
162 const authorData = await bsky.getProfile({ actor: bskyIdentifier });
163 declarationRecord.responsibleParty.did = authorData.data.did;
164 } catch (error) {
165 console.warn(
166 `Failed to resolve DID for identifier ${bskyIdentifier}:`,
167 error,
168 );
169 // Continue without DID rather than failing
170 }
171 }
172 }
173 }
174
175 return declarationRecord;
176};
177
178export const submitAutonomyDeclarationRecord = async () => {
179 const lex = new Lexicons();
180
181 try {
182 lex.add(AUTONOMY_DECLARATION_LEXICON as any);
183 const record = await createAutonomyDeclarationRecord();
184
185 lex.assertValidRecord(
186 "studio.voyager.account.autonomy",
187 record,
188 );
189
190 const repo = bsky.session?.did;
191 if (!repo) {
192 throw new Error("Not logged in - no DID available");
193 }
194
195 // Check if record already exists
196 let exists = false;
197 try {
198 await bsky.com.atproto.repo.getRecord({
199 repo,
200 collection: "studio.voyager.account.autonomy",
201 rkey: "self",
202 });
203 exists = true;
204 console.log("Existing autonomy declaration found - updating...");
205 } catch (error: any) {
206 // Handle "record not found" errors (status 400 with error: "RecordNotFound")
207 const isNotFound =
208 error?.status === 400 && error?.error === "RecordNotFound" ||
209 error?.status === 404 ||
210 error?.message?.includes("not found") ||
211 error?.message?.includes("Could not locate record");
212
213 if (isNotFound) {
214 console.log("No existing autonomy declaration found - creating new...");
215 } else {
216 // Re-throw if it's not a "not found" error
217 throw error;
218 }
219 }
220
221 // Create or update the record
222 const result = await bsky.com.atproto.repo.putRecord({
223 repo,
224 collection: "studio.voyager.account.autonomy",
225 rkey: "self",
226 record,
227 });
228
229 console.log(
230 `Autonomy declaration ${exists ? "updated" : "created"} successfully:`,
231 result,
232 );
233 return result;
234 } catch (error) {
235 console.error("Error submitting autonomy declaration record:", error);
236 throw error;
237 }
238};