+11
-7
prompts/quotePrompt.ts
+11
-7
prompts/quotePrompt.ts
···
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(
···
60
quotes: undefined,
61
}];
62
63
return `
64
# NOTIFICATION: Someone quoted your post
65
···
75
76
## Your Original Post
77
\`\`\`
78
-
${originalThread[originalThread.length - 1].message}
79
\`\`\`
80
81
## The Quote Post from @${notification.author.handle}
82
\`\`\`
83
-
${quotePostThread[quotePostThread.length - 1].message}
84
\`\`\`
85
86
## Quote Post Engagement
87
-
• **Likes:** ${quotePostThread[quotePostThread.length - 1].likes}
88
-
• **Replies:** ${quotePostThread[quotePostThread.length - 1].replies}
89
-
• **Reposts:** ${quotePostThread[quotePostThread.length - 1].reposts}
90
-
• **Quotes:** ${quotePostThread[quotePostThread.length - 1].quotes}
91
92
${
93
originalThread
···
1
import { Notification } from "../utils/types.ts";
2
import { doesUserFollowTarget } from "../utils/doesUserFollow.ts";
3
import { agentContext } from "../utils/agentContext.ts";
4
+
import { getCleanThread, isThreadPost } from "../utils/getCleanThread.ts";
5
6
export const quotePrompt = async (notification: Notification) => {
7
const isUserFollower = await doesUserFollowTarget(
···
60
quotes: undefined,
61
}];
62
63
+
// Get the last post from each thread (last item is always a post, never a system message)
64
+
const lastOriginalPost = originalThread[originalThread.length - 1] as any;
65
+
const lastQuotePost = quotePostThread[quotePostThread.length - 1] as any;
66
+
67
return `
68
# NOTIFICATION: Someone quoted your post
69
···
79
80
## Your Original Post
81
\`\`\`
82
+
${lastOriginalPost.message}
83
\`\`\`
84
85
## The Quote Post from @${notification.author.handle}
86
\`\`\`
87
+
${lastQuotePost.message}
88
\`\`\`
89
90
## Quote Post Engagement
91
+
• **Likes:** ${lastQuotePost.likes}
92
+
• **Replies:** ${lastQuotePost.replies}
93
+
• **Reposts:** ${lastQuotePost.reposts}
94
+
• **Quotes:** ${lastQuotePost.quotes}
95
96
${
97
originalThread
+1
-5
tasks/checkNotifications.ts
+1
-5
tasks/checkNotifications.ts
···
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
+11
utils/agentContext.ts
+11
utils/agentContext.ts
···
242
return (value / 100) + 1;
243
};
244
245
const getReflectionDelayMinimum = (): number => {
246
const value = msFrom.parse(Deno.env.get("REFLECTION_DELAY_MINIMUM"));
247
···
575
timeZone: getTimeZone(),
576
responsiblePartyType: getResponsiblePartyType(),
577
preserveAgentMemory: getPreserveMemoryBlocks(),
578
reflectionEnabled: setReflectionEnabled(),
579
proactiveEnabled: setProactiveEnabled(),
580
sleepEnabled: setSleepEnabled(),
···
242
return (value / 100) + 1;
243
};
244
245
+
const getMaxThreadPosts = (): number => {
246
+
const value = Number(Deno.env.get("MAX_THREAD_POSTS"));
247
+
248
+
if (isNaN(value) || value < 5 || value > 250) {
249
+
return 2;
250
+
}
251
+
252
+
return Math.round(value);
253
+
};
254
+
255
const getReflectionDelayMinimum = (): number => {
256
const value = msFrom.parse(Deno.env.get("REFLECTION_DELAY_MINIMUM"));
257
···
585
timeZone: getTimeZone(),
586
responsiblePartyType: getResponsiblePartyType(),
587
preserveAgentMemory: getPreserveMemoryBlocks(),
588
+
maxThreadPosts: getMaxThreadPosts(),
589
reflectionEnabled: setReflectionEnabled(),
590
proactiveEnabled: setProactiveEnabled(),
591
sleepEnabled: setSleepEnabled(),
+54
-3
utils/getCleanThread.ts
+54
-3
utils/getCleanThread.ts
···
1
import { bsky } from "./bsky.ts";
2
3
type threadPost = {
4
authorHandle: string;
···
13
quotes: number;
14
};
15
16
-
export const getCleanThread = async (uri: string): Promise<threadPost[]> => {
17
const res = await bsky.getPostThread({ uri: uri });
18
const { thread } = res.data;
19
20
-
const postsThread: threadPost[] = [];
21
22
// Type guard to check if thread is a ThreadViewPost
23
if (thread && "post" in thread) {
···
37
// Now traverse the parent chain
38
if ("parent" in thread) {
39
let current = thread.parent;
40
41
-
while (current && "post" in current) {
42
postsThread.push({
43
authorHandle: `@${current.post.author.handle}`,
44
message: (current.post.record as { text: string }).text,
···
51
likes: current.post.likeCount ?? 0,
52
quotes: current.post.quoteCount ?? 0,
53
});
54
current = "parent" in current ? current.parent : undefined;
55
}
56
postsThread.reverse();
57
}
58
}
59
60
return postsThread;
61
};
···
1
import { bsky } from "./bsky.ts";
2
+
import { agentContext } from "./agentContext.ts";
3
4
type threadPost = {
5
authorHandle: string;
···
14
quotes: number;
15
};
16
17
+
type threadTruncationIndicator = {
18
+
message: string;
19
+
};
20
+
21
+
type threadItem = threadPost | threadTruncationIndicator;
22
+
23
+
export 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) {
···
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,
···
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
+
110
+
export const isThreadPost = (item: threadItem): item is threadPost => {
111
+
return "authorHandle" in item;
112
+
};
+59
-8
utils/messageAgent.ts
+59
-8
utils/messageAgent.ts
···
1
import Letta 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, maxValueLength = 30): string => {
5
try {
6
const parsed = typeof args === "string" ? JSON.parse(args) : args;
7
if (typeof parsed !== "object" || parsed === null) {
···
12
let valueStr = typeof value === "object"
13
? JSON.stringify(value)
14
: String(value);
15
-
// Truncate long values
16
if (valueStr.length > maxValueLength) {
17
-
valueStr = valueStr.slice(0, maxValueLength) + "...";
18
}
19
return `${key}=${valueStr}`;
20
})
···
65
}
66
};
67
68
// Helper function to format tool response for logging
69
const formatToolResponse = (returnValue: string): string => {
70
try {
···
83
parsed = JSON.parse(pythonToJson);
84
}
85
86
// If parsed successfully and it's an object, format as key=value pairs
87
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
88
-
return `(${formatArgsInline(parsed, 30)})`;
89
}
90
91
-
// If it's an array or primitive, return the original string
92
return returnValue;
93
} catch {
94
// If parsing fails, return as-is (it's a simple string)
···
128
const toolCall = response.tool_call || response.tool_calls;
129
if (toolCall && toolCall.name) {
130
lastToolName = toolCall.name;
131
-
const formattedArgs = formatArgsInline(toolCall.arguments);
132
console.log(`🔧 tool called: ${toolCall.name} (${formattedArgs})`);
133
}
134
} else if (response.message_type === "tool_return_message") {
···
139
const separator = formattedResponse.startsWith("(") ? " " : ": ";
140
const logMessage = `↩️ tool response: ${lastToolName}${separator}${formattedResponse}`;
141
142
-
console.log(truncateString(logMessage, 80));
143
} else if (response.message_type === "usage_statistics") {
144
console.log(`🔢 total steps: ${response.step_count}`);
145
} else if (response.message_type === "hidden_reasoning_message") {
···
1
import Letta 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, maxValueLength = 50): string => {
5
try {
6
const parsed = typeof args === "string" ? JSON.parse(args) : args;
7
if (typeof parsed !== "object" || parsed === null) {
···
12
let valueStr = typeof value === "object"
13
? JSON.stringify(value)
14
: String(value);
15
+
// Truncate long values at word boundaries
16
if (valueStr.length > maxValueLength) {
17
+
const truncated = valueStr.slice(0, maxValueLength);
18
+
const lastSpace = truncated.lastIndexOf(" ");
19
+
// If we found a space in the last 30% of the truncated string, use it
20
+
valueStr = lastSpace > maxValueLength * 0.7
21
+
? truncated.slice(0, lastSpace) + "..."
22
+
: truncated + "...";
23
}
24
return `${key}=${valueStr}`;
25
})
···
70
}
71
};
72
73
+
// Helper function to select important params for logging
74
+
const selectImportantParams = (args: unknown): unknown => {
75
+
try {
76
+
const parsed = typeof args === "string" ? JSON.parse(args) : args;
77
+
if (typeof parsed !== "object" || parsed === null) {
78
+
return parsed;
79
+
}
80
+
81
+
const entries = Object.entries(parsed);
82
+
83
+
// Filter out URIs/DIDs and very long values
84
+
const filtered = entries.filter(([key, value]) => {
85
+
const str = String(value);
86
+
// Skip AT URIs and DIDs
87
+
if (str.includes("at://") || str.includes("did:plc:")) return false;
88
+
// Skip very long values
89
+
if (str.length > 60) return false;
90
+
return true;
91
+
});
92
+
93
+
// Take first 3, or first entry if none pass filters
94
+
const selected = filtered.length > 0
95
+
? filtered.slice(0, 3)
96
+
: entries.slice(0, 1);
97
+
98
+
return Object.fromEntries(selected);
99
+
} catch {
100
+
return args;
101
+
}
102
+
};
103
+
104
// Helper function to format tool response for logging
105
const formatToolResponse = (returnValue: string): string => {
106
try {
···
119
parsed = JSON.parse(pythonToJson);
120
}
121
122
+
// Handle arrays - show count and sample first item
123
+
if (Array.isArray(parsed)) {
124
+
const count = parsed.length;
125
+
if (count === 0) return "[]";
126
+
127
+
const firstItem = parsed[0];
128
+
if (typeof firstItem === "object" && firstItem !== null) {
129
+
// Format first item's key fields (use 30 for samples to keep concise)
130
+
const sample = formatArgsInline(firstItem, 30);
131
+
return `[${count} items] ${sample}`;
132
+
}
133
+
return `[${count} items]`;
134
+
}
135
+
136
// If parsed successfully and it's an object, format as key=value pairs
137
+
if (typeof parsed === "object" && parsed !== null) {
138
+
return `(${formatArgsInline(parsed, 50)})`;
139
}
140
141
+
// If it's a primitive, return the original string
142
return returnValue;
143
} catch {
144
// If parsing fails, return as-is (it's a simple string)
···
178
const toolCall = response.tool_call || response.tool_calls;
179
if (toolCall && toolCall.name) {
180
lastToolName = toolCall.name;
181
+
const importantParams = selectImportantParams(toolCall.arguments);
182
+
const formattedArgs = formatArgsInline(importantParams);
183
console.log(`🔧 tool called: ${toolCall.name} (${formattedArgs})`);
184
}
185
} else if (response.message_type === "tool_return_message") {
···
190
const separator = formattedResponse.startsWith("(") ? " " : ": ";
191
const logMessage = `↩️ tool response: ${lastToolName}${separator}${formattedResponse}`;
192
193
+
console.log(truncateString(logMessage, 300));
194
} else if (response.message_type === "usage_statistics") {
195
console.log(`🔢 total steps: ${response.step_count}`);
196
} else if (response.message_type === "hidden_reasoning_message") {
+1
utils/types.ts
+1
utils/types.ts
···
61
timeZone: string;
62
responsiblePartyType: string; // person / organization
63
preserveAgentMemory: boolean; // if true, mount won't update existing memory blocks
64
+
maxThreadPosts: number; // maximum number of posts to include in thread context
65
// set automatically
66
agentBskyDID: string;
67
reflectionEnabled: boolean;