+128
.claude/agents/mentor.md
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-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
+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
+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
+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
+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
+2
-2
utils/processNotification.ts
···
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
+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
+93
-1
utils/types.ts
···
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;