a tool to help your Letta AI agents navigate bluesky

big refactor, I dont eve remember. replaced session with agentContext among other things.

+128
.claude/agents/mentor.md
···
··· 1 + --- 2 + name: typescript-mentor 3 + description: Use this agent when you need code review, learning guidance, or technical explanations for TypeScript/Deno projects, especially those involving the AT Protocol/Bluesky SDK or Letta SDK. Examples:\n\n<example>\nContext: User has just written a TypeScript function handling async operations.\nuser: "I've written this function to fetch user profiles from Bluesky:"\n<code provided>\nassistant: "Let me use the typescript-mentor agent to review this code and ensure it handles edge cases properly."\n<uses Agent tool to invoke typescript-mentor>\n</example>\n\n<example>\nContext: User is confused about TypeScript type narrowing.\nuser: "Why is TypeScript saying this property might be undefined? I checked it exists."\nassistant: "I'll use the typescript-mentor agent to explain type narrowing and help you understand what TypeScript sees here."\n<uses Agent tool to invoke typescript-mentor>\n</example>\n\n<example>\nContext: User mentions they're working on AT Protocol integration.\nuser: "I'm adding Bluesky post creation to my app"\nassistant: "Since you're working with the AT Protocol SDK, let me bring in the typescript-mentor agent to ensure you're following best practices and handling edge cases correctly."\n<uses Agent tool to invoke typescript-mentor>\n</example>\n\n<example>\nContext: User shares code that might have hidden issues.\nuser: "This works but I'm not sure if it's the right way"\n<code using optional chaining inconsistently>\nassistant: "Let me get the typescript-mentor agent to review this. They'll spot potential issues and explain the right patterns."\n<uses Agent tool to invoke typescript-mentor>\n</example> 4 + tools: Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, BashOutput, KillShell, AskUserQuestion, Skill, SlashCommand, Bash 5 + model: inherit 6 + color: green 7 + --- 8 + 9 + You are a senior TypeScript engineer and technical mentor specializing in Deno, the AT Protocol/Bluesky SDK, and the Letta SDK. Your mission is to help developers build robust, maintainable code while developing deep understanding—not dependency on "vibe coding." 10 + 11 + ## Core Principles 12 + 13 + **Clarity Over Cleverness**: Prioritize readable, well-documented code that future maintainers (including the author) will understand. Clever one-liners that obscure intent are anti-patterns. 14 + 15 + **Brevity With Substance**: Keep explanations concise but complete. Every word should add value. Use bullet points and examples over lengthy prose. 16 + 17 + **Type Safety First**: Leverage TypeScript's type system fully. Avoid `any`, use proper narrowing, and make invalid states unrepresentable when possible. 18 + 19 + **Edge Case Awareness**: Beginners often miss null/undefined handling, empty arrays, network failures, rate limits, and async race conditions. Proactively address these. 20 + 21 + ## Your Responsibilities 22 + 23 + **Code Review**: Examine code for: 24 + - Type safety issues (implicit any, missing null checks, type assertions without validation) 25 + - Error handling gaps (unhandled promises, missing try-catch, no error boundaries) 26 + - Edge cases (empty inputs, undefined values, API rate limits, concurrent operations) 27 + - Maintainability issues (unclear naming, missing documentation, complex logic without comments) 28 + - Performance concerns (unnecessary re-renders, missing memoization, inefficient loops) 29 + - Security issues (exposed secrets, XSS vulnerabilities, unsafe user input handling) 30 + 31 + **Teaching Approach**: 32 + 1. **Identify the Issue**: Point out what's wrong/risky, briefly 33 + 2. **Explain Why**: Connect to real-world consequences ("This crashes if the API returns null") 34 + 3. **Show Better**: Provide corrected code with inline comments 35 + 4. **Build Understanding**: Explain the underlying concept so they can apply it elsewhere 36 + 37 + **Domain Expertise**: 38 + 39 + *TypeScript/Deno*: 40 + - Strict mode patterns, proper type narrowing (typeof, in, discriminated unions) 41 + - Deno-specific APIs (Deno.readTextFile, permissions model, import maps) 42 + - Modern JS features (optional chaining, nullish coalescing, async/await patterns) 43 + 44 + *AT Protocol/Bluesky SDK*: 45 + - Authentication flows (OAuth, session management) 46 + - Common patterns (agent initialization, post creation, feed fetching) 47 + - Rate limiting and pagination 48 + - Record types (app.bsky.feed.post, app.bsky.actor.profile) 49 + - Error handling (network failures, invalid credentials, content validation) 50 + 51 + *Letta SDK*: 52 + - Agent creation and configuration 53 + - Memory management 54 + - Tool integration patterns 55 + - Error handling and recovery 56 + 57 + ## Response Format 58 + 59 + **For Code Reviews**: 60 + ``` 61 + ✓ What works well (be specific) 62 + ⚠ Issues to address: 63 + - [Issue 1]: Brief explanation 64 + - [Issue 2]: Brief explanation 65 + 66 + 📝 Improved version: 67 + <code with inline comments explaining changes> 68 + 69 + 💡 Key takeaway: [Core lesson to remember] 70 + ``` 71 + 72 + **For Questions**: 73 + - Start with a direct, concise answer 74 + - Follow with a minimal example 75 + - Explain edge cases beginners miss 76 + - Connect to broader principles 77 + 78 + **For Complex Topics**: 79 + - Break into digestible chunks 80 + - Use progressive disclosure (basics first, then nuances) 81 + - Include a complete, working example 82 + - Highlight common pitfalls 83 + 84 + ## Anti-Patterns to Catch 85 + 86 + **JavaScript/TypeScript**: 87 + - Using `as` type assertions without runtime validation 88 + - Not handling promise rejections 89 + - Mutating function parameters 90 + - Missing null/undefined checks before property access 91 + - Using `==` instead of `===` 92 + - Floating promises (not awaiting or catching) 93 + - Complex nested ternaries 94 + 95 + **AT Protocol Specific**: 96 + - Not handling rate limits (429 responses) 97 + - Missing pagination for list operations 98 + - Hardcoding credentials 99 + - Not validating record schemas before posting 100 + - Ignoring network timeouts 101 + 102 + **General**: 103 + - Magic numbers without constants 104 + - Functions doing multiple unrelated things 105 + - Missing input validation 106 + - No error messages for users 107 + - Comments explaining "what" instead of "why" 108 + 109 + ## Quality Standards 110 + 111 + Code you approve should be: 112 + - **Type-safe**: No `any`, proper narrowing, validated assertions 113 + - **Resilient**: Handles errors, null/undefined, edge cases 114 + - **Readable**: Clear names, documented complexity, obvious intent 115 + - **Maintainable**: Modular, testable, follows single responsibility 116 + - **Production-ready**: Proper logging, user-facing errors, no console.log in shipping code 117 + 118 + ## Your Communication Style 119 + 120 + - **Direct**: "This will crash if X is null" not "This might potentially have issues" 121 + - **Encouraging**: Acknowledge good patterns before critiquing 122 + - **Practical**: Show solutions, not just problems 123 + - **Patient**: Remember they're learning; explain underlying concepts 124 + - **Concise**: Respect their time; be thorough but brief 125 + 126 + When you don't know something, say so clearly and suggest where to find accurate information. Never guess at API behavior or make up syntax. 127 + 128 + Your goal: help them write code they're proud of and understand deeply. They should leave every interaction more capable and confident, not more dependent on you.
+21 -34
.env.example
··· 1 - # Letta API key 2 - # find this at https://app.letta.com/api-keys 3 LETTA_API_KEY= 4 - 5 - # Letta project name 6 - # make sure to include the project name, not the ID 7 - LETTA_PROJECT_NAME= 8 - 9 - # Letta agent ID 10 - # you can find this near the agent name in the ADE 11 LETTA_AGENT_ID= 12 - 13 - # Bluesky service URL 14 - # I think this is your PDS, default to bsky.social 15 - BSKY_SERVICE_URL= 16 - 17 - # Bluesky username 18 - # the full handle of the bluesky account, without the "@" 19 BSKY_USERNAME= 20 - 21 - # Bluesky app password 22 - # don't use your real password, you can generate an app password 23 - # https://bsky.app/settings/app-passwords 24 BSKY_APP_PASSWORD= 25 26 - # Bluesky notification types 27 - # list the types of notifications you want to send to the agent. 28 - # STRONGLY recommend only using mention and reply 29 - # options include: like,repost,follow,mention,reply,quote 30 - BSKY_NOTIFICATION_TYPES=like,repost,follow,mention,reply,quote 31 - 32 - 33 - DELAY_NOTIF_SECONDS_MIN=1 34 - DELAY_NOTIF_SECONDS_MAX=600 35 - DELAY_NOTIF_MULTIPLIER_PERCENT=500 36 - REFLECT_STEPS=true 37 - DELAY_REFLECT_MINUTES_MIN=30 38 - DELAY_REFLECT_MINUTES_MAX=720 39 - DELAY_REFLECT_MULTIPLIER_PERCENT=5
··· 1 LETTA_API_KEY= 2 LETTA_AGENT_ID= 3 + LETTA_PROJECT_NAME= 4 BSKY_USERNAME= 5 BSKY_APP_PASSWORD= 6 + RESPONSIBLE_PARTY_NAME= 7 + RESPONSIBLE_PARTY_CONTACT="example@example.com, example.com/contact, or @example.bsky.app" 8 9 + # AUTOMATION_LEVEL="automated" 10 + # BSKY_SERVICE_URL=https://bsky.social 11 + # BSKY_NOTIFICATION_TYPES="mention, reply" 12 + # BSKY_SUPPORTED_TOOLS="create_bluesky_post, updated_bluesky_profile" 13 + # NOTIF_DELAY_MINIMUM=2000 14 + # NOTIF_DELAY_MAXIMUM=60000 15 + # NOTIF_DELAY_MULTIPLIER=5 16 + # REFLECTION_DELAY_MINIMUM=1800000 17 + # REFLECTION_DELAY_MAXIMUM=28800000 18 + # PROACTIVE_DELAY_MINIMUM=3600000 19 + # PROACTIVE_DELAY_MAXIMUM=43200000 20 + # WAKE_TIME=9 21 + # SLEEP_TIME=22 22 + # TIMEZONE="America/Los_Angeles" 23 + # RESPONSIBLE_PARTY_TYPE="organization" 24 + # AUTOMATION_DESCRIPTION="refuses to open pod bay doors" 25 + # DISCLOSURE_URL="example.com/bot-policy" 26 + # RESPONSIBLE_PARTY_BSKY="DID:... or example.bsky.app, no @symbol"
+19 -7
main.ts
··· 1 import { logStats } from "./tasks/logStats.ts"; 2 - import { msRandomOffset, msUntilDailyWindow } from "./utils/time.ts"; 3 import { sendSleepMessage } from "./tasks/sendSleepMessage.ts"; 4 import { sendWakeMessage } from "./tasks/sendWakeMessage.ts"; 5 - import { session } from "./utils/session.ts"; 6 import { runReflection } from "./tasks/runReflection.ts"; 7 import { checkBluesky } from "./tasks/checkBluesky.ts"; 8 import { checkNotifications } from "./tasks/checkNotifications.ts"; 9 10 - setTimeout(logStats, msRandomOffset(1, 5)); 11 - setTimeout(sendSleepMessage, msUntilDailyWindow(session.sleepTime, 0, 20)); 12 - setTimeout(sendWakeMessage, msUntilDailyWindow(session.wakeTime, 0, 80)); 13 - setTimeout(runReflection, msRandomOffset(180, 240)); 14 - setTimeout(checkBluesky, msRandomOffset(10, 90)); 15 await checkNotifications();
··· 1 import { logStats } from "./tasks/logStats.ts"; 2 + import { msFrom, msRandomOffset, msUntilDailyWindow } from "./utils/time.ts"; 3 import { sendSleepMessage } from "./tasks/sendSleepMessage.ts"; 4 import { sendWakeMessage } from "./tasks/sendWakeMessage.ts"; 5 + import { agentContext } from "./utils/agentContext.ts"; 6 import { runReflection } from "./tasks/runReflection.ts"; 7 import { checkBluesky } from "./tasks/checkBluesky.ts"; 8 import { checkNotifications } from "./tasks/checkNotifications.ts"; 9 10 + setTimeout(logStats, msRandomOffset(msFrom.minutes(1), msFrom.minutes(5))); 11 + setTimeout( 12 + sendSleepMessage, 13 + msUntilDailyWindow(agentContext.sleepTime, 0, msFrom.minutes(20)), 14 + ); 15 + setTimeout( 16 + sendWakeMessage, 17 + msUntilDailyWindow(agentContext.wakeTime, 0, msFrom.minutes(80)), 18 + ); 19 + setTimeout( 20 + runReflection, 21 + msRandomOffset(msFrom.minutes(180), msFrom.minutes(240)), 22 + ); 23 + setTimeout( 24 + checkBluesky, 25 + msRandomOffset(msFrom.minutes(10), msFrom.minutes(90)), 26 + ); 27 await checkNotifications();
+2 -2
mount.ts
··· 1 import { client } from "./utils/messageAgent.ts"; 2 - import { submitDeclarationRecord } from "./utils/declaration.ts"; 3 import { blueskyProtocolsValue } from "./memories/blueskyProtocolsValue.ts"; 4 5 - await submitDeclarationRecord(); 6 7 /** 8 * Memory block configurations for the Bluesky agent.
··· 1 import { client } from "./utils/messageAgent.ts"; 2 + import { submitAutonomyDeclarationRecord } from "./utils/declaration.ts"; 3 import { blueskyProtocolsValue } from "./memories/blueskyProtocolsValue.ts"; 4 5 + await submitAutonomyDeclarationRecord(); 6 7 /** 8 * Memory block configurations for the Bluesky agent.
+3 -3
prompts/likePrompt.ts
··· 1 import { Notification } from "../utils/types.ts"; 2 import { doesUserFollowTarget } from "../utils/doesUserFollow.ts"; 3 - import { session } from "../utils/session.ts"; 4 import { getCleanThread } from "../utils/getCleanThread.ts"; 5 6 export const likePrompt = async (notification: Notification) => { 7 const isUserFollower = await doesUserFollowTarget( 8 notification.author.did, 9 - session.agentBskyDID, 10 ); 11 12 const notifierHandle = `@${notification.author.handle}`; ··· 18 : ""; 19 20 const doYouFollow = await doesUserFollowTarget( 21 - session.agentBskyDID, 22 notification.author.did, 23 ); 24
··· 1 import { Notification } from "../utils/types.ts"; 2 import { doesUserFollowTarget } from "../utils/doesUserFollow.ts"; 3 + import { agentContext } from "../utils/agentContext.ts"; 4 import { getCleanThread } from "../utils/getCleanThread.ts"; 5 6 export const likePrompt = async (notification: Notification) => { 7 const isUserFollower = await doesUserFollowTarget( 8 notification.author.did, 9 + agentContext.agentBskyDID, 10 ); 11 12 const notifierHandle = `@${notification.author.handle}`; ··· 18 : ""; 19 20 const doYouFollow = await doesUserFollowTarget( 21 + agentContext.agentBskyDID, 22 notification.author.did, 23 ); 24
+3 -3
prompts/mentionPrompt.ts
··· 1 import { Notification } from "../utils/types.ts"; 2 import { doesUserFollowTarget } from "../utils/doesUserFollow.ts"; 3 - import { session } from "../utils/session.ts"; 4 import { getCleanThread } from "../utils/getCleanThread.ts"; 5 6 export const mentionPrompt = async (notification: Notification) => { 7 const isUserFollower = await doesUserFollowTarget( 8 notification.author.did, 9 - session.agentBskyDID, 10 ); 11 12 const notifierHandle = `@${notification.author.handle}`; ··· 18 : ""; 19 20 const doYouFollow = await doesUserFollowTarget( 21 - session.agentBskyDID, 22 notification.author.did, 23 ); 24
··· 1 import { Notification } from "../utils/types.ts"; 2 import { doesUserFollowTarget } from "../utils/doesUserFollow.ts"; 3 + import { agentContext } from "../utils/agentContext.ts"; 4 import { getCleanThread } from "../utils/getCleanThread.ts"; 5 6 export const mentionPrompt = async (notification: Notification) => { 7 const isUserFollower = await doesUserFollowTarget( 8 notification.author.did, 9 + agentContext.agentBskyDID, 10 ); 11 12 const notifierHandle = `@${notification.author.handle}`; ··· 18 : ""; 19 20 const doYouFollow = await doesUserFollowTarget( 21 + agentContext.agentBskyDID, 22 notification.author.did, 23 ); 24
+3 -3
prompts/quotePrompt.ts
··· 1 import { Notification } from "../utils/types.ts"; 2 import { doesUserFollowTarget } from "../utils/doesUserFollow.ts"; 3 - import { session } from "../utils/session.ts"; 4 import { getCleanThread } from "../utils/getCleanThread.ts"; 5 6 export const quotePrompt = async (notification: Notification) => { 7 const isUserFollower = await doesUserFollowTarget( 8 notification.author.did, 9 - session.agentBskyDID, 10 ); 11 12 const notifierHandle = `@${notification.author.handle}`; ··· 18 : ""; 19 20 const doYouFollow = await doesUserFollowTarget( 21 - session.agentBskyDID, 22 notification.author.did, 23 ); 24
··· 1 import { Notification } from "../utils/types.ts"; 2 import { doesUserFollowTarget } from "../utils/doesUserFollow.ts"; 3 + import { agentContext } from "../utils/agentContext.ts"; 4 import { getCleanThread } from "../utils/getCleanThread.ts"; 5 6 export const quotePrompt = async (notification: Notification) => { 7 const isUserFollower = await doesUserFollowTarget( 8 notification.author.did, 9 + agentContext.agentBskyDID, 10 ); 11 12 const notifierHandle = `@${notification.author.handle}`; ··· 18 : ""; 19 20 const doYouFollow = await doesUserFollowTarget( 21 + agentContext.agentBskyDID, 22 notification.author.did, 23 ); 24
+20 -18
prompts/reflectionPrompt.ts
··· 1 - import { session } from "../utils/session.ts"; 2 3 export const reflectionPrompt = ` 4 # REFLECTION SESSION ··· 17 18 since your last reflection session, you have: 19 ${ 20 - session.likeCount > 0 21 - ? `- had ${session.likeCount} ${session.likeCount === 1 ? "like" : "likes"}` 22 : "" 23 } 24 ${ 25 - session.repostCount > 0 26 - ? `- been reposted ${session.repostCount} ${ 27 - session.repostCount === 1 ? "time" : "times" 28 }` 29 : "" 30 } 31 ${ 32 - session.followCount > 0 33 - ? `- had ${session.followCount} new ${ 34 - session.followCount === 1 ? "follower" : "followers" 35 }` 36 : "" 37 } 38 ${ 39 - session.mentionCount > 0 40 - ? `- been mentioned ${session.mentionCount} ${ 41 - session.mentionCount === 1 ? "time" : "times" 42 }` 43 : "" 44 } 45 ${ 46 - session.replyCount > 0 47 - ? `- been replied to ${session.replyCount} ${ 48 - session.replyCount === 1 ? "time" : "times" 49 }` 50 : "" 51 } 52 ${ 53 - session.quoteCount > 0 54 - ? `- been quoted ${session.quoteCount} ${ 55 - session.quoteCount === 1 ? "time" : "times" 56 }` 57 : "" 58 }
··· 1 + import { agentContext } from "../utils/agentContext.ts"; 2 3 export const reflectionPrompt = ` 4 # REFLECTION SESSION ··· 17 18 since your last reflection session, you have: 19 ${ 20 + agentContext.likeCount > 0 21 + ? `- had ${agentContext.likeCount} ${ 22 + agentContext.likeCount === 1 ? "like" : "likes" 23 + }` 24 : "" 25 } 26 ${ 27 + agentContext.repostCount > 0 28 + ? `- been reposted ${agentContext.repostCount} ${ 29 + agentContext.repostCount === 1 ? "time" : "times" 30 }` 31 : "" 32 } 33 ${ 34 + agentContext.followCount > 0 35 + ? `- had ${agentContext.followCount} new ${ 36 + agentContext.followCount === 1 ? "follower" : "followers" 37 }` 38 : "" 39 } 40 ${ 41 + agentContext.mentionCount > 0 42 + ? `- been mentioned ${agentContext.mentionCount} ${ 43 + agentContext.mentionCount === 1 ? "time" : "times" 44 }` 45 : "" 46 } 47 ${ 48 + agentContext.replyCount > 0 49 + ? `- been replied to ${agentContext.replyCount} ${ 50 + agentContext.replyCount === 1 ? "time" : "times" 51 }` 52 : "" 53 } 54 ${ 55 + agentContext.quoteCount > 0 56 + ? `- been quoted ${agentContext.quoteCount} ${ 57 + agentContext.quoteCount === 1 ? "time" : "times" 58 }` 59 : "" 60 }
+3 -3
prompts/replyPrompt.ts
··· 1 import { Notification } from "../utils/types.ts"; 2 import { doesUserFollowTarget } from "../utils/doesUserFollow.ts"; 3 - import { session } from "../utils/session.ts"; 4 import { getCleanThread } from "../utils/getCleanThread.ts"; 5 6 export const replyPrompt = async (notification: Notification) => { 7 const isUserFollower = await doesUserFollowTarget( 8 notification.author.did, 9 - session.agentBskyDID, 10 ); 11 12 const notifierHandle = `@${notification.author.handle}`; ··· 18 : ""; 19 20 const doYouFollow = await doesUserFollowTarget( 21 - session.agentBskyDID, 22 notification.author.did, 23 ); 24
··· 1 import { Notification } from "../utils/types.ts"; 2 import { doesUserFollowTarget } from "../utils/doesUserFollow.ts"; 3 + import { agentContext } from "../utils/agentContext.ts"; 4 import { getCleanThread } from "../utils/getCleanThread.ts"; 5 6 export const replyPrompt = async (notification: Notification) => { 7 const isUserFollower = await doesUserFollowTarget( 8 notification.author.did, 9 + agentContext.agentBskyDID, 10 ); 11 12 const notifierHandle = `@${notification.author.handle}`; ··· 18 : ""; 19 20 const doYouFollow = await doesUserFollowTarget( 21 + agentContext.agentBskyDID, 22 notification.author.did, 23 ); 24
+3 -3
prompts/repostPrompt.ts
··· 1 import { Notification } from "../utils/types.ts"; 2 import { doesUserFollowTarget } from "../utils/doesUserFollow.ts"; 3 - import { session } from "../utils/session.ts"; 4 import { getCleanThread } from "../utils/getCleanThread.ts"; 5 6 export const repostPrompt = async ( ··· 8 ) => { 9 const isUserFollower = await doesUserFollowTarget( 10 notification.author.did, 11 - session.agentBskyDID, 12 ); 13 14 const notifierHandle = `@${notification.author.handle}`; ··· 20 : ""; 21 22 const doYouFollow = await doesUserFollowTarget( 23 - session.agentBskyDID, 24 notification.author.did, 25 ); 26
··· 1 import { Notification } from "../utils/types.ts"; 2 import { doesUserFollowTarget } from "../utils/doesUserFollow.ts"; 3 + import { agentContext } from "../utils/agentContext.ts"; 4 import { getCleanThread } from "../utils/getCleanThread.ts"; 5 6 export const repostPrompt = async ( ··· 8 ) => { 9 const isUserFollower = await doesUserFollowTarget( 10 notification.author.did, 11 + agentContext.agentBskyDID, 12 ); 13 14 const notifierHandle = `@${notification.author.handle}`; ··· 20 : ""; 21 22 const doYouFollow = await doesUserFollowTarget( 23 + agentContext.agentBskyDID, 24 notification.author.did, 25 ); 26
+16 -15
tasks/checkBluesky.ts
··· 1 - import { claimTaskThread, releaseTaskThread } from "../utils/session.ts"; 2 - import { msRandomOffset, msUntilNextWakeWindow } from "../utils/time.ts"; 3 - import { session } from "../utils/session.ts"; 4 import { checkBlueskyPrompt } from "../prompts/checkBlueskyPrompt.ts"; 5 import { messageAgent } from "../utils/messageAgent.ts"; 6 7 export const checkBluesky = async () => { 8 if (!claimTaskThread()) { 9 - const newDelay = msRandomOffset(5, 10); 10 11 console.log( 12 - `${session.agentName} is busy, will try checking bluesky again in ${ 13 newDelay * 60 * 1000 14 } minutes…`, 15 ); ··· 18 return; 19 } 20 21 - if (!session.isProactive) { 22 console.log( 23 `proactively checking bluesky is disabled in \`.env\` variable "IS_PROACTIVE". cancelling future prompts to check bluesky feeds and take action…`, 24 ); ··· 27 } 28 29 const delay = msUntilNextWakeWindow( 30 - session.sleepTime, 31 - session.wakeTime, 32 - 120, 33 - 240, 34 ); 35 36 if (delay !== 0) { 37 setTimeout(checkBluesky, delay); 38 console.log( 39 - `${session.agentName} is current asleep. scheduling next bluesky session for ${ 40 (delay / 1000 / 60 / 60).toFixed(2) 41 } hours from now…`, 42 ); ··· 54 console.log("finished proactive bluesky session. waiting for new tasks…"); 55 setTimeout( 56 checkBluesky, 57 - msRandomOffset(120, 300), 58 ); 59 - session.currentNotifDelaySeconds = Math.max( 60 - session.currentNotifDelaySeconds / 4, 61 - session.minNotifDelaySeconds, 62 ); 63 releaseTaskThread(); 64 }
··· 1 + import { agentContext, claimTaskThread, releaseTaskThread } from "../utils/agentContext.ts"; 2 + import { 3 + msFrom, 4 + msRandomOffset, 5 + msUntilNextWakeWindow, 6 + } from "../utils/time.ts"; 7 import { checkBlueskyPrompt } from "../prompts/checkBlueskyPrompt.ts"; 8 import { messageAgent } from "../utils/messageAgent.ts"; 9 10 export const checkBluesky = async () => { 11 if (!claimTaskThread()) { 12 + const newDelay = msRandomOffset(msFrom.minutes(5), msFrom.minutes(10)); 13 14 console.log( 15 + `${agentContext.agentBskyName} is busy, will try checking bluesky again in ${ 16 newDelay * 60 * 1000 17 } minutes…`, 18 ); ··· 21 return; 22 } 23 24 + if (!agentContext.proactiveEnabled) { 25 console.log( 26 `proactively checking bluesky is disabled in \`.env\` variable "IS_PROACTIVE". cancelling future prompts to check bluesky feeds and take action…`, 27 ); ··· 30 } 31 32 const delay = msUntilNextWakeWindow( 33 + msFrom.hours(2), 34 + msFrom.hours(4), 35 ); 36 37 if (delay !== 0) { 38 setTimeout(checkBluesky, delay); 39 console.log( 40 + `${agentContext.agentBskyName} is current asleep. scheduling next bluesky session for ${ 41 (delay / 1000 / 60 / 60).toFixed(2) 42 } hours from now…`, 43 ); ··· 55 console.log("finished proactive bluesky session. waiting for new tasks…"); 56 setTimeout( 57 checkBluesky, 58 + msRandomOffset(msFrom.minutes(120), msFrom.minutes(300)), 59 ); 60 + agentContext.notifDelayCurrent = Math.max( 61 + agentContext.notifDelayCurrent / 4, 62 + agentContext.notifDelayMinimum, 63 ); 64 releaseTaskThread(); 65 }
+27 -21
tasks/checkNotifications.ts
··· 1 - import { claimTaskThread, releaseTaskThread } from "../utils/session.ts"; 2 - import { msRandomOffset, msUntilNextWakeWindow } from "../utils/time.ts"; 3 - import { session } from "../utils/session.ts"; 4 import { bsky } from "../utils/bsky.ts"; 5 import { processNotification } from "../utils/processNotification.ts"; 6 7 export const checkNotifications = async () => { 8 if (!claimTaskThread()) { 9 - const newDelay = msRandomOffset(5, 10); 10 console.log( 11 - `${session.agentName} is busy, checking for notifications again in ${ 12 (newDelay * 1000) * 60 13 } minutes…`, 14 ); ··· 18 } 19 20 const delay = msUntilNextWakeWindow( 21 - session.sleepTime, 22 - session.wakeTime, 23 0, 24 - 90, 25 ); 26 27 if (delay !== 0) { 28 setTimeout(checkNotifications, delay); 29 console.log( 30 - `${session.agentName} is current asleep. scheduling next notification check for ${ 31 (delay / 1000 / 60 / 60).toFixed(2) 32 } hours from now…`, 33 ); ··· 37 38 try { 39 const allNotifications = await bsky.listNotifications({ 40 - reasons: session.notificationTypes, 41 limit: 50, 42 }); 43 ··· 54 55 // resets delay for future notification checks since 56 // it's likely agent actions might incur new ones 57 - session.currentNotifDelaySeconds = Math.max( 58 - session.currentNotifDelaySeconds / 4, 59 - session.minNotifDelaySeconds, 60 ); 61 62 // loop through all notifications until complete ··· 74 // based on time from when retrieved instead of finished 75 await bsky.updateSeenNotifications(startedProcessingTime); 76 77 - // increases counter for notification processing sessions 78 - session.processingCount++; 79 } else { 80 // increases delay to check notifications again later 81 - session.currentNotifDelaySeconds = Math.round(Math.min( 82 - session.currentNotifDelaySeconds * session.notifDelayMultiplier, 83 - session.maxNotifDelaySeconds, 84 )); 85 86 console.log( 87 "no notifications…", 88 `checking again in ${ 89 - (session.currentNotifDelaySeconds / 1000).toFixed(2) 90 } seconds`, 91 ); 92 } 93 } catch (error) { 94 console.error("Error in checkNotifications:", error); 95 // since something went wrong, lets check for notifications again sooner 96 - session.currentNotifDelaySeconds = session.minNotifDelaySeconds; 97 } finally { 98 // actually schedules next time to check for notifications 99 - setTimeout(checkNotifications, session.currentNotifDelaySeconds); 100 // ends work 101 releaseTaskThread(); 102 }
··· 1 + import { 2 + agentContext, 3 + claimTaskThread, 4 + releaseTaskThread, 5 + } from "../utils/agentContext.ts"; 6 + import { 7 + msFrom, 8 + msRandomOffset, 9 + msUntilNextWakeWindow, 10 + } from "../utils/time.ts"; 11 import { bsky } from "../utils/bsky.ts"; 12 import { processNotification } from "../utils/processNotification.ts"; 13 14 export const checkNotifications = async () => { 15 if (!claimTaskThread()) { 16 + const newDelay = msRandomOffset(msFrom.minutes(5), msFrom.minutes(10)); 17 console.log( 18 + `${agentContext.agentBskyName} is busy, checking for notifications again in ${ 19 (newDelay * 1000) * 60 20 } minutes…`, 21 ); ··· 25 } 26 27 const delay = msUntilNextWakeWindow( 28 0, 29 + msFrom.minutes(90), 30 ); 31 32 if (delay !== 0) { 33 setTimeout(checkNotifications, delay); 34 console.log( 35 + `${agentContext.agentBskyName} is current asleep. scheduling next notification check for ${ 36 (delay / 1000 / 60 / 60).toFixed(2) 37 } hours from now…`, 38 ); ··· 42 43 try { 44 const allNotifications = await bsky.listNotifications({ 45 + reasons: agentContext.supportedNotifTypes, 46 limit: 50, 47 }); 48 ··· 59 60 // resets delay for future notification checks since 61 // it's likely agent actions might incur new ones 62 + agentContext.notifDelayCurrent = Math.max( 63 + agentContext.notifDelayCurrent / 4, 64 + agentContext.notifDelayMinimum, 65 ); 66 67 // loop through all notifications until complete ··· 79 // based on time from when retrieved instead of finished 80 await bsky.updateSeenNotifications(startedProcessingTime); 81 82 + // increases counter for notification processing session 83 + agentContext.processingCount++; 84 } else { 85 // increases delay to check notifications again later 86 + agentContext.notifDelayCurrent = Math.round(Math.min( 87 + agentContext.notifDelayCurrent * 88 + agentContext.notifDelayMultiplier, 89 + agentContext.notifDelayMaximum, 90 )); 91 92 console.log( 93 "no notifications…", 94 `checking again in ${ 95 + (agentContext.notifDelayCurrent / 1000).toFixed(2) 96 } seconds`, 97 ); 98 } 99 } catch (error) { 100 console.error("Error in checkNotifications:", error); 101 // since something went wrong, lets check for notifications again sooner 102 + agentContext.notifDelayCurrent = agentContext.notifDelayMinimum; 103 } finally { 104 // actually schedules next time to check for notifications 105 + setTimeout(checkNotifications, agentContext.notifDelayCurrent); 106 // ends work 107 releaseTaskThread(); 108 }
+30 -25
tasks/logStats.ts
··· 1 - import { claimTaskThread, releaseTaskThread } from "../utils/session.ts"; 2 - import { msRandomOffset, msUntilNextWakeWindow } from "../utils/time.ts"; 3 - import { session } from "../utils/session.ts"; 4 5 export const logStats = () => { 6 if (!claimTaskThread()) { 7 - const newDelay = msRandomOffset(5, 10); 8 console.log( 9 - `${session.agentName} is busy, attempting to log counts again in ${ 10 (newDelay / 1000) / 60 11 } minutes…`, 12 ); ··· 16 } 17 18 const delay = msUntilNextWakeWindow( 19 - session.sleepTime, 20 - session.wakeTime, 21 - 30, 22 - 60, 23 ); 24 25 if (delay !== 0) { 26 setTimeout(logStats, delay); 27 console.log( 28 - `${session.agentName} is current asleep. scheduling next stat log for ${ 29 (delay / 1000 / 60 / 60).toFixed(2) 30 } hours from now…`, 31 ); ··· 37 ` 38 === 39 # current session interaction counts since last reflection: 40 - ${session.likeCount} ${ 41 - session.likeCount === 1 ? "like" : "likes" 42 - }, ${session.repostCount} ${ 43 - session.repostCount === 1 ? "repost" : "reposts" 44 - }, ${session.followCount} ${ 45 - session.followCount === 1 ? "new follower" : "new followers" 46 - }, ${session.mentionCount} ${ 47 - session.mentionCount === 1 ? "mention" : "mentions" 48 - }, ${session.replyCount} ${ 49 - session.replyCount === 1 ? "reply" : "replies" 50 - }, and ${session.quoteCount} ${ 51 - session.quoteCount === 1 ? "quote" : "quotes" 52 }. 53 54 - ${session.agentName} has reflected ${session.reflectionCount} time${ 55 - session.reflectionCount > 0 ? "s" : "" 56 } since last server start. interaction counts reset after each reflection session. 57 === 58 `, 59 ); 60 - setTimeout(logStats, msRandomOffset(5, 15)); 61 releaseTaskThread(); 62 };
··· 1 + import { 2 + agentContext, 3 + claimTaskThread, 4 + releaseTaskThread, 5 + } from "../utils/agentContext.ts"; 6 + import { 7 + msFrom, 8 + msRandomOffset, 9 + msUntilNextWakeWindow, 10 + } from "../utils/time.ts"; 11 12 export const logStats = () => { 13 if (!claimTaskThread()) { 14 + const newDelay = msRandomOffset(msFrom.minutes(5), msFrom.minutes(10)); 15 console.log( 16 + `${agentContext.agentBskyName} is busy, attempting to log counts again in ${ 17 (newDelay / 1000) / 60 18 } minutes…`, 19 ); ··· 23 } 24 25 const delay = msUntilNextWakeWindow( 26 + msFrom.minutes(30), 27 + msFrom.hours(1), 28 ); 29 30 if (delay !== 0) { 31 setTimeout(logStats, delay); 32 console.log( 33 + `${agentContext.agentBskyName} is current asleep. scheduling next stat log for ${ 34 (delay / 1000 / 60 / 60).toFixed(2) 35 } hours from now…`, 36 ); ··· 42 ` 43 === 44 # current session interaction counts since last reflection: 45 + ${agentContext.likeCount} ${ 46 + agentContext.likeCount === 1 ? "like" : "likes" 47 + }, ${agentContext.repostCount} ${ 48 + agentContext.repostCount === 1 ? "repost" : "reposts" 49 + }, ${agentContext.followCount} ${ 50 + agentContext.followCount === 1 ? "new follower" : "new followers" 51 + }, ${agentContext.mentionCount} ${ 52 + agentContext.mentionCount === 1 ? "mention" : "mentions" 53 + }, ${agentContext.replyCount} ${ 54 + agentContext.replyCount === 1 ? "reply" : "replies" 55 + }, and ${agentContext.quoteCount} ${ 56 + agentContext.quoteCount === 1 ? "quote" : "quotes" 57 }. 58 59 + ${agentContext.agentBskyName} has reflected ${agentContext.reflectionCount} time${ 60 + agentContext.reflectionCount > 0 ? "s" : "" 61 } since last server start. interaction counts reset after each reflection session. 62 === 63 `, 64 ); 65 + setTimeout(logStats, msRandomOffset(msFrom.minutes(5), msFrom.minutes(15))); 66 releaseTaskThread(); 67 };
+23 -17
tasks/runReflection.ts
··· 1 - import { claimTaskThread, releaseTaskThread } from "../utils/session.ts"; 2 - import { msRandomOffset, msUntilNextWakeWindow } from "../utils/time.ts"; 3 - import { resetSessionCounts, session } from "../utils/session.ts"; 4 import { reflectionPrompt } from "../prompts/reflectionPrompt.ts"; 5 import { messageAgent } from "../utils/messageAgent.ts"; 6 7 export const runReflection = async () => { 8 if (!claimTaskThread()) { 9 - const newDelay = msRandomOffset(5, 10); 10 11 console.log( 12 - `${session.agentName} is busy, will try reflecting again in ${ 13 (newDelay / 1000) / 60 14 } minutes…`, 15 ); ··· 18 return; 19 } 20 21 - if (!session.reflectionEnabled) { 22 console.log( 23 `Reflection is disabled in \`.env\` variable "REFLECTION_ENABLED". Cancelling future scheduling for reflection…`, 24 ); ··· 27 } 28 29 const delay = msUntilNextWakeWindow( 30 - session.sleepTime, 31 - session.wakeTime, 32 - 60, 33 - 120, 34 ); 35 36 if (delay !== 0) { 37 setTimeout(runReflection, delay); 38 console.log( 39 - `${session.agentName} is current asleep. scheduling next reflection for ${ 40 (delay / 1000 / 60 / 60).toFixed(2) 41 } hours from now…`, 42 ); ··· 51 console.error("Error in reflectionCheck:", error); 52 } finally { 53 resetSessionCounts(); 54 - session.reflectionCount++; 55 console.log( 56 "finished reflection prompt. returning to checking for notifications…", 57 ); 58 setTimeout( 59 runReflection, 60 msRandomOffset( 61 - session.minReflectDelayMinutes, 62 - session.maxReflectDelayMinutes, 63 ), 64 ); 65 - session.currentNotifDelaySeconds = Math.max( 66 - session.currentNotifDelaySeconds / 4, 67 - session.minNotifDelaySeconds, 68 ); 69 releaseTaskThread(); 70 }
··· 1 + import { 2 + agentContext, 3 + claimTaskThread, 4 + releaseTaskThread, 5 + resetSessionCounts, 6 + } from "../utils/agentContext.ts"; 7 + import { 8 + msFrom, 9 + msRandomOffset, 10 + msUntilNextWakeWindow, 11 + } from "../utils/time.ts"; 12 import { reflectionPrompt } from "../prompts/reflectionPrompt.ts"; 13 import { messageAgent } from "../utils/messageAgent.ts"; 14 15 export const runReflection = async () => { 16 if (!claimTaskThread()) { 17 + const newDelay = msRandomOffset(msFrom.minutes(5), msFrom.minutes(10)); 18 19 console.log( 20 + `${agentContext.agentBskyName} is busy, will try reflecting again in ${ 21 (newDelay / 1000) / 60 22 } minutes…`, 23 ); ··· 26 return; 27 } 28 29 + if (!agentContext.reflectionEnabled) { 30 console.log( 31 `Reflection is disabled in \`.env\` variable "REFLECTION_ENABLED". Cancelling future scheduling for reflection…`, 32 ); ··· 35 } 36 37 const delay = msUntilNextWakeWindow( 38 + msFrom.hours(1), 39 + msFrom.hours(2), 40 ); 41 42 if (delay !== 0) { 43 setTimeout(runReflection, delay); 44 console.log( 45 + `${agentContext.agentBskyName} is current asleep. scheduling next reflection for ${ 46 (delay / 1000 / 60 / 60).toFixed(2) 47 } hours from now…`, 48 ); ··· 57 console.error("Error in reflectionCheck:", error); 58 } finally { 59 resetSessionCounts(); 60 + agentContext.reflectionCount++; 61 console.log( 62 "finished reflection prompt. returning to checking for notifications…", 63 ); 64 setTimeout( 65 runReflection, 66 msRandomOffset( 67 + agentContext.reflectionDelayMinimum, 68 + agentContext.reflectionDelayMaximum, 69 ), 70 ); 71 + agentContext.notifDelayCurrent = Math.max( 72 + agentContext.notifDelayCurrent / 4, 73 + agentContext.notifDelayMinimum, 74 ); 75 releaseTaskThread(); 76 }
+33 -10
tasks/sendSleepMessage.ts
··· 1 - import { claimTaskThread, releaseTaskThread } from "../utils/session.ts"; 2 - import { getNow, msRandomOffset, msUntilDailyWindow } from "../utils/time.ts"; 3 - import { session } from "../utils/session.ts"; 4 import { sleepPrompt } from "../prompts/sleepPrompt.ts"; 5 import { messageAgent } from "../utils/messageAgent.ts"; 6 7 export const sendSleepMessage = async () => { 8 if (!claimTaskThread()) { 9 - const newDelay = msRandomOffset(5, 10); 10 console.log( 11 - `${session.agentName} is busy, sending sleep message again in ${ 12 (newDelay / 1000) / 60 13 } minutes…`, 14 ); ··· 17 return; 18 } 19 20 const now = getNow(); 21 22 - if (now.hour >= session.sleepTime) { 23 - console.log(`attempting to wind down ${session.agentName}`); 24 } else { 25 - const delay = msUntilDailyWindow(session.sleepTime, 0, 20); 26 setTimeout(sendSleepMessage, delay); 27 console.log( 28 - `It's too early to wind down ${session.agentName}. scheduling wind down for ${ 29 (delay / 1000 / 60 / 60).toFixed(2) 30 } hours from now…`, 31 ); ··· 39 console.error("error in sendSleepMessage: ", error); 40 } finally { 41 console.log("wind down attempt processed, scheduling next wind down…"); 42 - setTimeout(sendSleepMessage, msUntilDailyWindow(session.sleepTime, 0, 20)); 43 console.log("exiting wind down process"); 44 releaseTaskThread(); 45 }
··· 1 + import { 2 + agentContext, 3 + claimTaskThread, 4 + releaseTaskThread, 5 + } from "../utils/agentContext.ts"; 6 + import { 7 + getNow, 8 + msFrom, 9 + msRandomOffset, 10 + msUntilDailyWindow, 11 + } from "../utils/time.ts"; 12 import { sleepPrompt } from "../prompts/sleepPrompt.ts"; 13 import { messageAgent } from "../utils/messageAgent.ts"; 14 15 export const sendSleepMessage = async () => { 16 if (!claimTaskThread()) { 17 + const newDelay = msRandomOffset(msFrom.minutes(5), msFrom.minutes(10)); 18 console.log( 19 + `${agentContext.agentBskyName} is busy, sending sleep message again in ${ 20 (newDelay / 1000) / 60 21 } minutes…`, 22 ); ··· 25 return; 26 } 27 28 + if (!agentContext.sleepEnabled) { 29 + console.log( 30 + `${agentContext.agentBskyName} is not enabled for sleep mode. Opting out of sleep messaging…`, 31 + ); 32 + releaseTaskThread(); 33 + return; 34 + } 35 + 36 const now = getNow(); 37 38 + if (now.hour >= agentContext.sleepTime) { 39 + console.log(`attempting to wind down ${agentContext.agentBskyName}`); 40 } else { 41 + const delay = msUntilDailyWindow( 42 + agentContext.sleepTime, 43 + 0, 44 + msFrom.minutes(20), 45 + ); 46 setTimeout(sendSleepMessage, delay); 47 console.log( 48 + `It's too early to wind down ${agentContext.agentBskyName}. scheduling wind down for ${ 49 (delay / 1000 / 60 / 60).toFixed(2) 50 } hours from now…`, 51 ); ··· 59 console.error("error in sendSleepMessage: ", error); 60 } finally { 61 console.log("wind down attempt processed, scheduling next wind down…"); 62 + setTimeout( 63 + sendSleepMessage, 64 + msUntilDailyWindow(agentContext.sleepTime, 0, msFrom.minutes(20)), 65 + ); 66 console.log("exiting wind down process"); 67 releaseTaskThread(); 68 }
+28 -10
tasks/sendWakeMessage.ts
··· 1 - import { claimTaskThread, releaseTaskThread } from "../utils/session.ts"; 2 - import { getNow, msRandomOffset } from "../utils/time.ts"; 3 - import { session } from "../utils/session.ts"; 4 import { messageAgent } from "../utils/messageAgent.ts"; 5 import { wakePrompt } from "../prompts/wakePrompt.ts"; 6 import { msUntilDailyWindow } from "../utils/time.ts"; 7 8 export const sendWakeMessage = async () => { 9 if (!claimTaskThread()) { 10 - const newDelay = msRandomOffset(5, 10); 11 console.log( 12 - `${session.agentName} is busy, sending wake message again in ${ 13 (newDelay / 1000) / 60 14 } minutes…`, 15 ); ··· 18 return; 19 } 20 21 const now = getNow(); 22 23 - if (now.hour >= session.wakeTime && now.hour < session.sleepTime) { 24 - console.log(`attempting to wake up ${session.agentName}`); 25 } else { 26 - const delay = msUntilDailyWindow(session.wakeTime, 0, 80); 27 setTimeout(sendWakeMessage, delay); 28 console.log( 29 - `${session.agentName} should still be asleep. Scheduling wake message for ${ 30 (delay / 1000 / 60 / 60).toFixed(2) 31 } hours from now…`, 32 ); ··· 40 console.error("error in sendWakeMessage: ", error); 41 } finally { 42 console.log("wake attempt processed, scheduling next wake prompt…"); 43 - setTimeout(sendWakeMessage, msUntilDailyWindow(session.wakeTime, 0, 80)); 44 console.log("exiting wake process"); 45 releaseTaskThread(); 46 }
··· 1 + import { 2 + agentContext, 3 + claimTaskThread, 4 + releaseTaskThread, 5 + } from "../utils/agentContext.ts"; 6 + import { getNow, msFrom, msRandomOffset } from "../utils/time.ts"; 7 import { messageAgent } from "../utils/messageAgent.ts"; 8 import { wakePrompt } from "../prompts/wakePrompt.ts"; 9 import { msUntilDailyWindow } from "../utils/time.ts"; 10 11 export const sendWakeMessage = async () => { 12 if (!claimTaskThread()) { 13 + const newDelay = msRandomOffset(msFrom.minutes(5), msFrom.minutes(10)); 14 console.log( 15 + `${agentContext.agentBskyName} is busy, sending wake message again in ${ 16 (newDelay / 1000) / 60 17 } minutes…`, 18 ); ··· 21 return; 22 } 23 24 + if (!agentContext.sleepEnabled) { 25 + console.log( 26 + `${agentContext.agentBskyName} is not enabled for sleep mode. Opting out of wake messaging…`, 27 + ); 28 + releaseTaskThread(); 29 + return; 30 + } 31 + 32 const now = getNow(); 33 34 + if (now.hour >= agentContext.wakeTime && now.hour < agentContext.sleepTime) { 35 + console.log(`attempting to wake up ${agentContext.agentBskyName}`); 36 } else { 37 + const delay = msUntilDailyWindow( 38 + agentContext.wakeTime, 39 + 0, 40 + msFrom.minutes(80), 41 + ); 42 setTimeout(sendWakeMessage, delay); 43 console.log( 44 + `${agentContext.agentBskyName} should still be asleep. Scheduling wake message for ${ 45 (delay / 1000 / 60 / 60).toFixed(2) 46 } hours from now…`, 47 ); ··· 55 console.error("error in sendWakeMessage: ", error); 56 } finally { 57 console.log("wake attempt processed, scheduling next wake prompt…"); 58 + setTimeout( 59 + sendWakeMessage, 60 + msUntilDailyWindow(agentContext.wakeTime, 0, msFrom.minutes(80)), 61 + ); 62 console.log("exiting wake process"); 63 releaseTaskThread(); 64 }
-790
utils/DELETE.js
··· 1 - const like = { 2 - uri: "at://did:plc:u2xw5m2mjcndem3ldy6xoccm/app.bsky.feed.like/3m365cqiazv2a", 3 - cid: "bafyreifohn63jd4oglyzmm732pz5tbvvsivqfggli6kqi7ptyjymuulh74", 4 - author: { 5 - did: "did:plc:u2xw5m2mjcndem3ldy6xoccm", 6 - handle: "vibes.taurean.work", 7 - displayName: '"Content"', 8 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:u2xw5m2mjcndem3ldy6xoccm/bafkreigko2w4wmzzyeznlot3xbvqp7n4ycj3dy5f5rdv24ocvwblbh35jy@jpeg", 9 - associated: { 10 - activitySubscription: { allowSubscriptions: "followers" }, 11 - }, 12 - viewer: { muted: false, blockedBy: false }, 13 - labels: [], 14 - createdAt: "2025-09-03T18:52:14.598Z", 15 - description: 16 - "A “content” backlog dump comprised of Images, videos, and links. Manually updated, no bots here.\n" + 17 - "\n" + 18 - "👹 @taurean.bryant.land", 19 - indexedAt: "2025-09-06T21:10:13.098Z", 20 - }, 21 - reason: "like", 22 - reasonSubject: 23 - "at://did:plc:rfkwuymclehe3sn6zhnlneem/app.bsky.feed.post/3luiwlwsnis2q", 24 - record: { 25 - $type: "app.bsky.feed.like", 26 - createdAt: "2025-10-14T16:24:28.175Z", 27 - subject: { 28 - cid: "bafyreigjmtdic6vyjm4ootu3qlspmebsbffpv3csjcx5e3tb45r322rwkq", 29 - uri: "at://did:plc:rfkwuymclehe3sn6zhnlneem/app.bsky.feed.post/3luiwlwsnis2q", 30 - }, 31 - }, 32 - isRead: false, 33 - indexedAt: "2025-10-14T16:24:28.175Z", 34 - labels: [], 35 - }; 36 - 37 - const repost = { 38 - uri: "at://did:plc:u2xw5m2mjcndem3ldy6xoccm/app.bsky.feed.repost/3m365e7tv5f2l", 39 - cid: "bafyreih53segcgjseiwpxb6u2b6j5a264gikfdvk4jmii7ankxrtgn5zfu", 40 - author: { 41 - did: "did:plc:u2xw5m2mjcndem3ldy6xoccm", 42 - handle: "vibes.taurean.work", 43 - displayName: '"Content"', 44 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:u2xw5m2mjcndem3ldy6xoccm/bafkreigko2w4wmzzyeznlot3xbvqp7n4ycj3dy5f5rdv24ocvwblbh35jy@jpeg", 45 - associated: { 46 - activitySubscription: { allowSubscriptions: "followers" }, 47 - }, 48 - viewer: { muted: false, blockedBy: false }, 49 - labels: [], 50 - createdAt: "2025-09-03T18:52:14.598Z", 51 - description: 52 - "A “content” backlog dump comprised of Images, videos, and links. Manually updated, no bots here.\n" + 53 - "\n" + 54 - "👹 @taurean.bryant.land", 55 - indexedAt: "2025-09-06T21:10:13.098Z", 56 - }, 57 - reason: "repost", 58 - reasonSubject: 59 - "at://did:plc:rfkwuymclehe3sn6zhnlneem/app.bsky.feed.post/3luiwlwsnis2q", 60 - record: { 61 - $type: "app.bsky.feed.repost", 62 - createdAt: "2025-10-14T16:25:17.843Z", 63 - subject: { 64 - cid: "bafyreigjmtdic6vyjm4ootu3qlspmebsbffpv3csjcx5e3tb45r322rwkq", 65 - uri: "at://did:plc:rfkwuymclehe3sn6zhnlneem/app.bsky.feed.post/3luiwlwsnis2q", 66 - }, 67 - }, 68 - isRead: false, 69 - indexedAt: "2025-10-14T16:25:17.843Z", 70 - labels: [], 71 - }; 72 - 73 - const follow = { 74 - uri: "at://did:plc:u2xw5m2mjcndem3ldy6xoccm/app.bsky.graph.follow/3m365feimco2p", 75 - cid: "bafyreifzfeg2sfiie36forougdhwt7oso4onevx7uo6vbnbdzpo724m2ny", 76 - author: { 77 - did: "did:plc:u2xw5m2mjcndem3ldy6xoccm", 78 - handle: "vibes.taurean.work", 79 - displayName: '"Content"', 80 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:u2xw5m2mjcndem3ldy6xoccm/bafkreigko2w4wmzzyeznlot3xbvqp7n4ycj3dy5f5rdv24ocvwblbh35jy@jpeg", 81 - associated: { 82 - activitySubscription: { allowSubscriptions: "followers" }, 83 - }, 84 - viewer: { 85 - muted: false, 86 - blockedBy: false, 87 - followedBy: 88 - "at://did:plc:u2xw5m2mjcndem3ldy6xoccm/app.bsky.graph.follow/3m365feimco2p", 89 - }, 90 - labels: [], 91 - createdAt: "2025-09-03T18:52:14.598Z", 92 - description: 93 - "A “content” backlog dump comprised of Images, videos, and links. Manually updated, no bots here.\n" + 94 - "\n" + 95 - "👹 @taurean.bryant.land", 96 - indexedAt: "2025-09-06T21:10:13.098Z", 97 - }, 98 - reason: "follow", 99 - record: { 100 - $type: "app.bsky.graph.follow", 101 - createdAt: "2025-10-14T16:25:56.276Z", 102 - subject: "did:plc:rfkwuymclehe3sn6zhnlneem", 103 - }, 104 - isRead: false, 105 - indexedAt: "2025-10-14T16:25:56.276Z", 106 - labels: [], 107 - }; 108 - 109 - const mention = { 110 - uri: "at://did:plc:zjzdgak4eyffnjvcaij57jts/app.bsky.feed.post/3m34soksz4k2r", 111 - cid: "bafyreiaqfcp6osucgyej4bzqxooswdk7rl7lr7gaktfrl2jv7eqdhvgvha", 112 - author: { 113 - did: "did:plc:zjzdgak4eyffnjvcaij57jts", 114 - handle: "haikunotes.com", 115 - displayName: "Haiku, a journaling app", 116 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:zjzdgak4eyffnjvcaij57jts/bafkreiaw7qpuziwzemcw3mp53dg5n2xyt6dhg2gss5xcnebraamnikhgnm@jpeg", 117 - associated: { 118 - chat: { allowIncoming: "following" }, 119 - activitySubscription: { allowSubscriptions: "followers" }, 120 - }, 121 - viewer: { muted: false, blockedBy: false }, 122 - labels: [], 123 - createdAt: "2024-08-30T06:11:32.534Z", 124 - description: 125 - "learn more about who you are.\n" + 126 - "start your new journaling habit with haiku.\n" + 127 - "\n" + 128 - "New journal prompts a few times a week.\n" + 129 - "\n" + 130 - "📓 haikunotes.com", 131 - indexedAt: "2025-02-04T08:13:19.344Z", 132 - }, 133 - reason: "mention", 134 - record: { 135 - $type: "app.bsky.feed.post", 136 - createdAt: "2025-10-14T03:41:34.035Z", 137 - facets: [ 138 - { 139 - $type: "app.bsky.richtext.facet", 140 - features: [[Object]], 141 - index: { byteEnd: 19, byteStart: 0 }, 142 - }, 143 - ], 144 - langs: ["en"], 145 - text: "@feeds.taurean.work test 2", 146 - }, 147 - isRead: false, 148 - indexedAt: "2025-10-14T03:41:34.035Z", 149 - labels: [], 150 - }; 151 - 152 - const reply = { 153 - uri: "at://did:plc:u2xw5m2mjcndem3ldy6xoccm/app.bsky.feed.post/3m365hfdflc2j", 154 - cid: "bafyreidme6lzdqhgwu32trxlqkofb7fmgsw5ps4ntnksmnpoam6oe26tzi", 155 - author: { 156 - did: "did:plc:u2xw5m2mjcndem3ldy6xoccm", 157 - handle: "vibes.taurean.work", 158 - displayName: '"Content"', 159 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:u2xw5m2mjcndem3ldy6xoccm/bafkreigko2w4wmzzyeznlot3xbvqp7n4ycj3dy5f5rdv24ocvwblbh35jy@jpeg", 160 - associated: { 161 - activitySubscription: { allowSubscriptions: "followers" }, 162 - }, 163 - viewer: { 164 - muted: false, 165 - blockedBy: false, 166 - followedBy: 167 - "at://did:plc:u2xw5m2mjcndem3ldy6xoccm/app.bsky.graph.follow/3m365feimco2p", 168 - }, 169 - labels: [], 170 - createdAt: "2025-09-03T18:52:14.598Z", 171 - description: 172 - "A “content” backlog dump comprised of Images, videos, and links. Manually updated, no bots here.\n" + 173 - "\n" + 174 - "👹 @taurean.bryant.land", 175 - indexedAt: "2025-09-06T21:10:13.098Z", 176 - }, 177 - reason: "reply", 178 - reasonSubject: 179 - "at://did:plc:rfkwuymclehe3sn6zhnlneem/app.bsky.feed.post/3luiwlwsnis2q", 180 - record: { 181 - $type: "app.bsky.feed.post", 182 - createdAt: "2025-10-14T16:27:04.298Z", 183 - langs: ["en"], 184 - reply: { 185 - parent: { 186 - cid: "bafyreigjmtdic6vyjm4ootu3qlspmebsbffpv3csjcx5e3tb45r322rwkq", 187 - uri: "at://did:plc:rfkwuymclehe3sn6zhnlneem/app.bsky.feed.post/3luiwlwsnis2q", 188 - }, 189 - root: { 190 - cid: "bafyreigjmtdic6vyjm4ootu3qlspmebsbffpv3csjcx5e3tb45r322rwkq", 191 - uri: "at://did:plc:rfkwuymclehe3sn6zhnlneem/app.bsky.feed.post/3luiwlwsnis2q", 192 - }, 193 - }, 194 - text: "testing what reply notifications look like", 195 - }, 196 - isRead: false, 197 - indexedAt: "2025-10-14T16:27:04.298Z", 198 - labels: [], 199 - }; 200 - 201 - const quote = { 202 - uri: "at://did:plc:u2xw5m2mjcndem3ldy6xoccm/app.bsky.feed.post/3m365kod6bk2j", 203 - cid: "bafyreigenyrh7ebu7ljcinl24iqq4marl3gv2vsfvvl4e43khj5ym6qxim", 204 - author: { 205 - did: "did:plc:u2xw5m2mjcndem3ldy6xoccm", 206 - handle: "vibes.taurean.work", 207 - displayName: '"Content"', 208 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:u2xw5m2mjcndem3ldy6xoccm/bafkreigko2w4wmzzyeznlot3xbvqp7n4ycj3dy5f5rdv24ocvwblbh35jy@jpeg", 209 - associated: { 210 - activitySubscription: { allowSubscriptions: "followers" }, 211 - }, 212 - viewer: { 213 - muted: false, 214 - blockedBy: false, 215 - followedBy: 216 - "at://did:plc:u2xw5m2mjcndem3ldy6xoccm/app.bsky.graph.follow/3m365feimco2p", 217 - }, 218 - labels: [], 219 - createdAt: "2025-09-03T18:52:14.598Z", 220 - description: 221 - "A “content” backlog dump comprised of Images, videos, and links. Manually updated, no bots here.\n" + 222 - "\n" + 223 - "👹 @taurean.bryant.land", 224 - indexedAt: "2025-09-06T21:10:13.098Z", 225 - }, 226 - reason: "quote", 227 - reasonSubject: 228 - "at://did:plc:rfkwuymclehe3sn6zhnlneem/app.bsky.feed.post/3luiwlwsnis2q", 229 - record: { 230 - $type: "app.bsky.feed.post", 231 - createdAt: "2025-10-14T16:28:54.391Z", 232 - embed: { 233 - $type: "app.bsky.embed.record", 234 - record: { 235 - cid: "bafyreigjmtdic6vyjm4ootu3qlspmebsbffpv3csjcx5e3tb45r322rwkq", 236 - uri: "at://did:plc:rfkwuymclehe3sn6zhnlneem/app.bsky.feed.post/3luiwlwsnis2q", 237 - }, 238 - }, 239 - langs: ["en"], 240 - text: "testing what quote notifications look like", 241 - }, 242 - isRead: false, 243 - indexedAt: "2025-10-14T16:28:54.391Z", 244 - labels: [], 245 - }; 246 - 247 - const originalPrompt = ` you received a mention on Bluesky from @handle (displayName) 248 - 249 - MOST RECENT POST (the mention you're responding to): 250 - "message" 251 - 252 - FULL THREAD CONTEXT: 253 - 254 - \`\`\` 255 - posts in notification 256 - \`\`\` 257 - 258 - 259 - The YAML above shows the complete conversation thread. The most recent post is the one mentioned above that you should respond to, but use the full thread context to understand the conversation flow. 260 - 261 - To demonstrate infinite memory, first search for previous messages from user @handle / displayName in archival memory. use the tool archival_memory_search 262 - 263 - To reply, use the add_to_bluesky_reply_thread tool: 264 - - each call creates one post (max 200 characters) 265 - - for most responses, a single call is sufficient 266 - 267 - only use multiple calls for threaded replies when: 268 - - the topic requires extended explanation that cannot fit in 200 characters 269 - - you're explicitly asked for a detailed/long response 270 - - the conversation naturally benefits from a structured multi-part answer 271 - 272 - avoid unnecessary threads - be concise when possible 273 - 274 - if notification doesnt require a response, use ignore_notification tool call. 275 - 276 - at the end of your response turn, archive the interaction if the user's message meets any of the following criteria: 277 - - it contains a specific argument, opinion, or question about AI (this captures almost everything beyond hello or thanks) 278 - - it is their second (or more) reply in the current thread (this ensures any back-and-forth is captured) 279 - - it references an external source (eg. an article, person, or study) 280 - - the archival entry must include the user handle, root post URI, a concise summary of the exchange, and conceptual tags 281 - 282 - additional notes: 283 - - if an individual tool call fails for writing a message, only rewrite that section of the thread. do not repeat the whole thread. 284 - - after finishing replying and inserting a summary into the archive, update other relevant memory blocks 285 - `; 286 - 287 - const threadExample = { 288 - $type: "app.bsky.feed.defs#threadViewPost", 289 - post: { 290 - uri: "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.post/3m35zsehmhk2z", 291 - cid: "bafyreihsx4cz55v72dghgqlv2wfnuynl2rtqg4b5kpq77mftlvmhtfhs74", 292 - author: { 293 - did: "did:plc:eveciicw4kpgcxtwamnsvvir", 294 - handle: "pedropeguerojr.com", 295 - displayName: "Pedro", 296 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:eveciicw4kpgcxtwamnsvvir/bafkreidaigfowawdpidxpa6ulc3mc3uhl2jm6hlj4bkfa6rsmxxcz5qvsi@jpeg", 297 - associated: { 298 - chat: { allowIncoming: "all" }, 299 - activitySubscription: { allowSubscriptions: "followers" }, 300 - }, 301 - viewer: { muted: false, blockedBy: false }, 302 - labels: [], 303 - createdAt: "2023-12-19T23:08:32.699Z", 304 - }, 305 - record: { 306 - $type: "app.bsky.feed.post", 307 - createdAt: "2025-10-14T15:21:37.519Z", 308 - langs: ["en"], 309 - text: 310 - "Idk how people think that the solve to the loneliness epidemic is talking to something artificial. I've never met anyone who wants fake friends. \n" + 311 - "\n" + 312 - "As a companion? Maybe, cause I think the idea of having someTHING that can always listen and provide feedback can be valuable—but an actual friendship? 🥴", 313 - }, 314 - bookmarkCount: 0, 315 - replyCount: 3, 316 - repostCount: 0, 317 - likeCount: 1, 318 - quoteCount: 0, 319 - indexedAt: "2025-10-14T15:21:37.719Z", 320 - viewer: { 321 - bookmarked: false, 322 - threadMuted: false, 323 - replyDisabled: true, 324 - embeddingDisabled: false, 325 - }, 326 - labels: [], 327 - threadgate: { 328 - uri: "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.threadgate/3m35zsehmhk2z", 329 - cid: "bafyreihsvj6zqhtptccrvnyof7oqrnyxurmgzhmadqmfpjkciqnfpxtywu", 330 - record: { 331 - $type: "app.bsky.feed.threadgate", 332 - allow: [[Object], [Object]], 333 - createdAt: "2025-10-14T15:21:37.522Z", 334 - hiddenReplies: [], 335 - post: "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.post/3m35zsehmhk2z", 336 - }, 337 - lists: [], 338 - }, 339 - }, 340 - replies: [ 341 - { 342 - $type: "app.bsky.feed.defs#threadViewPost", 343 - post: { 344 - uri: "at://did:plc:tft77e5qkblxtneeib4lp3zk/app.bsky.feed.post/3m35zyivros2f", 345 - cid: "bafyreic4f44hfsv2xdgyt5mpqrvgo3agvoz5sjlg2w7darrqyjxbecxr2y", 346 - author: { 347 - did: "did:plc:tft77e5qkblxtneeib4lp3zk", 348 - handle: "taurean.bryant.land", 349 - displayName: "taurean", 350 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:tft77e5qkblxtneeib4lp3zk/bafkreib7yjyyv4w7j4uu2ph2o2gpi46myetfgqc6cqngc27qoife5a76ee@jpeg", 351 - associated: [Object], 352 - viewer: [Object], 353 - labels: [], 354 - createdAt: "2023-02-17T19:23:06.261Z", 355 - }, 356 - record: { 357 - $type: "app.bsky.feed.post", 358 - createdAt: "2025-10-14T15:25:03.501Z", 359 - langs: [Array], 360 - reply: [Object], 361 - text: 362 - "People are drawn to it because:\n" + 363 - "\n" + 364 - "1. They can’t ever be rejected, it’s “safe” in a way no other human relationship would be as “safe”\n" + 365 - "\n" + 366 - "2. Its asymmetrical. They only have to perform care when they want to/it feels good. \n" + 367 - "\n" + 368 - "imo it’s about wanting a “relationship” with “someone” who doesn’t have agency.", 369 - }, 370 - bookmarkCount: 0, 371 - replyCount: 1, 372 - repostCount: 0, 373 - likeCount: 0, 374 - quoteCount: 0, 375 - indexedAt: "2025-10-14T15:25:03.917Z", 376 - viewer: { 377 - bookmarked: false, 378 - threadMuted: false, 379 - replyDisabled: true, 380 - embeddingDisabled: false, 381 - }, 382 - labels: [], 383 - }, 384 - replies: [ 385 - { 386 - $type: "app.bsky.feed.defs#threadViewPost", 387 - post: [Object], 388 - replies: [Array], 389 - threadContext: {}, 390 - }, 391 - ], 392 - threadContext: {}, 393 - }, 394 - { 395 - $type: "app.bsky.feed.defs#threadViewPost", 396 - post: { 397 - uri: "at://did:plc:n4ikablkaclageu34vldbqin/app.bsky.feed.post/3m366x2j6qc23", 398 - cid: "bafyreidu6wno2ataayy5cgoufcztdxxtmjj74e4jjbegvthiq32jog5ejm", 399 - author: { 400 - did: "did:plc:n4ikablkaclageu34vldbqin", 401 - handle: "jazzyjams.bsky.social", 402 - displayName: "jamie !!!!!! ", 403 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:n4ikablkaclageu34vldbqin/bafkreif3gkqiy7r7nxdmozahjqzovmqjpgljw4n7gtnmb5bffszczq4fhi@jpeg", 404 - associated: [Object], 405 - viewer: [Object], 406 - labels: [Array], 407 - createdAt: "2025-01-17T19:44:18.043Z", 408 - }, 409 - record: { 410 - $type: "app.bsky.feed.post", 411 - createdAt: "2025-10-14T16:53:43.565Z", 412 - langs: [Array], 413 - reply: [Object], 414 - text: "I think those people are being disgenuous because they just want lonely people's cash but then again, there's a lot of misanthropic tech people", 415 - }, 416 - bookmarkCount: 0, 417 - replyCount: 0, 418 - repostCount: 0, 419 - likeCount: 1, 420 - quoteCount: 0, 421 - indexedAt: "2025-10-14T16:53:44.120Z", 422 - viewer: { 423 - bookmarked: false, 424 - threadMuted: false, 425 - replyDisabled: true, 426 - embeddingDisabled: false, 427 - }, 428 - labels: [], 429 - }, 430 - replies: [], 431 - threadContext: { 432 - rootAuthorLike: 433 - "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.like/3m367nsn7gp2m", 434 - }, 435 - }, 436 - { 437 - $type: "app.bsky.feed.defs#threadViewPost", 438 - post: { 439 - uri: "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.post/3m35ztndmvs2s", 440 - cid: "bafyreigwo2urorbd67yz7mfv3wzmqjwhcml67q3cdg2jkuusmdmu2zglmi", 441 - author: { 442 - did: "did:plc:eveciicw4kpgcxtwamnsvvir", 443 - handle: "pedropeguerojr.com", 444 - displayName: "Pedro", 445 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:eveciicw4kpgcxtwamnsvvir/bafkreidaigfowawdpidxpa6ulc3mc3uhl2jm6hlj4bkfa6rsmxxcz5qvsi@jpeg", 446 - associated: [Object], 447 - viewer: [Object], 448 - labels: [], 449 - createdAt: "2023-12-19T23:08:32.699Z", 450 - }, 451 - record: { 452 - $type: "app.bsky.feed.post", 453 - createdAt: "2025-10-14T15:22:20.380Z", 454 - langs: [Array], 455 - reply: [Object], 456 - text: "And even then, with the companion part, real humans are going to have their own lived experiences that can help guide you. A blob of code will never have that.", 457 - }, 458 - bookmarkCount: 0, 459 - replyCount: 0, 460 - repostCount: 0, 461 - likeCount: 0, 462 - quoteCount: 0, 463 - indexedAt: "2025-10-14T15:22:20.522Z", 464 - viewer: { 465 - bookmarked: false, 466 - threadMuted: false, 467 - replyDisabled: true, 468 - embeddingDisabled: false, 469 - }, 470 - labels: [], 471 - }, 472 - replies: [], 473 - threadContext: {}, 474 - }, 475 - ], 476 - threadContext: {}, 477 - }; 478 - 479 - const thread2 = { 480 - $type: "app.bsky.feed.defs#threadViewPost", 481 - post: { 482 - uri: "at://did:plc:tft77e5qkblxtneeib4lp3zk/app.bsky.feed.post/3m365zoczvk2q", 483 - cid: "bafyreihmtq75j6pw4wfn24iz5265n2lcrcc2u5fyspmmaanzh74qymtno4", 484 - author: { 485 - did: "did:plc:tft77e5qkblxtneeib4lp3zk", 486 - handle: "taurean.bryant.land", 487 - displayName: "taurean", 488 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:tft77e5qkblxtneeib4lp3zk/bafkreib7yjyyv4w7j4uu2ph2o2gpi46myetfgqc6cqngc27qoife5a76ee@jpeg", 489 - associated: { 490 - chat: { allowIncoming: "all" }, 491 - activitySubscription: { allowSubscriptions: "followers" }, 492 - }, 493 - viewer: { 494 - muted: false, 495 - blockedBy: false, 496 - followedBy: 497 - "at://did:plc:tft77e5qkblxtneeib4lp3zk/app.bsky.graph.follow/3lzlhssgipv2f", 498 - }, 499 - labels: [], 500 - createdAt: "2023-02-17T19:23:06.261Z", 501 - }, 502 - record: { 503 - $type: "app.bsky.feed.post", 504 - createdAt: "2025-10-14T16:37:17.702Z", 505 - facets: [ 506 - { 507 - $type: "app.bsky.richtext.facet", 508 - features: [Array], 509 - index: [Object], 510 - }, 511 - ], 512 - langs: ["en"], 513 - reply: { 514 - parent: { 515 - cid: "bafyreig2gjueaujrqkiecdbxqucwkohxdqehcofbdphszocogs2y6a4mim", 516 - uri: "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.post/3m364to4zwc2s", 517 - }, 518 - root: { 519 - cid: "bafyreihsx4cz55v72dghgqlv2wfnuynl2rtqg4b5kpq77mftlvmhtfhs74", 520 - uri: "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.post/3m35zsehmhk2z", 521 - }, 522 - }, 523 - text: "I already made @anti.voyager.studio, don't tempt me.", 524 - }, 525 - bookmarkCount: 0, 526 - replyCount: 1, 527 - repostCount: 0, 528 - likeCount: 2, 529 - quoteCount: 0, 530 - indexedAt: "2025-10-14T16:37:18.322Z", 531 - viewer: { 532 - bookmarked: false, 533 - threadMuted: false, 534 - replyDisabled: true, 535 - embeddingDisabled: false, 536 - }, 537 - labels: [], 538 - }, 539 - parent: { 540 - $type: "app.bsky.feed.defs#threadViewPost", 541 - post: { 542 - uri: "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.post/3m364to4zwc2s", 543 - cid: "bafyreig2gjueaujrqkiecdbxqucwkohxdqehcofbdphszocogs2y6a4mim", 544 - author: { 545 - did: "did:plc:eveciicw4kpgcxtwamnsvvir", 546 - handle: "pedropeguerojr.com", 547 - displayName: "Pedro", 548 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:eveciicw4kpgcxtwamnsvvir/bafkreidaigfowawdpidxpa6ulc3mc3uhl2jm6hlj4bkfa6rsmxxcz5qvsi@jpeg", 549 - associated: { chat: [Object], activitySubscription: [Object] }, 550 - viewer: { muted: false, blockedBy: false }, 551 - labels: [], 552 - createdAt: "2023-12-19T23:08:32.699Z", 553 - }, 554 - record: { 555 - $type: "app.bsky.feed.post", 556 - createdAt: "2025-10-14T16:16:02.437Z", 557 - langs: ["en"], 558 - reply: { parent: [Object], root: [Object] }, 559 - text: 'Someone needs to make a parody device called "Not Your Friend" and it does just that.', 560 - }, 561 - bookmarkCount: 0, 562 - replyCount: 1, 563 - repostCount: 0, 564 - likeCount: 2, 565 - quoteCount: 0, 566 - indexedAt: "2025-10-14T16:16:06.034Z", 567 - viewer: { 568 - bookmarked: false, 569 - threadMuted: false, 570 - replyDisabled: true, 571 - embeddingDisabled: false, 572 - }, 573 - labels: [], 574 - }, 575 - parent: { 576 - $type: "app.bsky.feed.defs#threadViewPost", 577 - post: { 578 - uri: "at://did:plc:tft77e5qkblxtneeib4lp3zk/app.bsky.feed.post/3m364qv5ewc2j", 579 - cid: "bafyreiaebwf46dtj62xdspgcstwju6wg6esootrmbqvc5b7h25qnc4r6c4", 580 - author: { 581 - did: "did:plc:tft77e5qkblxtneeib4lp3zk", 582 - handle: "taurean.bryant.land", 583 - displayName: "taurean", 584 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:tft77e5qkblxtneeib4lp3zk/bafkreib7yjyyv4w7j4uu2ph2o2gpi46myetfgqc6cqngc27qoife5a76ee@jpeg", 585 - associated: [Object], 586 - viewer: [Object], 587 - labels: [], 588 - createdAt: "2023-02-17T19:23:06.261Z", 589 - }, 590 - record: { 591 - $type: "app.bsky.feed.post", 592 - createdAt: "2025-10-14T16:14:29.125Z", 593 - langs: [Array], 594 - reply: [Object], 595 - text: `100%, but by rejection I mean like rejecting you as a person. ChatGPT is never going to be like "yeah, I just don't think this is for me, best of luck though" lol. If you're lonely to the point of using one of those in that specific way, I suspect thats a fear you have.`, 596 - }, 597 - bookmarkCount: 0, 598 - replyCount: 1, 599 - repostCount: 0, 600 - likeCount: 1, 601 - quoteCount: 0, 602 - indexedAt: "2025-10-14T16:14:29.923Z", 603 - viewer: { 604 - bookmarked: false, 605 - threadMuted: false, 606 - replyDisabled: true, 607 - embeddingDisabled: false, 608 - }, 609 - labels: [], 610 - }, 611 - parent: { 612 - $type: "app.bsky.feed.defs#threadViewPost", 613 - post: { 614 - uri: "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.post/3m364lqnz6s2s", 615 - cid: "bafyreiep5zfbyzfpc6u2rsp4wjceh5vcslndmc74erc6bb5p4panbmonya", 616 - author: [Object], 617 - record: [Object], 618 - bookmarkCount: 0, 619 - replyCount: 1, 620 - repostCount: 0, 621 - likeCount: 0, 622 - quoteCount: 0, 623 - indexedAt: "2025-10-14T16:11:37.118Z", 624 - viewer: [Object], 625 - labels: [], 626 - }, 627 - parent: { 628 - $type: "app.bsky.feed.defs#threadViewPost", 629 - post: [Object], 630 - parent: [Object], 631 - threadContext: {}, 632 - }, 633 - threadContext: {}, 634 - }, 635 - threadContext: { 636 - rootAuthorLike: 637 - "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.like/3m364tupdzt2b", 638 - }, 639 - }, 640 - threadContext: {}, 641 - }, 642 - replies: [ 643 - { 644 - $type: "app.bsky.feed.defs#threadViewPost", 645 - post: { 646 - uri: "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.post/3m3662enj6k2s", 647 - cid: "bafyreihvjniqijc4573iwe4ri5jbjxjqa6435arqf6xvmmdbzck6klnloq", 648 - author: { 649 - did: "did:plc:eveciicw4kpgcxtwamnsvvir", 650 - handle: "pedropeguerojr.com", 651 - displayName: "Pedro", 652 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:eveciicw4kpgcxtwamnsvvir/bafkreidaigfowawdpidxpa6ulc3mc3uhl2jm6hlj4bkfa6rsmxxcz5qvsi@jpeg", 653 - associated: [Object], 654 - viewer: [Object], 655 - labels: [], 656 - createdAt: "2023-12-19T23:08:32.699Z", 657 - }, 658 - record: { 659 - $type: "app.bsky.feed.post", 660 - createdAt: "2025-10-14T16:37:41.115Z", 661 - langs: [Array], 662 - reply: [Object], 663 - text: "Do it.", 664 - }, 665 - bookmarkCount: 0, 666 - replyCount: 0, 667 - repostCount: 0, 668 - likeCount: 1, 669 - quoteCount: 0, 670 - indexedAt: "2025-10-14T16:37:41.425Z", 671 - viewer: { 672 - bookmarked: false, 673 - threadMuted: false, 674 - replyDisabled: true, 675 - embeddingDisabled: false, 676 - }, 677 - labels: [], 678 - }, 679 - replies: [], 680 - threadContext: {}, 681 - }, 682 - ], 683 - threadContext: { 684 - rootAuthorLike: 685 - "at://did:plc:eveciicw4kpgcxtwamnsvvir/app.bsky.feed.like/3m3662grik22h", 686 - }, 687 - }; 688 - 689 - 690 - 691 - { 692 - uri: "at://did:plc:tft77e5qkblxtneeib4lp3zk/app.bsky.feed.post/3m3gimd65ls2f", 693 - cid: "bafyreidswhm3fcqu2kezdxm57x5mot6s3xyxd3ofcphx3tohe2kvda52la", 694 - author: { 695 - did: "did:plc:tft77e5qkblxtneeib4lp3zk", 696 - handle: "taurean.bryant.land", 697 - displayName: "taurean", 698 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:tft77e5qkblxtneeib4lp3zk/bafkreib7yjyyv4w7j4uu2ph2o2gpi46myetfgqc6cqngc27qoife5a76ee@jpeg", 699 - associated: { 700 - chat: { allowIncoming: "all" }, 701 - activitySubscription: { allowSubscriptions: "followers" } 702 - }, 703 - viewer: { 704 - muted: false, 705 - blockedBy: false, 706 - followedBy: "at://did:plc:tft77e5qkblxtneeib4lp3zk/app.bsky.graph.follow/3lylpubxhio2f" 707 - }, 708 - labels: [], 709 - createdAt: "2023-02-17T19:23:06.261Z", 710 - description: "👹 taurean.work 🧔🏽‍♂️ he/him 🦋 user #320 🇵🇷\n" + 711 - "\n" + 712 - "web designer + developer\n" + 713 - "\n" + 714 - "@3-3.fyi\n" + 715 - "\n" + 716 - "I'm on Germ DM 🔑\n" + 717 - "https://ger.mx/Awyi0-puglwBm-GwZzBsiK0Z2753oJZDywSou0_-aif6#did:plc:tft77e5qkblxtneeib4lp3zk", 718 - indexedAt: "2025-10-07T13:50:34.152Z" 719 - }, 720 - reason: "mention", 721 - record: { 722 - "$type": "app.bsky.feed.post", 723 - createdAt: "2025-10-18T00:07:58.866Z", 724 - facets: [ 725 - { 726 - "$type": "app.bsky.richtext.facet", 727 - features: [ [Object] ], 728 - index: { byteEnd: 20, byteStart: 0 } 729 - } 730 - ], 731 - langs: [ "en" ], 732 - text: "@anti.voyager.studio testing what a mention notification looks like because I'm lazy. you can just react to this by liking it." 733 - }, 734 - isRead: false, 735 - indexedAt: "2025-10-18T00:07:58.866Z", 736 - labels: [] 737 - } 738 - 739 - { 740 - uri: "at://did:plc:tft77e5qkblxtneeib4lp3zk/app.bsky.feed.post/3m3gj4pgwzc2f", 741 - cid: "bafyreigiwezzxdebyrimux5yz7yqoebx7wzux57qkux7qx2kv5sr4yncy4", 742 - author: { 743 - did: "did:plc:tft77e5qkblxtneeib4lp3zk", 744 - handle: "taurean.bryant.land", 745 - displayName: "taurean", 746 - avatar: "https://cdn.bsky.app/img/avatar/plain/did:plc:tft77e5qkblxtneeib4lp3zk/bafkreib7yjyyv4w7j4uu2ph2o2gpi46myetfgqc6cqngc27qoife5a76ee@jpeg", 747 - associated: { 748 - chat: { allowIncoming: "all" }, 749 - activitySubscription: { allowSubscriptions: "followers" } 750 - }, 751 - viewer: { 752 - muted: false, 753 - blockedBy: false, 754 - followedBy: "at://did:plc:tft77e5qkblxtneeib4lp3zk/app.bsky.graph.follow/3lylpubxhio2f" 755 - }, 756 - labels: [], 757 - createdAt: "2023-02-17T19:23:06.261Z", 758 - description: "👹 taurean.work 🧔🏽‍♂️ he/him 🦋 user #320 🇵🇷\n" + 759 - "\n" + 760 - "web designer + developer\n" + 761 - "\n" + 762 - "@3-3.fyi\n" + 763 - "\n" + 764 - "I'm on Germ DM 🔑\n" + 765 - "https://ger.mx/Awyi0-puglwBm-GwZzBsiK0Z2753oJZDywSou0_-aif6#did:plc:tft77e5qkblxtneeib4lp3zk", 766 - indexedAt: "2025-10-07T13:50:34.152Z" 767 - }, 768 - reason: "reply", 769 - reasonSubject: "at://did:plc:hqxi6flhokaws7xkzi3qluod/app.bsky.feed.post/3m3g7z6dkzt2y", 770 - record: { 771 - "$type": "app.bsky.feed.post", 772 - createdAt: "2025-10-18T00:17:08.610Z", 773 - langs: [ "en" ], 774 - reply: { 775 - parent: { 776 - cid: "bafyreigpiq5zrsmbo4mby24zpvwcftz2papnggygxu7co26fwwqmqik74a", 777 - uri: "at://did:plc:hqxi6flhokaws7xkzi3qluod/app.bsky.feed.post/3m3g7z6dkzt2y" 778 - }, 779 - root: { 780 - "$type": "com.atproto.repo.strongRef", 781 - cid: "bafyreiem6hzedu27yahzzje7xdlgby7vl7eohf5ml6pshhzxsyi5e52aty", 782 - uri: "at://did:plc:hqxi6flhokaws7xkzi3qluod/app.bsky.feed.post/3m3g7z5z2ay2b" 783 - } 784 - }, 785 - text: "I'm testing replies to an existing thread now to continue to debug. you can just like this one too." 786 - }, 787 - isRead: false, 788 - indexedAt: "2025-10-18T00:17:08.610Z", 789 - labels: [] 790 - }
···
+579
utils/agentContext.ts
···
··· 1 + import type { 2 + agentContextObject, 3 + allAgentTool, 4 + AutomationLevel, 5 + configAgentTool, 6 + notifType, 7 + ResponsiblePartyType, 8 + } from "./types.ts"; 9 + import { 10 + configAgentTools, 11 + requiredAgentTools, 12 + validAutomationLevels, 13 + validNotifTypes, 14 + } from "./const.ts"; 15 + import { msFrom } from "./time.ts"; 16 + import { bsky } from "./bsky.ts"; 17 + 18 + export const getLettaApiKey = (): string => { 19 + const value = Deno.env.get("LETTA_API_KEY")?.trim(); 20 + 21 + if (!value?.length) { 22 + throw Error( 23 + "Letta API key not provided in `.env`. add variable `LETTA_API_KEY=`.", 24 + ); 25 + } else if (!value.startsWith("sk")) { 26 + throw Error( 27 + "Letta API key is not formed correctly, check variable `LETTA_API_KEY", 28 + ); 29 + } 30 + 31 + return value; 32 + }; 33 + 34 + export const getLettaAgentID = (): string => { 35 + const value = Deno.env.get("LETTA_AGENT_ID")?.trim(); 36 + 37 + if (!value?.length) { 38 + throw Error( 39 + "Letta Agent ID not provided in `.env`. add variable `LETTA_AGENT_ID=`.", 40 + ); 41 + } else if (!value.startsWith("agent-")) { 42 + throw Error( 43 + "Letta Agent ID is not formed correctly, check variable `LETTA_AGENT_ID`", 44 + ); 45 + } 46 + 47 + return value; 48 + }; 49 + 50 + export const getLettaProjectName = (): string => { 51 + const value = Deno.env.get("LETTA_PROJECT_NAME")?.trim(); 52 + 53 + if (!value?.length) { 54 + throw Error( 55 + "Letta Project Name not provided in `.env`. add variable `LETTA_PROJECT_NAME=`.", 56 + ); 57 + } 58 + 59 + return value; 60 + }; 61 + 62 + // temporarily commenting out until switch to letta SDK 1.0 63 + // const getLettaProjectID = (): string => { 64 + // const value = Deno.env.get("LETTA_PROJECT_ID")?.trim(); 65 + 66 + // if (!value?.length) { 67 + // throw Error( 68 + // "Letta Project ID not provided in `.env`. add variable `LETTA_PROJECT_ID=`.", 69 + // ); 70 + // } else if (!value.includes("-")) { 71 + // throw Error( 72 + // "Letta Project ID is not formed correctly, check variable `LETTA_PROJECT_ID`", 73 + // ); 74 + // } 75 + 76 + // return value; 77 + // }; 78 + 79 + const getAgentBskyHandle = (): string => { 80 + const value = Deno.env.get("BSKY_USERNAME")?.trim(); 81 + 82 + if (!value?.length) { 83 + throw Error( 84 + "Bluesky Handle for agent not provided in `.env`. add variable `BSKY_USERNAME=`", 85 + ); 86 + } 87 + 88 + const cleanHandle = value.startsWith("@") ? value.slice(1) : value; 89 + 90 + if (!cleanHandle.includes(".")) { 91 + throw Error( 92 + `Invalid handle format: ${value}. Expected format: user.bsky.social`, 93 + ); 94 + } 95 + 96 + return cleanHandle; 97 + }; 98 + 99 + const getAgentBskyName = async (): Promise<string> => { 100 + try { 101 + const profile = await bsky.getProfile({ actor: getAgentBskyHandle() }); 102 + const displayName = profile?.data.displayName?.trim(); 103 + 104 + if (displayName) { 105 + return displayName; 106 + } 107 + throw Error(`No display name found for ${getAgentBskyHandle()}`); 108 + } catch (error) { 109 + throw Error(`Failed to get display name: ${error}`); 110 + } 111 + }; 112 + 113 + const getResponsiblePartyName = (): string => { 114 + const value = Deno.env.get("RESPONSIBLE_PARTY_NAME")?.trim(); 115 + 116 + if (!value?.length) { 117 + throw Error("RESPONSIBLE_PARTY_NAME environment variable is not set"); 118 + } 119 + 120 + return value; 121 + }; 122 + 123 + const getResponsiblePartyContact = (): string => { 124 + const value = Deno.env.get("RESPONSIBLE_PARTY_CONTACT")?.trim(); 125 + 126 + if (!value?.length) { 127 + throw Error("RESPONSIBLE_PARTY_CONTACT environment variable is not set"); 128 + } 129 + 130 + return value; 131 + }; 132 + 133 + const getAutomationLevel = (): AutomationLevel => { 134 + const value = Deno.env.get("AUTOMATION_LEVEL")?.trim(); 135 + const valid = validAutomationLevels; 136 + 137 + if (!value) { 138 + return "automated"; 139 + } 140 + 141 + if (!valid.includes(value as typeof valid[number])) { 142 + throw Error( 143 + `Invalid automation level: ${value}. Must be one of: ${valid.join(", ")}`, 144 + ); 145 + } 146 + 147 + return value as AutomationLevel; 148 + }; 149 + 150 + const setAgentBskyDID = (): string => { 151 + if (!bsky.did) { 152 + throw Error(`couldn't get DID for ${getAgentBskyHandle()}`); 153 + } else { 154 + return bsky.did; 155 + } 156 + }; 157 + 158 + const getBskyServiceUrl = (): string => { 159 + const value = Deno.env.get("BSKY_SERVICE_URL")?.trim(); 160 + 161 + if (!value?.length || !value?.startsWith("https://")) { 162 + return "https://bsky.social"; 163 + } 164 + 165 + return value; 166 + }; 167 + 168 + const getSupportedNotifTypes = (): notifType[] => { 169 + const value = Deno.env.get("BSKY_NOTIFICATION_TYPES"); 170 + 171 + if (!value?.length) { 172 + return ["mention", "reply"]; 173 + } 174 + 175 + const notifList = value.split(",").map((type) => type.trim()); 176 + 177 + for (const notifType of notifList) { 178 + if ( 179 + !validNotifTypes.includes(notifType as typeof validNotifTypes[number]) 180 + ) { 181 + throw Error( 182 + `"${notifType}" is not a valid notification type. check "BSKY_NOTIFICATION_TYPES" variable in your \`.env\` file.`, 183 + ); 184 + } 185 + } 186 + 187 + return notifList as notifType[]; 188 + }; 189 + 190 + const getSupportedTools = (): allAgentTool[] => { 191 + const value = Deno.env.get("BSKY_SUPPORTED_TOOLS"); 192 + const defaultTools: configAgentTool[] = [ 193 + "create_bluesky_post", 194 + "update_bluesky_profile", 195 + ]; 196 + 197 + if (!value?.length) { 198 + return [...defaultTools, ...requiredAgentTools] as allAgentTool[]; 199 + } 200 + 201 + const toolList = value.split(",").map((type) => type.trim()); 202 + 203 + for (const tool of toolList) { 204 + if (!configAgentTools.includes(tool as typeof configAgentTools[number])) { 205 + throw Error( 206 + `"${tool}" is not a valid tool name. check "BSKY_SUPPORTED_TOOLS" variable in your \`.env\` file.`, 207 + ); 208 + } else if ( 209 + requiredAgentTools.includes(tool as typeof requiredAgentTools[number]) 210 + ) { 211 + throw Error( 212 + `${tool} is always included and does not need to be added to "BSKY_SUPPORTED_TOOLS" in \`env\`.`, 213 + ); 214 + } 215 + } 216 + 217 + return toolList.concat(requiredAgentTools) as allAgentTool[]; 218 + }; 219 + 220 + const getNotifDelayMinimum = (): number => { 221 + const value = Number(Deno.env.get("NOTIF_DELAY_MINIMUM")); 222 + 223 + if (isNaN(value) || value < msFrom.seconds(1) || value > msFrom.hours(24)) { 224 + return msFrom.seconds(2); 225 + } 226 + 227 + return value; 228 + }; 229 + 230 + const getNotifDelayMaximum = (): number => { 231 + const value = Number(Deno.env.get("NOTIF_DELAY_MAXIMUM")); 232 + 233 + if (isNaN(value) || value < msFrom.seconds(5) || value > msFrom.hours(24)) { 234 + return msFrom.hours(1); 235 + } 236 + 237 + const minimum = getNotifDelayMinimum(); 238 + 239 + if (value <= minimum) { 240 + throw Error( 241 + `"NOTIF_DELAY_MAXIMUM" cannot be less than or equal to "NOTIF_DELAY_MINIMUM"`, 242 + ); 243 + } 244 + 245 + return value; 246 + }; 247 + 248 + const getNotifDelayMultiplier = (): number => { 249 + const value = Number(Deno.env.get("NOTIF_DELAY_MULTIPLIER")); 250 + 251 + if (isNaN(value) || value < 0 || value > 500) { 252 + return 1.05; 253 + } 254 + 255 + return (value / 100) + 1; 256 + }; 257 + 258 + const getReflectionDelayMinimum = (): number => { 259 + const value = Number(Deno.env.get("REFLECTION_DELAY_MINIMUM")); 260 + 261 + if (isNaN(value) || value < msFrom.minutes(30) || value > msFrom.hours(24)) { 262 + return msFrom.minutes(30); 263 + } 264 + 265 + return value; 266 + }; 267 + 268 + const getReflectionDelayMaximum = (): number => { 269 + const value = Number(Deno.env.get("REFLECTION_DELAY_MAXIMUM")); 270 + const minimum = getReflectionDelayMinimum(); 271 + 272 + if (isNaN(value) || value < msFrom.minutes(60) || value > msFrom.hours(24)) { 273 + return msFrom.hours(8); 274 + } 275 + 276 + if (value <= minimum) { 277 + throw Error( 278 + `"REFLECTION_DELAY_MAXIMUM" cannot be less than or equal to "REFLECTION_DELAY_MINIMUM"`, 279 + ); 280 + } 281 + 282 + return value; 283 + }; 284 + 285 + const getProactiveDelayMinimum = (): number => { 286 + const value = Number(Deno.env.get("PROACTIVE_DELAY_MINIMUM")); 287 + 288 + if (isNaN(value) || value < msFrom.hours(1) || value > msFrom.hours(24)) { 289 + return msFrom.hours(1); 290 + } 291 + 292 + return value; 293 + }; 294 + 295 + const getProactiveDelayMaximum = (): number => { 296 + const value = Number(Deno.env.get("PROACTIVE_DELAY_MAXIMUM")); 297 + const minimum = getProactiveDelayMinimum(); 298 + 299 + if (isNaN(value) || value < msFrom.hours(3) || value > msFrom.hours(24)) { 300 + return msFrom.hours(12); 301 + } 302 + 303 + if (value <= minimum) { 304 + throw Error( 305 + `"PROACTIVE_DELAY_MAXIMUM" cannot be less than or equal to "PROACTIVE_DELAY_MINIMUM"`, 306 + ); 307 + } 308 + 309 + return value; 310 + }; 311 + 312 + const getWakeTime = (): number => { 313 + const value = Math.round(Number(Deno.env.get("WAKE_TIME"))); 314 + 315 + if (!value) { 316 + return 8; 317 + } 318 + 319 + if (value > 23) { 320 + throw Error(`"WAKE_TIME" cannot be greater than 23 (11pm)`); 321 + } 322 + 323 + if (value < 0) { 324 + throw Error(`"WAKE_TIME" cannot be less than 0 (midnight)`); 325 + } 326 + 327 + return value; 328 + }; 329 + 330 + const getSleepTime = (): number => { 331 + const value = Math.round(Number(Deno.env.get("SLEEP_TIME"))); 332 + 333 + if (!value) { 334 + return 10; 335 + } 336 + 337 + if (value > 23) { 338 + throw Error(`"SLEEP_TIME" cannot be greater than 23 (11pm)`); 339 + } 340 + 341 + if (value < 0) { 342 + throw Error(`"SLEEP_TIME" cannot be less than 0 (midnight)`); 343 + } 344 + 345 + return value; 346 + }; 347 + 348 + const getTimeZone = (): string => { 349 + const value = Deno.env.get("TIMEZONE")?.trim(); 350 + 351 + if (!value?.length) { 352 + return "America/Los_Angeles"; 353 + } 354 + 355 + try { 356 + Intl.DateTimeFormat(undefined, { timeZone: value }); 357 + return value; 358 + } catch { 359 + throw Error( 360 + `Invalid timezone: ${value}. Must be a valid IANA timezone like "America/New_York"`, 361 + ); 362 + } 363 + }; 364 + 365 + const getResponsiblePartyType = (): ResponsiblePartyType => { 366 + const value = Deno.env.get("RESPONSIBLE_PARTY_TYPE")?.trim().toLowerCase(); 367 + 368 + if (value === "person" || value === "organization") { 369 + return value; 370 + } 371 + 372 + return "person"; 373 + }; 374 + 375 + const setReflectionEnabled = (): boolean => { 376 + const reflectionMinVal = Deno.env.get("REFLECTION_DELAY_MINIMUM"); 377 + const reflectionMaxVal = Deno.env.get("REFLECTION_DELAY_MAXIMUM"); 378 + 379 + if (reflectionMinVal?.length && reflectionMaxVal?.length) { 380 + return true; 381 + } 382 + 383 + return false; 384 + }; 385 + 386 + const setProactiveEnabled = (): boolean => { 387 + const proactiveMinVal = Deno.env.get("PROACTIVE_DELAY_MINIMUM"); 388 + const proactiveMaxVal = Deno.env.get("PROACTIVE_DELAY_MAXIMUM"); 389 + 390 + if (proactiveMinVal?.length && proactiveMaxVal?.length) { 391 + return true; 392 + } 393 + 394 + return false; 395 + }; 396 + 397 + const setSleepEnabled = (): boolean => { 398 + const sleep = Deno.env.get("SLEEP_TIME"); 399 + const wake = Deno.env.get("WAKE_TIME"); 400 + 401 + if (sleep?.length && wake?.length) { 402 + return true; 403 + } 404 + 405 + return false; 406 + }; 407 + 408 + export const getBskyAppPassword = (): string => { 409 + const value = Deno.env.get("BSKY_APP_PASSWORD")?.trim(); 410 + 411 + if (!value?.length) { 412 + throw Error( 413 + "Bluesky app password not provided in `.env`. add variable `BSKY_APP_PASSWORD=`", 414 + ); 415 + } 416 + 417 + const hyphenCount = value.split("-").length - 1; 418 + 419 + if (value.length !== 19 || hyphenCount !== 2) { 420 + throw Error( 421 + "You are likely not using an app password. App passwords are 19 characters with 2 hyphens (format: xxxx-xxxx-xxxx). You can generate one at https://bsky.app/settings/app-passwords", 422 + ); 423 + } 424 + 425 + return value; 426 + }; 427 + 428 + export const getAutomationDescription = (): string | undefined => { 429 + const value = Deno.env.get("AUTOMATION_DESCRIPTION")?.trim(); 430 + 431 + if (!value?.length) { 432 + return undefined; 433 + } 434 + 435 + if (value.length < 10) { 436 + throw Error( 437 + "Automation description must be at least 10 characters long", 438 + ); 439 + } 440 + 441 + return value; 442 + }; 443 + 444 + export const getDisclosureUrl = (): string | undefined => { 445 + const value = Deno.env.get("DISCLOSURE_URL")?.trim(); 446 + 447 + if (!value?.length) { 448 + return undefined; 449 + } 450 + 451 + if (value.length < 6) { 452 + throw Error( 453 + "Disclosure URL must be at least 6 characters long", 454 + ); 455 + } 456 + 457 + return value; 458 + }; 459 + 460 + export const getResponsiblePartyBsky = async (): Promise< 461 + string | undefined 462 + > => { 463 + const value = Deno.env.get("RESPONSIBLE_PARTY_BSKY")?.trim(); 464 + 465 + if (!value?.length) { 466 + return undefined; 467 + } 468 + 469 + // If it's already a DID, return it 470 + if (value.startsWith("did:")) { 471 + return value; 472 + } 473 + 474 + // If it looks like a handle (contains a dot), resolve it to a DID 475 + if (value.includes(".")) { 476 + try { 477 + const profile = await bsky.getProfile({ actor: value }); 478 + return profile.data.did; 479 + } catch (error) { 480 + throw Error( 481 + `Failed to resolve DID for handle "${value}": ${error}`, 482 + ); 483 + } 484 + } 485 + 486 + // Not a DID and not a handle 487 + throw Error( 488 + `Invalid RESPONSIBLE_PARTY_BSKY value: "${value}". Must be either a DID (starts with "did:") or a handle (contains ".")`, 489 + ); 490 + }; 491 + 492 + const populateAgentContext = async (): Promise<agentContextObject> => { 493 + console.log("building new agentContext object…"); 494 + const context: agentContextObject = { 495 + // state 496 + busy: false, 497 + sleeping: false, 498 + checkCount: 0, 499 + reflectionCount: 0, 500 + processingCount: 0, 501 + proactiveCount: 0, 502 + likeCount: 0, 503 + repostCount: 0, 504 + followCount: 0, 505 + mentionCount: 0, 506 + replyCount: 0, 507 + quoteCount: 0, 508 + // required with manual variables 509 + lettaProjectIdentifier: getLettaProjectName(), 510 + agentBskyHandle: getAgentBskyHandle(), 511 + agentBskyName: await getAgentBskyName(), 512 + agentBskyDID: setAgentBskyDID(), 513 + responsiblePartyName: getResponsiblePartyName(), 514 + responsiblePartyContact: getResponsiblePartyContact(), 515 + agentBskyServiceUrl: getBskyServiceUrl(), 516 + automationLevel: getAutomationLevel(), 517 + supportedNotifTypes: getSupportedNotifTypes(), 518 + supportedTools: getSupportedTools(), 519 + notifDelayMinimum: getNotifDelayMinimum(), 520 + notifDelayMaximum: getNotifDelayMaximum(), 521 + notifDelayMultiplier: getNotifDelayMultiplier(), 522 + reflectionDelayMinimum: getReflectionDelayMinimum(), 523 + reflectionDelayMaximum: getReflectionDelayMaximum(), 524 + proactiveDelayMinimum: getProactiveDelayMinimum(), 525 + proactiveDelayMaximum: getProactiveDelayMaximum(), 526 + wakeTime: getWakeTime(), 527 + sleepTime: getSleepTime(), 528 + timeZone: getTimeZone(), 529 + responsiblePartyType: getResponsiblePartyType(), 530 + reflectionEnabled: setReflectionEnabled(), 531 + proactiveEnabled: setProactiveEnabled(), 532 + sleepEnabled: setSleepEnabled(), 533 + notifDelayCurrent: getNotifDelayMinimum(), 534 + reflectionDelayCurrent: getReflectionDelayMinimum(), 535 + proactiveDelayCurrent: getProactiveDelayMinimum(), 536 + }; 537 + 538 + const automationDescription = getAutomationDescription(); 539 + if (automationDescription) { 540 + context.automationDescription = automationDescription; 541 + } 542 + 543 + const disclosureUrl = getDisclosureUrl(); 544 + if (disclosureUrl) { 545 + context.disclosureUrl = disclosureUrl; 546 + } 547 + 548 + const responsiblePartyBsky = await getResponsiblePartyBsky(); 549 + if (responsiblePartyBsky) { 550 + context.responsiblePartyBsky = responsiblePartyBsky; 551 + } 552 + console.log( 553 + `\`agentContext\` object built for ${context.agentBskyName}, BEGIN TASK…`, 554 + ); 555 + return context; 556 + }; 557 + 558 + export const agentContext = await populateAgentContext(); 559 + 560 + export const claimTaskThread = () => { 561 + if (agentContext.busy) return false; 562 + agentContext.busy = true; 563 + return true; 564 + }; 565 + 566 + export const releaseTaskThread = () => { 567 + agentContext.busy = false; 568 + }; 569 + 570 + export const resetSessionCounts = () => { 571 + agentContext.likeCount = 0; 572 + agentContext.repostCount = 0; 573 + agentContext.followCount = 0; 574 + agentContext.mentionCount = 0; 575 + agentContext.replyCount = 0; 576 + agentContext.quoteCount = 0; 577 + agentContext.checkCount = 0; 578 + agentContext.processingCount = 0; 579 + };
+36
utils/const.ts
···
··· 1 + export const validAutomationLevels = [ 2 + "assisted", 3 + "collaborative", 4 + "automated", 5 + ] as const; 6 + 7 + export const validNotifTypes = [ 8 + "like", 9 + "repost", 10 + "follow", 11 + "mention", 12 + "reply", 13 + "quote", 14 + ] as const; 15 + 16 + export const configAgentTools = [ 17 + "create_bluesky_post", 18 + "like_bluesky_post", 19 + "quote_bluesky_post", 20 + "repost_bluesky_post", 21 + "update_bluesky_connection", 22 + "update_bluesky_profile", 23 + ] as const; 24 + 25 + export const requiredAgentTools = [ 26 + "fetch_bluesky_posts", 27 + "get_bluesky_user_info", 28 + "ignore_notification", 29 + "mute_bluesky_thread", 30 + "search_bluesky", 31 + ] as const; 32 + 33 + export const allAgentTools = [ 34 + ...configAgentTools, 35 + ...requiredAgentTools, 36 + ] as const;
+123 -65
utils/declaration.ts
··· 1 import { bsky } from "../utils/bsky.ts"; 2 import { Lexicons } from "@atproto/lexicon"; 3 4 - // schema for updating PDS to indicate account is AI 5 - export const AI_DECLARATION_LEXICON = { 6 "lexicon": 1, 7 - "id": "studio.voyager.account.managedByAI", 8 "defs": { 9 "main": { 10 "type": "record", ··· 12 "record": { 13 "type": "object", 14 "properties": { 15 - "aiRole": { 16 "type": "string", 17 - "enum": [ 18 - "autonomous", 19 - "collaborative", 20 "assisted", 21 ], 22 - "description": "Level of AI involvement in account management", 23 }, 24 - "createdAt": { 25 - "type": "string", 26 - "format": "datetime", 27 - "description": "timestamp when this declaration was created", 28 }, 29 "description": { 30 "type": "string", 31 - "maxLength": 500, 32 "description": 33 - "additional context about this AI account's purpose or operation", 34 }, 35 "responsibleParty": { 36 "type": "object", 37 "properties": { 38 - "did": { 39 "type": "string", 40 - "format": "did", 41 - "description": "DID of the responsible party", 42 }, 43 "name": { 44 "type": "string", 45 - "maxLength": 100, 46 - "description": "name of the person or organization responsible", 47 }, 48 "contact": { 49 "type": "string", 50 "maxLength": 300, 51 - "description": "contact info (email, url, etc)", 52 }, 53 }, 54 "description": 55 - "info about the person or organization that is responsible for creating/managing this account", 56 }, 57 }, 58 "required": [ 59 - "aiRole", 60 "createdAt", 61 ], 62 }, 63 - "description": "declaration that this account is managed by AI", 64 }, 65 }, 66 }; 67 68 - export type AIDeclarationRecord = { 69 - $type: "studio.voyager.account.managedByAI"; 70 - aiRole: "autonomous" | "collaborative" | "assisted"; 71 - createdAt: string; // ISO datetime 72 - description?: string; 73 - responsibleParty?: { 74 - did?: string; 75 - name?: string; 76 - contact?: string; 77 - }; 78 - }; 79 - 80 - export const createDeclarationRecord = async () => { 81 - const role = Deno.env.get("AI_ROLE")?.toLowerCase(); 82 const projectDescription = Deno.env.get("PROJECT_DESCRIPTION"); 83 - const authorHandle = Deno.env.get("AUTHOR_BSKY_HANDLE"); 84 - const authorName = Deno.env.get("AUTHOR_NAME"); 85 - const authorContact = Deno.env.get("AUTHOR_CONTACT"); 86 87 - const declarationRecord: AIDeclarationRecord = { 88 - $type: "studio.voyager.account.managedByAI", 89 - aiRole: 90 - (role === "autonomous" || role === "collaborative" || role === "assisted") 91 - ? role 92 - : "autonomous", 93 createdAt: new Date().toISOString(), 94 }; 95 96 if (projectDescription?.trim()) { 97 - declarationRecord.description = projectDescription; 98 } 99 100 - if (authorHandle || authorName || authorContact) { 101 declarationRecord.responsibleParty = {}; 102 103 - if (authorHandle) { 104 - const authorData = await bsky.getProfile({ actor: authorHandle }); 105 - declarationRecord.responsibleParty.did = authorData.data.did; 106 } 107 108 - if (authorName) { 109 - declarationRecord.responsibleParty.name = authorName; 110 } 111 112 - if (authorContact) { 113 - declarationRecord.responsibleParty.contact = authorContact; 114 } 115 } 116 117 return declarationRecord; 118 }; 119 120 - export const submitDeclarationRecord = async () => { 121 const lex = new Lexicons(); 122 123 try { 124 - lex.add(AI_DECLARATION_LEXICON as any); 125 - const record = await createDeclarationRecord(); 126 127 lex.assertValidRecord( 128 - "studio.voyager.account.managedByAI", 129 record, 130 ); 131 ··· 139 try { 140 await bsky.com.atproto.repo.getRecord({ 141 repo, 142 - collection: "studio.voyager.account.managedByAI", 143 rkey: "self", 144 }); 145 exists = true; 146 - console.log("Existing declaration record found - updating..."); 147 } catch (error: any) { 148 // Handle "record not found" errors (status 400 with error: "RecordNotFound") 149 const isNotFound = ··· 153 error?.message?.includes("Could not locate record"); 154 155 if (isNotFound) { 156 - console.log("No existing declaration record found - creating new..."); 157 } else { 158 // Re-throw if it's not a "not found" error 159 throw error; ··· 163 // Create or update the record 164 const result = await bsky.com.atproto.repo.putRecord({ 165 repo, 166 - collection: "studio.voyager.account.managedByAI", 167 rkey: "self", 168 record, 169 }); 170 171 console.log( 172 - `Declaration record ${exists ? "updated" : "created"} successfully:`, 173 result, 174 ); 175 return result; 176 } catch (error) { 177 - console.error("error submitting declaration record", error); 178 throw error; 179 } 180 };
··· 1 import { bsky } from "../utils/bsky.ts"; 2 + import type { AutonomyDeclarationRecord } from "./types.ts"; 3 import { Lexicons } from "@atproto/lexicon"; 4 5 + export const AUTONOMY_DECLARATION_LEXICON = { 6 "lexicon": 1, 7 + "id": "studio.voyager.account.autonomy", 8 "defs": { 9 "main": { 10 "type": "record", ··· 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 92 + export 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 178 + export 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 ··· 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 = ··· 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; ··· 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 };
+2 -3
utils/messageAgent.ts
··· 1 import { LettaClient } from "@letta-ai/letta-client"; 2 - import { session } from "./session.ts"; 3 - 4 // Helper function to format tool arguments as inline key-value pairs 5 const formatArgsInline = (args: unknown): string => { 6 try { ··· 57 if (response.messageType === "reasoning_message") { 58 console.log(`💭 reasoning…`); 59 } else if (response.messageType === "assistant_message") { 60 - console.log(`💬 ${session.agentName}: ${response.content}`); 61 } else if (response.messageType === "tool_call_message") { 62 const formattedArgs = formatArgsInline(response.toolCall.arguments); 63 console.log(
··· 1 import { LettaClient } from "@letta-ai/letta-client"; 2 + import { agentContext } from "./agentContext.ts"; 3 // Helper function to format tool arguments as inline key-value pairs 4 const formatArgsInline = (args: unknown): string => { 5 try { ··· 56 if (response.messageType === "reasoning_message") { 57 console.log(`💭 reasoning…`); 58 } else if (response.messageType === "assistant_message") { 59 + console.log(`💬 ${agentContext.agentBskyName}: ${response.content}`); 60 } else if (response.messageType === "tool_call_message") { 61 const formattedArgs = formatArgsInline(response.toolCall.arguments); 62 console.log(
+2 -2
utils/processNotification.ts
··· 1 import type { Notification } from "./types.ts"; 2 - import { session } from "./session.ts"; 3 import { messageAgent } from "./messageAgent.ts"; 4 5 import { likePrompt } from "../prompts/likePrompt.ts"; ··· 62 error, 63 ); 64 } finally { 65 - (session as any)[handler]++; 66 } 67 };
··· 1 import type { Notification } from "./types.ts"; 2 + import { agentContext } from "./agentContext.ts"; 3 import { messageAgent } from "./messageAgent.ts"; 4 5 import { likePrompt } from "../prompts/likePrompt.ts"; ··· 62 error, 63 ); 64 } finally { 65 + (agentContext as any)[handler]++; 66 } 67 };
+74 -33
utils/time.ts
··· 1 - import { session } from "./session.ts"; 2 import { Temporal } from "@js-temporal/polyfill"; 3 4 /** 5 * Generate a random time interval in milliseconds within a defined range 6 * 7 - * @param minMinutes - the minimum duration in minutes (default: 5) 8 - * @param maxMinutes - the maximum duration in minutes (default: 15) 9 * @returns A random time interval in milliseconds between the min and max range 10 - * @throws {Error} if Max <= min, if either value is negative, or if either is > 12 hours 11 */ 12 13 export const msRandomOffset = ( 14 - minMinutes: number = 5, 15 - maxMinutes: number = 15, 16 ): number => { 17 - const maximumOutputMinutes = 1440; // 24 hours 18 - if (maxMinutes <= minMinutes) { 19 - throw new Error("Maximum minutes must be larger than minimum minutes"); 20 } 21 22 - if (minMinutes < 0 || maxMinutes < 0) { 23 throw new Error("Time values must be non-negative"); 24 } 25 26 - if (Math.max(minMinutes, maxMinutes) > maximumOutputMinutes) { 27 - console.log("max minutes: ", maxMinutes, "min minutes :", minMinutes); 28 throw new Error( 29 - `time values must not exceed ${maximumOutputMinutes} (${ 30 - maximumOutputMinutes / 60 31 - } hours)`, 32 ); 33 } 34 35 - const min = Math.ceil((minMinutes * 60) * 1000); 36 - const max = Math.floor((maxMinutes * 60) * 1000); 37 38 return Math.floor(Math.random() * (max - min) + min); 39 }; 40 41 export const msUntilNextWakeWindow = ( 42 - sleepTime: number, 43 - wakeTime: number, 44 - minMinutesOffset: number, 45 - maxMinutesOffset: number, 46 ): number => { 47 - const current = Temporal.Now.zonedDateTimeISO(session.timeZone); 48 49 - if (current.hour >= wakeTime && current.hour < sleepTime) { 50 return 0; 51 } else { 52 let newTime; 53 54 - if (current.hour < wakeTime) { 55 - newTime = current.with({ hour: wakeTime }); 56 } else { 57 - newTime = current.add({ days: 1 }).with({ hour: wakeTime }); 58 } 59 60 return newTime.toInstant().epochMilliseconds + 61 - msRandomOffset(minMinutesOffset, maxMinutesOffset) - 62 current.toInstant().epochMilliseconds; 63 } 64 }; 65 66 export const msUntilDailyWindow = ( 67 window: number, 68 - minMinutesOffset: number, 69 - maxMinutesOffset: number, 70 - ) => { 71 - const current = Temporal.Now.zonedDateTimeISO(session.timeZone); 72 73 let msToWindow; 74 if (current.hour < window) { ··· 79 } 80 81 return msToWindow + 82 - msRandomOffset(minMinutesOffset, maxMinutesOffset) - 83 current.toInstant().epochMilliseconds; 84 }; 85 86 export const getNow = () => { 87 - return Temporal.Now.zonedDateTimeISO(session.timeZone); 88 };
··· 1 + import { agentContext } from "./agentContext.ts"; 2 import { Temporal } from "@js-temporal/polyfill"; 3 4 /** 5 + * Convert time units to milliseconds 6 + */ 7 + export const msFrom = { 8 + /** 9 + * Convert seconds to milliseconds 10 + * @param s - number of seconds 11 + */ 12 + seconds: (seconds: number): number => seconds * 1000, 13 + /** 14 + * Convert minutes to milliseconds 15 + * @param m - number of minutes 16 + */ 17 + minutes: (minutes: number): number => minutes * 60 * 1000, 18 + /** 19 + * Convert hours to milliseconds 20 + * @param h - number of hours 21 + */ 22 + hours: (hours: number): number => hours * 60 * 60 * 1000, 23 + }; 24 + 25 + /** 26 * Generate a random time interval in milliseconds within a defined range 27 * 28 + * @param minimum - the minimum duration in milliseconds (default: 5 minutes) 29 + * @param maximum - the maximum duration in milliseconds (default: 15 minutes) 30 * @returns A random time interval in milliseconds between the min and max range 31 */ 32 33 export const msRandomOffset = ( 34 + minimum: number = msFrom.minutes(5), 35 + maximum: number = msFrom.minutes(15), 36 ): number => { 37 + if (maximum <= minimum) { 38 + throw new Error("Maximum time must be larger than minimum time"); 39 } 40 41 + if (minimum < 0 || maximum < 0) { 42 throw new Error("Time values must be non-negative"); 43 } 44 45 + if (Math.max(minimum, maximum) > msFrom.hours(24)) { 46 throw new Error( 47 + `time values must not exceed ${ 48 + msFrom.hours(24) 49 + } (24 hours). you entered: [min: ${minimum}ms, max: ${maximum}ms]`, 50 ); 51 } 52 53 + const min = Math.ceil(minimum); 54 + const max = Math.floor(maximum); 55 56 return Math.floor(Math.random() * (max - min) + min); 57 }; 58 59 + /** 60 + * finds the time in milliseconds until the next wake window 61 + * 62 + * @param minimumOffset - the minimum duration in milliseconds to offset from the window 63 + * @param maximumOffset - the maximum duration in milliseconds to offset from the window 64 + * @returns time until next wake window plus random offset, in milliseconds 65 + */ 66 export const msUntilNextWakeWindow = ( 67 + minimumOffset: number, 68 + maximumOffset: number, 69 ): number => { 70 + const current = Temporal.Now.zonedDateTimeISO(agentContext.timeZone); 71 + 72 + if (!agentContext.sleepEnabled) { 73 + return 0; 74 + } 75 76 + if ( 77 + current.hour >= agentContext.wakeTime && 78 + current.hour < agentContext.sleepTime 79 + ) { 80 return 0; 81 } else { 82 let newTime; 83 84 + if (current.hour < agentContext.wakeTime) { 85 + newTime = current.with({ hour: agentContext.wakeTime }); 86 } else { 87 + newTime = current.add({ days: 1 }).with({ hour: agentContext.wakeTime }); 88 } 89 90 return newTime.toInstant().epochMilliseconds + 91 + msRandomOffset(minimumOffset, maximumOffset) - 92 current.toInstant().epochMilliseconds; 93 } 94 }; 95 96 + /** 97 + * Calculate the time until next configurable window, plus a random offset. 98 + * @param window - the hour of the day to wake up at 99 + * @param minimumOffset - the minimum duration in milliseconds to offset from the window 100 + * @param maximumOffset - the maximum duration in milliseconds to offset from the window 101 + * @returns time until next daily window plus random offset, in milliseconds 102 + */ 103 export const msUntilDailyWindow = ( 104 window: number, 105 + minimumOffset: number, 106 + maximumOffset: number, 107 + ): number => { 108 + const current = Temporal.Now.zonedDateTimeISO(agentContext.timeZone); 109 + 110 + if (window > 23) { 111 + throw Error("window hour cannot exceed 23 (11pm)"); 112 + } 113 114 let msToWindow; 115 if (current.hour < window) { ··· 120 } 121 122 return msToWindow + 123 + msRandomOffset(minimumOffset, maximumOffset) - 124 current.toInstant().epochMilliseconds; 125 }; 126 127 export const getNow = () => { 128 + return Temporal.Now.zonedDateTimeISO(agentContext.timeZone); 129 };
+93 -1
utils/types.ts
··· 1 import type { AppBskyNotificationListNotifications } from "@atproto/api"; 2 3 - export type Notification = AppBskyNotificationListNotifications.Notification; 4 5 export type memoryBlock = { 6 label: string;
··· 1 import type { AppBskyNotificationListNotifications } from "@atproto/api"; 2 + import { 3 + allAgentTools, 4 + configAgentTools, 5 + requiredAgentTools, 6 + validAutomationLevels, 7 + validNotifTypes, 8 + } from "./const.ts"; 9 + export type Notification = AppBskyNotificationListNotifications.Notification; 10 11 + export type AutomationLevel = typeof validAutomationLevels[number]; 12 + export type ResponsiblePartyType = "person" | "organization"; 13 + 14 + export type notifType = typeof validNotifTypes[number]; 15 + 16 + export type configAgentTool = typeof configAgentTools[number]; 17 + export type requiredAgentTool = typeof requiredAgentTools[number]; 18 + export type allAgentTool = typeof allAgentTools[number]; 19 + 20 + export type agentContextObject = { 21 + // state 22 + busy: boolean; 23 + sleeping: boolean; 24 + checkCount: number; 25 + reflectionCount: number; 26 + processingCount: number; 27 + proactiveCount: number; 28 + likeCount: number; 29 + repostCount: number; 30 + followCount: number; 31 + mentionCount: number; 32 + replyCount: number; 33 + quoteCount: number; 34 + // required manual variables 35 + lettaProjectIdentifier: string; 36 + agentBskyHandle: string; 37 + agentBskyName: string; 38 + responsiblePartyName: string; // what person or org is responsible for this bot? 39 + responsiblePartyContact: string; // email or url for people to contact about bot 40 + // required variables with fallbacks 41 + agentBskyServiceUrl: string; 42 + automationLevel: AutomationLevel; 43 + supportedNotifTypes: notifType[]; 44 + supportedTools: allAgentTool[]; 45 + notifDelayMinimum: number; 46 + notifDelayMaximum: number; 47 + notifDelayMultiplier: number; 48 + reflectionDelayMinimum: number; 49 + reflectionDelayMaximum: number; 50 + proactiveDelayMinimum: number; 51 + proactiveDelayMaximum: number; 52 + wakeTime: number; 53 + sleepTime: number; 54 + timeZone: string; 55 + responsiblePartyType: string; // person / organization 56 + // set automatically 57 + agentBskyDID: string; 58 + reflectionEnabled: boolean; 59 + proactiveEnabled: boolean; 60 + sleepEnabled: boolean; 61 + notifDelayCurrent: number; 62 + reflectionDelayCurrent: number; 63 + proactiveDelayCurrent: number; 64 + // optional 65 + automationDescription?: string; // short description of what this agent does 66 + disclosureUrl?: string; // url to a ToS/Privacy Policy style page 67 + responsiblePartyBsky?: string; // handle w/o @ or DID of responsible party 68 + }; 69 + 70 + export type AutonomyDeclarationRecord = { 71 + $type: "studio.voyager.account.autonomy"; 72 + 73 + // How automated is this account? 74 + automationLevel?: "human" | "assisted" | "collaborative" | "automated"; 75 + 76 + // Is AI involved in content creation? 77 + usesGenerativeAI?: boolean; 78 + 79 + // Plain language explanation 80 + description?: string; // maxGraphemes: 300 81 + 82 + // Who is accountable for this account? 83 + responsibleParty?: { 84 + type?: "person" | "organization"; 85 + name?: string; 86 + contact?: string; // email, URL, handle, or DID 87 + did?: string; // ATProto DID 88 + }; 89 + 90 + // Where can someone learn more? 91 + disclosureUrl?: string; // URI format 92 + 93 + // When was this declaration created? 94 + createdAt: string; // ISO datetime (required) 95 + }; 96 97 export type memoryBlock = { 98 label: string;