+11
-7
prompts/quotePrompt.ts
+11
-7
prompts/quotePrompt.ts
···
1
1
import { Notification } from "../utils/types.ts";
2
2
import { doesUserFollowTarget } from "../utils/doesUserFollow.ts";
3
3
import { agentContext } from "../utils/agentContext.ts";
4
-
import { getCleanThread } from "../utils/getCleanThread.ts";
4
+
import { getCleanThread, isThreadPost } from "../utils/getCleanThread.ts";
5
5
6
6
export const quotePrompt = async (notification: Notification) => {
7
7
const isUserFollower = await doesUserFollowTarget(
···
60
60
quotes: undefined,
61
61
}];
62
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
+
63
67
return `
64
68
# NOTIFICATION: Someone quoted your post
65
69
···
75
79
76
80
## Your Original Post
77
81
\`\`\`
78
-
${originalThread[originalThread.length - 1].message}
82
+
${lastOriginalPost.message}
79
83
\`\`\`
80
84
81
85
## The Quote Post from @${notification.author.handle}
82
86
\`\`\`
83
-
${quotePostThread[quotePostThread.length - 1].message}
87
+
${lastQuotePost.message}
84
88
\`\`\`
85
89
86
90
## 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
+
• **Likes:** ${lastQuotePost.likes}
92
+
• **Replies:** ${lastQuotePost.replies}
93
+
• **Reposts:** ${lastQuotePost.reposts}
94
+
• **Quotes:** ${lastQuotePost.quotes}
91
95
92
96
${
93
97
originalThread
+1
-5
tasks/checkNotifications.ts
+1
-5
tasks/checkNotifications.ts
···
3
3
claimTaskThread,
4
4
releaseTaskThread,
5
5
} from "../utils/agentContext.ts";
6
-
import {
7
-
msFrom,
8
-
msRandomOffset,
9
-
msUntilNextWakeWindow,
10
-
} from "../utils/time.ts";
6
+
import { msFrom, msUntilNextWakeWindow } from "../utils/time.ts";
11
7
import { bsky } from "../utils/bsky.ts";
12
8
import { processNotification } from "../utils/processNotification.ts";
13
9
+11
utils/agentContext.ts
+11
utils/agentContext.ts
···
242
242
return (value / 100) + 1;
243
243
};
244
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
+
245
255
const getReflectionDelayMinimum = (): number => {
246
256
const value = msFrom.parse(Deno.env.get("REFLECTION_DELAY_MINIMUM"));
247
257
···
575
585
timeZone: getTimeZone(),
576
586
responsiblePartyType: getResponsiblePartyType(),
577
587
preserveAgentMemory: getPreserveMemoryBlocks(),
588
+
maxThreadPosts: getMaxThreadPosts(),
578
589
reflectionEnabled: setReflectionEnabled(),
579
590
proactiveEnabled: setProactiveEnabled(),
580
591
sleepEnabled: setSleepEnabled(),
+54
-3
utils/getCleanThread.ts
+54
-3
utils/getCleanThread.ts
···
1
1
import { bsky } from "./bsky.ts";
2
+
import { agentContext } from "./agentContext.ts";
2
3
3
4
type threadPost = {
4
5
authorHandle: string;
···
13
14
quotes: number;
14
15
};
15
16
16
-
export const getCleanThread = async (uri: string): Promise<threadPost[]> => {
17
+
type threadTruncationIndicator = {
18
+
message: string;
19
+
};
20
+
21
+
type threadItem = threadPost | threadTruncationIndicator;
22
+
23
+
export const getCleanThread = async (uri: string): Promise<threadItem[]> => {
17
24
const res = await bsky.getPostThread({ uri: uri });
18
25
const { thread } = res.data;
19
26
20
-
const postsThread: threadPost[] = [];
27
+
const postsThread: threadItem[] = [];
21
28
22
29
// Type guard to check if thread is a ThreadViewPost
23
30
if (thread && "post" in thread) {
···
37
44
// Now traverse the parent chain
38
45
if ("parent" in thread) {
39
46
let current = thread.parent;
47
+
let postCount = 1; // Start at 1 for the main post
48
+
let wasTruncated = false;
40
49
41
-
while (current && "post" in current) {
50
+
// Collect up to configured limit of posts
51
+
while (current && "post" in current && postCount < agentContext.maxThreadPosts) {
42
52
postsThread.push({
43
53
authorHandle: `@${current.post.author.handle}`,
44
54
message: (current.post.record as { text: string }).text,
···
51
61
likes: current.post.likeCount ?? 0,
52
62
quotes: current.post.quoteCount ?? 0,
53
63
});
64
+
postCount++;
54
65
current = "parent" in current ? current.parent : undefined;
55
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
56
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
+
}
57
104
}
58
105
}
59
106
60
107
return postsThread;
61
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
1
import Letta from "@letta-ai/letta-client";
2
2
import { agentContext } from "./agentContext.ts";
3
3
// Helper function to format tool arguments as inline key-value pairs
4
-
const formatArgsInline = (args: unknown, maxValueLength = 30): string => {
4
+
const formatArgsInline = (args: unknown, maxValueLength = 50): string => {
5
5
try {
6
6
const parsed = typeof args === "string" ? JSON.parse(args) : args;
7
7
if (typeof parsed !== "object" || parsed === null) {
···
12
12
let valueStr = typeof value === "object"
13
13
? JSON.stringify(value)
14
14
: String(value);
15
-
// Truncate long values
15
+
// Truncate long values at word boundaries
16
16
if (valueStr.length > maxValueLength) {
17
-
valueStr = valueStr.slice(0, 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 + "...";
18
23
}
19
24
return `${key}=${valueStr}`;
20
25
})
···
65
70
}
66
71
};
67
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
+
68
104
// Helper function to format tool response for logging
69
105
const formatToolResponse = (returnValue: string): string => {
70
106
try {
···
83
119
parsed = JSON.parse(pythonToJson);
84
120
}
85
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
+
86
136
// 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)})`;
137
+
if (typeof parsed === "object" && parsed !== null) {
138
+
return `(${formatArgsInline(parsed, 50)})`;
89
139
}
90
140
91
-
// If it's an array or primitive, return the original string
141
+
// If it's a primitive, return the original string
92
142
return returnValue;
93
143
} catch {
94
144
// If parsing fails, return as-is (it's a simple string)
···
128
178
const toolCall = response.tool_call || response.tool_calls;
129
179
if (toolCall && toolCall.name) {
130
180
lastToolName = toolCall.name;
131
-
const formattedArgs = formatArgsInline(toolCall.arguments);
181
+
const importantParams = selectImportantParams(toolCall.arguments);
182
+
const formattedArgs = formatArgsInline(importantParams);
132
183
console.log(`🔧 tool called: ${toolCall.name} (${formattedArgs})`);
133
184
}
134
185
} else if (response.message_type === "tool_return_message") {
···
139
190
const separator = formattedResponse.startsWith("(") ? " " : ": ";
140
191
const logMessage = `↩️ tool response: ${lastToolName}${separator}${formattedResponse}`;
141
192
142
-
console.log(truncateString(logMessage, 80));
193
+
console.log(truncateString(logMessage, 300));
143
194
} else if (response.message_type === "usage_statistics") {
144
195
console.log(`🔢 total steps: ${response.step_count}`);
145
196
} else if (response.message_type === "hidden_reasoning_message") {
+1
utils/types.ts
+1
utils/types.ts
···
61
61
timeZone: string;
62
62
responsiblePartyType: string; // person / organization
63
63
preserveAgentMemory: boolean; // if true, mount won't update existing memory blocks
64
+
maxThreadPosts: number; // maximum number of posts to include in thread context
64
65
// set automatically
65
66
agentBskyDID: string;
66
67
reflectionEnabled: boolean;