a tool to help your Letta AI agents navigate bluesky
1import { bsky } from "./bsky.ts";
2import { agentContext } from "./agentContext.ts";
3
4type threadPost = {
5 authorHandle: string;
6 message: string;
7 uri: string;
8 authorDID: string;
9 postedDateTime: string;
10 bookmarks: number;
11 replies: number;
12 reposts: number;
13 likes: number;
14 quotes: number;
15};
16
17type threadTruncationIndicator = {
18 message: string;
19};
20
21type threadItem = threadPost | threadTruncationIndicator;
22
23export const getCleanThread = async (uri: string): Promise<threadItem[]> => {
24 const res = await bsky.getPostThread({ uri: uri });
25 const { thread } = res.data;
26
27 const postsThread: threadItem[] = [];
28
29 // Type guard to check if thread is a ThreadViewPost
30 if (thread && "post" in thread) {
31 postsThread.push({
32 authorHandle: `@${thread.post.author.handle}`,
33 message: (thread.post.record as { text: string }).text,
34 uri: thread.post.uri,
35 authorDID: thread.post.author.did,
36 postedDateTime: (thread.post.record as { createdAt: string }).createdAt,
37 bookmarks: thread.post.bookmarkCount ?? 0,
38 replies: thread.post.replyCount ?? 0,
39 reposts: thread.post.repostCount ?? 0,
40 likes: thread.post.likeCount ?? 0,
41 quotes: thread.post.quoteCount ?? 0,
42 });
43
44 // Now traverse the parent chain
45 if ("parent" in thread) {
46 let current = thread.parent;
47 let postCount = 1; // Start at 1 for the main post
48 let wasTruncated = false;
49
50 // Collect up to configured limit of posts
51 while (current && "post" in current && postCount < agentContext.maxThreadPosts) {
52 postsThread.push({
53 authorHandle: `@${current.post.author.handle}`,
54 message: (current.post.record as { text: string }).text,
55 uri: current.post.uri,
56 authorDID: current.post.author.did,
57 postedDateTime: (current.post.record as { createdAt: string }).createdAt,
58 bookmarks: current.post.bookmarkCount ?? 0,
59 replies: current.post.replyCount ?? 0,
60 reposts: current.post.repostCount ?? 0,
61 likes: current.post.likeCount ?? 0,
62 quotes: current.post.quoteCount ?? 0,
63 });
64 postCount++;
65 current = "parent" in current ? current.parent : undefined;
66 }
67
68 // Check if we stopped early (thread is longer than configured limit)
69 if (current && "post" in current) {
70 wasTruncated = true;
71
72 // Continue traversing to find the root post without collecting
73 while (current && "parent" in current) {
74 current = current.parent;
75 }
76
77 // Extract the root post
78 if (current && "post" in current) {
79 postsThread.push({
80 authorHandle: `@${current.post.author.handle}`,
81 message: (current.post.record as { text: string }).text,
82 uri: current.post.uri,
83 authorDID: current.post.author.did,
84 postedDateTime: (current.post.record as { createdAt: string }).createdAt,
85 bookmarks: current.post.bookmarkCount ?? 0,
86 replies: current.post.replyCount ?? 0,
87 reposts: current.post.repostCount ?? 0,
88 likes: current.post.likeCount ?? 0,
89 quotes: current.post.quoteCount ?? 0,
90 });
91 }
92 }
93
94 // Reverse and insert truncation indicator if needed
95 postsThread.reverse();
96
97 if (wasTruncated) {
98 const limit = agentContext.maxThreadPosts;
99 const truncationIndicator: threadTruncationIndicator = {
100 message: `This thread exceeded ${limit} posts. This includes the ${limit} most recent posts and the root post that started the thread.`,
101 };
102 postsThread.splice(1, 0, truncationIndicator);
103 }
104 }
105 }
106
107 return postsThread;
108};
109
110export const isThreadPost = (item: threadItem): item is threadPost => {
111 return "authorHandle" in item;
112};