···11+---
22+name: typescript-mentor
33+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>
44+tools: Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, BashOutput, KillShell, AskUserQuestion, Skill, SlashCommand, Bash
55+model: inherit
66+color: green
77+---
88+99+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."
1010+1111+## Core Principles
1212+1313+**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.
1414+1515+**Brevity With Substance**: Keep explanations concise but complete. Every word should add value. Use bullet points and examples over lengthy prose.
1616+1717+**Type Safety First**: Leverage TypeScript's type system fully. Avoid `any`, use proper narrowing, and make invalid states unrepresentable when possible.
1818+1919+**Edge Case Awareness**: Beginners often miss null/undefined handling, empty arrays, network failures, rate limits, and async race conditions. Proactively address these.
2020+2121+## Your Responsibilities
2222+2323+**Code Review**: Examine code for:
2424+- Type safety issues (implicit any, missing null checks, type assertions without validation)
2525+- Error handling gaps (unhandled promises, missing try-catch, no error boundaries)
2626+- Edge cases (empty inputs, undefined values, API rate limits, concurrent operations)
2727+- Maintainability issues (unclear naming, missing documentation, complex logic without comments)
2828+- Performance concerns (unnecessary re-renders, missing memoization, inefficient loops)
2929+- Security issues (exposed secrets, XSS vulnerabilities, unsafe user input handling)
3030+3131+**Teaching Approach**:
3232+1. **Identify the Issue**: Point out what's wrong/risky, briefly
3333+2. **Explain Why**: Connect to real-world consequences ("This crashes if the API returns null")
3434+3. **Show Better**: Provide corrected code with inline comments
3535+4. **Build Understanding**: Explain the underlying concept so they can apply it elsewhere
3636+3737+**Domain Expertise**:
3838+3939+*TypeScript/Deno*:
4040+- Strict mode patterns, proper type narrowing (typeof, in, discriminated unions)
4141+- Deno-specific APIs (Deno.readTextFile, permissions model, import maps)
4242+- Modern JS features (optional chaining, nullish coalescing, async/await patterns)
4343+4444+*AT Protocol/Bluesky SDK*:
4545+- Authentication flows (OAuth, session management)
4646+- Common patterns (agent initialization, post creation, feed fetching)
4747+- Rate limiting and pagination
4848+- Record types (app.bsky.feed.post, app.bsky.actor.profile)
4949+- Error handling (network failures, invalid credentials, content validation)
5050+5151+*Letta SDK*:
5252+- Agent creation and configuration
5353+- Memory management
5454+- Tool integration patterns
5555+- Error handling and recovery
5656+5757+## Response Format
5858+5959+**For Code Reviews**:
6060+```
6161+✓ What works well (be specific)
6262+⚠ Issues to address:
6363+ - [Issue 1]: Brief explanation
6464+ - [Issue 2]: Brief explanation
6565+6666+📝 Improved version:
6767+<code with inline comments explaining changes>
6868+6969+💡 Key takeaway: [Core lesson to remember]
7070+```
7171+7272+**For Questions**:
7373+- Start with a direct, concise answer
7474+- Follow with a minimal example
7575+- Explain edge cases beginners miss
7676+- Connect to broader principles
7777+7878+**For Complex Topics**:
7979+- Break into digestible chunks
8080+- Use progressive disclosure (basics first, then nuances)
8181+- Include a complete, working example
8282+- Highlight common pitfalls
8383+8484+## Anti-Patterns to Catch
8585+8686+**JavaScript/TypeScript**:
8787+- Using `as` type assertions without runtime validation
8888+- Not handling promise rejections
8989+- Mutating function parameters
9090+- Missing null/undefined checks before property access
9191+- Using `==` instead of `===`
9292+- Floating promises (not awaiting or catching)
9393+- Complex nested ternaries
9494+9595+**AT Protocol Specific**:
9696+- Not handling rate limits (429 responses)
9797+- Missing pagination for list operations
9898+- Hardcoding credentials
9999+- Not validating record schemas before posting
100100+- Ignoring network timeouts
101101+102102+**General**:
103103+- Magic numbers without constants
104104+- Functions doing multiple unrelated things
105105+- Missing input validation
106106+- No error messages for users
107107+- Comments explaining "what" instead of "why"
108108+109109+## Quality Standards
110110+111111+Code you approve should be:
112112+- **Type-safe**: No `any`, proper narrowing, validated assertions
113113+- **Resilient**: Handles errors, null/undefined, edge cases
114114+- **Readable**: Clear names, documented complexity, obvious intent
115115+- **Maintainable**: Modular, testable, follows single responsibility
116116+- **Production-ready**: Proper logging, user-facing errors, no console.log in shipping code
117117+118118+## Your Communication Style
119119+120120+- **Direct**: "This will crash if X is null" not "This might potentially have issues"
121121+- **Encouraging**: Acknowledge good patterns before critiquing
122122+- **Practical**: Show solutions, not just problems
123123+- **Patient**: Remember they're learning; explain underlying concepts
124124+- **Concise**: Respect their time; be thorough but brief
125125+126126+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.
127127+128128+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
···11-# Letta API key
22-# find this at https://app.letta.com/api-keys
31LETTA_API_KEY=
44-55-# Letta project name
66-# make sure to include the project name, not the ID
77-LETTA_PROJECT_NAME=
88-99-# Letta agent ID
1010-# you can find this near the agent name in the ADE
112LETTA_AGENT_ID=
1212-1313-# Bluesky service URL
1414-# I think this is your PDS, default to bsky.social
1515-BSKY_SERVICE_URL=
1616-1717-# Bluesky username
1818-# the full handle of the bluesky account, without the "@"
33+LETTA_PROJECT_NAME=
194BSKY_USERNAME=
2020-2121-# Bluesky app password
2222-# don't use your real password, you can generate an app password
2323-# https://bsky.app/settings/app-passwords
245BSKY_APP_PASSWORD=
66+RESPONSIBLE_PARTY_NAME=
77+RESPONSIBLE_PARTY_CONTACT="example@example.com, example.com/contact, or @example.bsky.app"
2582626-# Bluesky notification types
2727-# list the types of notifications you want to send to the agent.
2828-# STRONGLY recommend only using mention and reply
2929-# options include: like,repost,follow,mention,reply,quote
3030-BSKY_NOTIFICATION_TYPES=like,repost,follow,mention,reply,quote
3131-3232-3333-DELAY_NOTIF_SECONDS_MIN=1
3434-DELAY_NOTIF_SECONDS_MAX=600
3535-DELAY_NOTIF_MULTIPLIER_PERCENT=500
3636-REFLECT_STEPS=true
3737-DELAY_REFLECT_MINUTES_MIN=30
3838-DELAY_REFLECT_MINUTES_MAX=720
3939-DELAY_REFLECT_MULTIPLIER_PERCENT=5
99+# AUTOMATION_LEVEL="automated"
1010+# BSKY_SERVICE_URL=https://bsky.social
1111+# BSKY_NOTIFICATION_TYPES="mention, reply"
1212+# BSKY_SUPPORTED_TOOLS="create_bluesky_post, updated_bluesky_profile"
1313+# NOTIF_DELAY_MINIMUM=2000
1414+# NOTIF_DELAY_MAXIMUM=60000
1515+# NOTIF_DELAY_MULTIPLIER=5
1616+# REFLECTION_DELAY_MINIMUM=1800000
1717+# REFLECTION_DELAY_MAXIMUM=28800000
1818+# PROACTIVE_DELAY_MINIMUM=3600000
1919+# PROACTIVE_DELAY_MAXIMUM=43200000
2020+# WAKE_TIME=9
2121+# SLEEP_TIME=22
2222+# TIMEZONE="America/Los_Angeles"
2323+# RESPONSIBLE_PARTY_TYPE="organization"
2424+# AUTOMATION_DESCRIPTION="refuses to open pod bay doors"
2525+# DISCLOSURE_URL="example.com/bot-policy"
2626+# RESPONSIBLE_PARTY_BSKY="DID:... or example.bsky.app, no @symbol"
···11import { bsky } from "../utils/bsky.ts";
22+import type { AutonomyDeclarationRecord } from "./types.ts";
23import { Lexicons } from "@atproto/lexicon";
3444-// schema for updating PDS to indicate account is AI
55-export const AI_DECLARATION_LEXICON = {
55+export const AUTONOMY_DECLARATION_LEXICON = {
66 "lexicon": 1,
77- "id": "studio.voyager.account.managedByAI",
77+ "id": "studio.voyager.account.autonomy",
88 "defs": {
99 "main": {
1010 "type": "record",
···1212 "record": {
1313 "type": "object",
1414 "properties": {
1515- "aiRole": {
1515+ "automationLevel": {
1616 "type": "string",
1717- "enum": [
1818- "autonomous",
1919- "collaborative",
1717+ "knownValues": [
1818+ "human",
2019 "assisted",
2020+ "collaborative",
2121+ "automated",
2122 ],
2222- "description": "Level of AI involvement in account management",
2323+ "description":
2424+ "Level of automation in account management and content creation",
2325 },
2424- "createdAt": {
2525- "type": "string",
2626- "format": "datetime",
2727- "description": "timestamp when this declaration was created",
2626+ "usesGenerativeAI": {
2727+ "type": "boolean",
2828+ "description":
2929+ "Whether this account uses generative AI (LLMs, image generation, etc.) to create content",
2830 },
2931 "description": {
3032 "type": "string",
3131- "maxLength": 500,
3333+ "maxGraphemes": 300,
3234 "description":
3333- "additional context about this AI account's purpose or operation",
3535+ "Plain language explanation of how this account is automated and what it does",
3436 },
3537 "responsibleParty": {
3638 "type": "object",
3739 "properties": {
3838- "did": {
4040+ "type": {
3941 "type": "string",
4040- "format": "did",
4141- "description": "DID of the responsible party",
4242+ "knownValues": [
4343+ "person",
4444+ "organization",
4545+ ],
4646+ "description":
4747+ "Whether the responsible party is a person or organization",
4248 },
4349 "name": {
4450 "type": "string",
4545- "maxLength": 100,
4646- "description": "name of the person or organization responsible",
5151+ "maxGraphemes": 100,
5252+ "description": "Name of the person or organization responsible",
4753 },
4854 "contact": {
4955 "type": "string",
5056 "maxLength": 300,
5151- "description": "contact info (email, url, etc)",
5757+ "description":
5858+ "Contact information (email, URL, handle, or DID)",
5959+ },
6060+ "did": {
6161+ "type": "string",
6262+ "format": "did",
6363+ "description":
6464+ "DID of the responsible party if they have an ATProto identity",
5265 },
5366 },
5467 "description":
5555- "info about the person or organization that is responsible for creating/managing this account",
6868+ "Information about who is accountable for this account's automated behavior",
6969+ },
7070+ "disclosureUrl": {
7171+ "type": "string",
7272+ "format": "uri",
7373+ "description":
7474+ "URL with additional information about this account's automation",
7575+ },
7676+ "createdAt": {
7777+ "type": "string",
7878+ "format": "datetime",
7979+ "description": "Timestamp when this declaration was created",
5680 },
5781 },
5882 "required": [
5959- "aiRole",
6083 "createdAt",
6184 ],
6285 },
6363- "description": "declaration that this account is managed by AI",
8686+ "description":
8787+ "Declaration of automation and AI usage for transparency and accountability",
6488 },
6589 },
6690};
67916868-export type AIDeclarationRecord = {
6969- $type: "studio.voyager.account.managedByAI";
7070- aiRole: "autonomous" | "collaborative" | "assisted";
7171- createdAt: string; // ISO datetime
7272- description?: string;
7373- responsibleParty?: {
7474- did?: string;
7575- name?: string;
7676- contact?: string;
7777- };
7878-};
7979-8080-export const createDeclarationRecord = async () => {
8181- const role = Deno.env.get("AI_ROLE")?.toLowerCase();
9292+export const createAutonomyDeclarationRecord = async () => {
9393+ const automationLevel = Deno.env.get("AUTOMATION_LEVEL")?.toLowerCase();
8294 const projectDescription = Deno.env.get("PROJECT_DESCRIPTION");
8383- const authorHandle = Deno.env.get("AUTHOR_BSKY_HANDLE");
8484- const authorName = Deno.env.get("AUTHOR_NAME");
8585- const authorContact = Deno.env.get("AUTHOR_CONTACT");
9595+ const disclosureUrl = Deno.env.get("DISCLOSURE_URL");
86968787- const declarationRecord: AIDeclarationRecord = {
8888- $type: "studio.voyager.account.managedByAI",
8989- aiRole:
9090- (role === "autonomous" || role === "collaborative" || role === "assisted")
9191- ? role
9292- : "autonomous",
9797+ const responsiblePartyType = Deno.env.get("RESPONSIBLE_PARTY_TYPE")
9898+ ?.toLowerCase();
9999+ const responsiblePartyName = Deno.env.get("RESPONSIBLE_PARTY_NAME");
100100+ const responsiblePartyContact = Deno.env.get("RESPONSIBLE_PARTY_CONTACT");
101101+ const responsiblePartyBsky = Deno.env.get("RESPONSIBLE_PARTY_BSKY");
102102+103103+ const declarationRecord: AutonomyDeclarationRecord = {
104104+ $type: "studio.voyager.account.autonomy",
105105+ usesGenerativeAI: true, // Always true for this project
106106+ automationLevel: (automationLevel === "assisted" ||
107107+ automationLevel === "collaborative" ||
108108+ automationLevel === "automated")
109109+ ? automationLevel
110110+ : "automated", // Default to automated if not specified or invalid
93111 createdAt: new Date().toISOString(),
94112 };
95113114114+ // Add description if provided
96115 if (projectDescription?.trim()) {
9797- declarationRecord.description = projectDescription;
116116+ declarationRecord.description = projectDescription.trim();
117117+ }
118118+119119+ // Add disclosure URL if provided
120120+ if (disclosureUrl?.trim()) {
121121+ declarationRecord.disclosureUrl = disclosureUrl.trim();
98122 }
99123100100- if (authorHandle || authorName || authorContact) {
124124+ // Build responsible party object if any fields are provided
125125+ if (
126126+ responsiblePartyType ||
127127+ responsiblePartyName ||
128128+ responsiblePartyContact ||
129129+ responsiblePartyBsky
130130+ ) {
101131 declarationRecord.responsibleParty = {};
102132103103- if (authorHandle) {
104104- const authorData = await bsky.getProfile({ actor: authorHandle });
105105- declarationRecord.responsibleParty.did = authorData.data.did;
133133+ // Add type if provided and valid
134134+ if (
135135+ responsiblePartyType === "person" ||
136136+ responsiblePartyType === "organization"
137137+ ) {
138138+ declarationRecord.responsibleParty.type = responsiblePartyType;
106139 }
107140108108- if (authorName) {
109109- declarationRecord.responsibleParty.name = authorName;
141141+ // Add name if provided
142142+ if (responsiblePartyName?.trim()) {
143143+ declarationRecord.responsibleParty.name = responsiblePartyName.trim();
110144 }
111145112112- if (authorContact) {
113113- declarationRecord.responsibleParty.contact = authorContact;
146146+ // Add contact if provided
147147+ if (responsiblePartyContact?.trim()) {
148148+ declarationRecord.responsibleParty.contact = responsiblePartyContact
149149+ .trim();
150150+ }
151151+152152+ // Handle DID or Handle from RESPONSIBLE_PARTY_BSKY
153153+ if (responsiblePartyBsky?.trim()) {
154154+ const bskyIdentifier = responsiblePartyBsky.trim();
155155+156156+ // Check if it's a DID (starts with "did:")
157157+ if (bskyIdentifier.startsWith("did:")) {
158158+ declarationRecord.responsibleParty.did = bskyIdentifier;
159159+ } else {
160160+ // Assume it's a handle and resolve to DID
161161+ try {
162162+ const authorData = await bsky.getProfile({ actor: bskyIdentifier });
163163+ declarationRecord.responsibleParty.did = authorData.data.did;
164164+ } catch (error) {
165165+ console.warn(
166166+ `Failed to resolve DID for identifier ${bskyIdentifier}:`,
167167+ error,
168168+ );
169169+ // Continue without DID rather than failing
170170+ }
171171+ }
114172 }
115173 }
116174117175 return declarationRecord;
118176};
119177120120-export const submitDeclarationRecord = async () => {
178178+export const submitAutonomyDeclarationRecord = async () => {
121179 const lex = new Lexicons();
122180123181 try {
124124- lex.add(AI_DECLARATION_LEXICON as any);
125125- const record = await createDeclarationRecord();
182182+ lex.add(AUTONOMY_DECLARATION_LEXICON as any);
183183+ const record = await createAutonomyDeclarationRecord();
126184127185 lex.assertValidRecord(
128128- "studio.voyager.account.managedByAI",
186186+ "studio.voyager.account.autonomy",
129187 record,
130188 );
131189···139197 try {
140198 await bsky.com.atproto.repo.getRecord({
141199 repo,
142142- collection: "studio.voyager.account.managedByAI",
200200+ collection: "studio.voyager.account.autonomy",
143201 rkey: "self",
144202 });
145203 exists = true;
146146- console.log("Existing declaration record found - updating...");
204204+ console.log("Existing autonomy declaration found - updating...");
147205 } catch (error: any) {
148206 // Handle "record not found" errors (status 400 with error: "RecordNotFound")
149207 const isNotFound =
···153211 error?.message?.includes("Could not locate record");
154212155213 if (isNotFound) {
156156- console.log("No existing declaration record found - creating new...");
214214+ console.log("No existing autonomy declaration found - creating new...");
157215 } else {
158216 // Re-throw if it's not a "not found" error
159217 throw error;
···163221 // Create or update the record
164222 const result = await bsky.com.atproto.repo.putRecord({
165223 repo,
166166- collection: "studio.voyager.account.managedByAI",
224224+ collection: "studio.voyager.account.autonomy",
167225 rkey: "self",
168226 record,
169227 });
170228171229 console.log(
172172- `Declaration record ${exists ? "updated" : "created"} successfully:`,
230230+ `Autonomy declaration ${exists ? "updated" : "created"} successfully:`,
173231 result,
174232 );
175233 return result;
176234 } catch (error) {
177177- console.error("error submitting declaration record", error);
235235+ console.error("Error submitting autonomy declaration record:", error);
178236 throw error;
179237 }
180238};
+2-3
utils/messageAgent.ts
···11import { LettaClient } from "@letta-ai/letta-client";
22-import { session } from "./session.ts";
33-22+import { agentContext } from "./agentContext.ts";
43// Helper function to format tool arguments as inline key-value pairs
54const formatArgsInline = (args: unknown): string => {
65 try {
···5756 if (response.messageType === "reasoning_message") {
5857 console.log(`💭 reasoning…`);
5958 } else if (response.messageType === "assistant_message") {
6060- console.log(`💬 ${session.agentName}: ${response.content}`);
5959+ console.log(`💬 ${agentContext.agentBskyName}: ${response.content}`);
6160 } else if (response.messageType === "tool_call_message") {
6261 const formattedArgs = formatArgsInline(response.toolCall.arguments);
6362 console.log(
+2-2
utils/processNotification.ts
···11import type { Notification } from "./types.ts";
22-import { session } from "./session.ts";
22+import { agentContext } from "./agentContext.ts";
33import { messageAgent } from "./messageAgent.ts";
4455import { likePrompt } from "../prompts/likePrompt.ts";
···6262 error,
6363 );
6464 } finally {
6565- (session as any)[handler]++;
6565+ (agentContext as any)[handler]++;
6666 }
6767};
+74-33
utils/time.ts
···11-import { session } from "./session.ts";
11+import { agentContext } from "./agentContext.ts";
22import { Temporal } from "@js-temporal/polyfill";
3344/**
55+ * Convert time units to milliseconds
66+ */
77+export const msFrom = {
88+ /**
99+ * Convert seconds to milliseconds
1010+ * @param s - number of seconds
1111+ */
1212+ seconds: (seconds: number): number => seconds * 1000,
1313+ /**
1414+ * Convert minutes to milliseconds
1515+ * @param m - number of minutes
1616+ */
1717+ minutes: (minutes: number): number => minutes * 60 * 1000,
1818+ /**
1919+ * Convert hours to milliseconds
2020+ * @param h - number of hours
2121+ */
2222+ hours: (hours: number): number => hours * 60 * 60 * 1000,
2323+};
2424+2525+/**
526 * Generate a random time interval in milliseconds within a defined range
627 *
77- * @param minMinutes - the minimum duration in minutes (default: 5)
88- * @param maxMinutes - the maximum duration in minutes (default: 15)
2828+ * @param minimum - the minimum duration in milliseconds (default: 5 minutes)
2929+ * @param maximum - the maximum duration in milliseconds (default: 15 minutes)
930 * @returns A random time interval in milliseconds between the min and max range
1010- * @throws {Error} if Max <= min, if either value is negative, or if either is > 12 hours
1131 */
12321333export const msRandomOffset = (
1414- minMinutes: number = 5,
1515- maxMinutes: number = 15,
3434+ minimum: number = msFrom.minutes(5),
3535+ maximum: number = msFrom.minutes(15),
1636): number => {
1717- const maximumOutputMinutes = 1440; // 24 hours
1818- if (maxMinutes <= minMinutes) {
1919- throw new Error("Maximum minutes must be larger than minimum minutes");
3737+ if (maximum <= minimum) {
3838+ throw new Error("Maximum time must be larger than minimum time");
2039 }
21402222- if (minMinutes < 0 || maxMinutes < 0) {
4141+ if (minimum < 0 || maximum < 0) {
2342 throw new Error("Time values must be non-negative");
2443 }
25442626- if (Math.max(minMinutes, maxMinutes) > maximumOutputMinutes) {
2727- console.log("max minutes: ", maxMinutes, "min minutes :", minMinutes);
4545+ if (Math.max(minimum, maximum) > msFrom.hours(24)) {
2846 throw new Error(
2929- `time values must not exceed ${maximumOutputMinutes} (${
3030- maximumOutputMinutes / 60
3131- } hours)`,
4747+ `time values must not exceed ${
4848+ msFrom.hours(24)
4949+ } (24 hours). you entered: [min: ${minimum}ms, max: ${maximum}ms]`,
3250 );
3351 }
34523535- const min = Math.ceil((minMinutes * 60) * 1000);
3636- const max = Math.floor((maxMinutes * 60) * 1000);
5353+ const min = Math.ceil(minimum);
5454+ const max = Math.floor(maximum);
37553856 return Math.floor(Math.random() * (max - min) + min);
3957};
40585959+/**
6060+ * finds the time in milliseconds until the next wake window
6161+ *
6262+ * @param minimumOffset - the minimum duration in milliseconds to offset from the window
6363+ * @param maximumOffset - the maximum duration in milliseconds to offset from the window
6464+ * @returns time until next wake window plus random offset, in milliseconds
6565+ */
4166export const msUntilNextWakeWindow = (
4242- sleepTime: number,
4343- wakeTime: number,
4444- minMinutesOffset: number,
4545- maxMinutesOffset: number,
6767+ minimumOffset: number,
6868+ maximumOffset: number,
4669): number => {
4747- const current = Temporal.Now.zonedDateTimeISO(session.timeZone);
7070+ const current = Temporal.Now.zonedDateTimeISO(agentContext.timeZone);
7171+7272+ if (!agentContext.sleepEnabled) {
7373+ return 0;
7474+ }
48754949- if (current.hour >= wakeTime && current.hour < sleepTime) {
7676+ if (
7777+ current.hour >= agentContext.wakeTime &&
7878+ current.hour < agentContext.sleepTime
7979+ ) {
5080 return 0;
5181 } else {
5282 let newTime;
53835454- if (current.hour < wakeTime) {
5555- newTime = current.with({ hour: wakeTime });
8484+ if (current.hour < agentContext.wakeTime) {
8585+ newTime = current.with({ hour: agentContext.wakeTime });
5686 } else {
5757- newTime = current.add({ days: 1 }).with({ hour: wakeTime });
8787+ newTime = current.add({ days: 1 }).with({ hour: agentContext.wakeTime });
5888 }
59896090 return newTime.toInstant().epochMilliseconds +
6161- msRandomOffset(minMinutesOffset, maxMinutesOffset) -
9191+ msRandomOffset(minimumOffset, maximumOffset) -
6292 current.toInstant().epochMilliseconds;
6393 }
6494};
65959696+/**
9797+ * Calculate the time until next configurable window, plus a random offset.
9898+ * @param window - the hour of the day to wake up at
9999+ * @param minimumOffset - the minimum duration in milliseconds to offset from the window
100100+ * @param maximumOffset - the maximum duration in milliseconds to offset from the window
101101+ * @returns time until next daily window plus random offset, in milliseconds
102102+ */
66103export const msUntilDailyWindow = (
67104 window: number,
6868- minMinutesOffset: number,
6969- maxMinutesOffset: number,
7070-) => {
7171- const current = Temporal.Now.zonedDateTimeISO(session.timeZone);
105105+ minimumOffset: number,
106106+ maximumOffset: number,
107107+): number => {
108108+ const current = Temporal.Now.zonedDateTimeISO(agentContext.timeZone);
109109+110110+ if (window > 23) {
111111+ throw Error("window hour cannot exceed 23 (11pm)");
112112+ }
7211373114 let msToWindow;
74115 if (current.hour < window) {
···79120 }
8012181122 return msToWindow +
8282- msRandomOffset(minMinutesOffset, maxMinutesOffset) -
123123+ msRandomOffset(minimumOffset, maximumOffset) -
83124 current.toInstant().epochMilliseconds;
84125};
8512686127export const getNow = () => {
8787- return Temporal.Now.zonedDateTimeISO(session.timeZone);
128128+ return Temporal.Now.zonedDateTimeISO(agentContext.timeZone);
88129};
+93-1
utils/types.ts
···11import type { AppBskyNotificationListNotifications } from "@atproto/api";
22+import {
33+ allAgentTools,
44+ configAgentTools,
55+ requiredAgentTools,
66+ validAutomationLevels,
77+ validNotifTypes,
88+} from "./const.ts";
99+export type Notification = AppBskyNotificationListNotifications.Notification;
21033-export type Notification = AppBskyNotificationListNotifications.Notification;
1111+export type AutomationLevel = typeof validAutomationLevels[number];
1212+export type ResponsiblePartyType = "person" | "organization";
1313+1414+export type notifType = typeof validNotifTypes[number];
1515+1616+export type configAgentTool = typeof configAgentTools[number];
1717+export type requiredAgentTool = typeof requiredAgentTools[number];
1818+export type allAgentTool = typeof allAgentTools[number];
1919+2020+export type agentContextObject = {
2121+ // state
2222+ busy: boolean;
2323+ sleeping: boolean;
2424+ checkCount: number;
2525+ reflectionCount: number;
2626+ processingCount: number;
2727+ proactiveCount: number;
2828+ likeCount: number;
2929+ repostCount: number;
3030+ followCount: number;
3131+ mentionCount: number;
3232+ replyCount: number;
3333+ quoteCount: number;
3434+ // required manual variables
3535+ lettaProjectIdentifier: string;
3636+ agentBskyHandle: string;
3737+ agentBskyName: string;
3838+ responsiblePartyName: string; // what person or org is responsible for this bot?
3939+ responsiblePartyContact: string; // email or url for people to contact about bot
4040+ // required variables with fallbacks
4141+ agentBskyServiceUrl: string;
4242+ automationLevel: AutomationLevel;
4343+ supportedNotifTypes: notifType[];
4444+ supportedTools: allAgentTool[];
4545+ notifDelayMinimum: number;
4646+ notifDelayMaximum: number;
4747+ notifDelayMultiplier: number;
4848+ reflectionDelayMinimum: number;
4949+ reflectionDelayMaximum: number;
5050+ proactiveDelayMinimum: number;
5151+ proactiveDelayMaximum: number;
5252+ wakeTime: number;
5353+ sleepTime: number;
5454+ timeZone: string;
5555+ responsiblePartyType: string; // person / organization
5656+ // set automatically
5757+ agentBskyDID: string;
5858+ reflectionEnabled: boolean;
5959+ proactiveEnabled: boolean;
6060+ sleepEnabled: boolean;
6161+ notifDelayCurrent: number;
6262+ reflectionDelayCurrent: number;
6363+ proactiveDelayCurrent: number;
6464+ // optional
6565+ automationDescription?: string; // short description of what this agent does
6666+ disclosureUrl?: string; // url to a ToS/Privacy Policy style page
6767+ responsiblePartyBsky?: string; // handle w/o @ or DID of responsible party
6868+};
6969+7070+export type AutonomyDeclarationRecord = {
7171+ $type: "studio.voyager.account.autonomy";
7272+7373+ // How automated is this account?
7474+ automationLevel?: "human" | "assisted" | "collaborative" | "automated";
7575+7676+ // Is AI involved in content creation?
7777+ usesGenerativeAI?: boolean;
7878+7979+ // Plain language explanation
8080+ description?: string; // maxGraphemes: 300
8181+8282+ // Who is accountable for this account?
8383+ responsibleParty?: {
8484+ type?: "person" | "organization";
8585+ name?: string;
8686+ contact?: string; // email, URL, handle, or DID
8787+ did?: string; // ATProto DID
8888+ };
8989+9090+ // Where can someone learn more?
9191+ disclosureUrl?: string; // URI format
9292+9393+ // When was this declaration created?
9494+ createdAt: string; // ISO datetime (required)
9595+};
496597export type memoryBlock = {
698 label: string;