+1
.env.example
+1
.env.example
+1
README.md
+1
README.md
···
70
70
- **`RESPONSIBLE_PARTY_BSKY`**: the DID or bluesky handle of the responsible party
71
71
- **`EXTERNAL_SERVICES`**: a comma-separated list of external tools and services your agent relies on outside of Bluesky (e.g., "Letta, Railway, Google Gemini 2.5-pro"). This information is added to your agent's autonomy declaration record on the PDS and included in the agent's memory for transparency.
72
72
- **`PRESERVE_MEMORY_BLOCKS`**: a boolean for controlling if your agent's memory blocks can be overridden if you run `deno task mount` more than once. Setting this value to **`true`** will allow your agent's version of those memory blocks to persist if they already exist. This is false by default.
73
+
- **`MAX_THREAD_POSTS`**: maximum number of posts to include when fetching thread context (5-250). When a thread exceeds this limit, it will include the root post, a truncation indicator, and the most recent N posts. Default: 25
+2
-1
deno.json
+2
-1
deno.json
···
3
3
"config": "deno run --allow-read --allow-write setup.ts",
4
4
"mount": "deno run --allow-net --allow-env --allow-read --env mount.ts",
5
5
"watch": "deno run --allow-net --allow-env --env --watch main.ts",
6
-
"start": "deno run --allow-net --allow-env --env main.ts"
6
+
"start": "deno run --allow-net --allow-env --env main.ts",
7
+
"test": "deno test"
7
8
},
8
9
"imports": {
9
10
"@std/assert": "jsr:@std/assert@1",
+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
+7
-6
tasks/checkNotifications.ts
+7
-6
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
···
36
32
(delay / 1000 / 60 / 60).toFixed(2)
37
33
} hours from nowโฆ`,
38
34
);
35
+
agentContext.notifDelayCurrent = agentContext.notifDelayMinimum;
39
36
releaseTaskThread();
40
37
return;
41
38
}
···
78
75
// marks all notifications that were processed as seen
79
76
// based on time from when retrieved instead of finished
80
77
await bsky.updateSeenNotifications(startedProcessingTime);
81
-
78
+
console.log(
79
+
`๐น done processing ${unreadNotifications.length} notification${
80
+
unreadNotifications.length > 1 ? "s" : ""
81
+
}โฆ`,
82
+
);
82
83
// increases counter for notification processing session
83
84
agentContext.processingCount++;
84
85
} else {
+2
-1
tasks/logTasks.ts
+2
-1
tasks/logTasks.ts
···
86
86
}
87
87
88
88
const message = actions.join(", ");
89
+
89
90
console.log(
90
-
`๐น ${message}. uptime: ${uptimeFormatted}. next log in ${nextCheckMinutes} minutes`,
91
+
`๐น ${message}. total notifications: ${agentContext.notifCount}. uptime: ${uptimeFormatted}. next log in ${nextCheckMinutes} minutes`,
91
92
);
92
93
}
93
94
+2
-1
tasks/sendSleepMessage.ts
+2
-1
tasks/sendSleepMessage.ts
···
2
2
agentContext,
3
3
claimTaskThread,
4
4
releaseTaskThread,
5
+
isAgentAsleep,
5
6
} from "../utils/agentContext.ts";
6
7
import {
7
8
getNow,
···
35
36
36
37
const now = getNow();
37
38
38
-
if (now.hour >= agentContext.sleepTime) {
39
+
if (isAgentAsleep(now.hour)) {
39
40
console.log(`๐น attempting to wind down ${agentContext.agentBskyName}`);
40
41
} else {
41
42
const delay = msUntilDailyWindow(
+2
-1
tasks/sendWakeMessage.ts
+2
-1
tasks/sendWakeMessage.ts
···
2
2
agentContext,
3
3
claimTaskThread,
4
4
releaseTaskThread,
5
+
isAgentAwake,
5
6
} from "../utils/agentContext.ts";
6
7
import { getNow, msFrom, msRandomOffset } from "../utils/time.ts";
7
8
import { messageAgent } from "../utils/messageAgent.ts";
···
31
32
32
33
const now = getNow();
33
34
34
-
if (now.hour >= agentContext.wakeTime && now.hour < agentContext.sleepTime) {
35
+
if (isAgentAwake(now.hour)) {
35
36
console.log(`๐น attempting to wake up ${agentContext.agentBskyName}`);
36
37
} else {
37
38
const delay = msUntilDailyWindow(
+175
utils/agentContext.test.ts
+175
utils/agentContext.test.ts
···
1
+
import { assertEquals } from "@std/assert";
2
+
import { isAgentAwake, isAgentAsleep } from "./sleepWakeHelpers.ts";
3
+
4
+
// Normal Schedule Tests (wake=8, sleep=22)
5
+
// Agent should be awake from 8am to 10pm
6
+
7
+
Deno.test("Normal schedule - should be asleep before wake time (7am)", () => {
8
+
assertEquals(isAgentAwake(7, 8, 22), false);
9
+
assertEquals(isAgentAsleep(7, 8, 22), true);
10
+
});
11
+
12
+
Deno.test("Normal schedule - should be awake at wake time (8am)", () => {
13
+
assertEquals(isAgentAwake(8, 8, 22), true);
14
+
assertEquals(isAgentAsleep(8, 8, 22), false);
15
+
});
16
+
17
+
Deno.test("Normal schedule - should be awake during day (12pm)", () => {
18
+
assertEquals(isAgentAwake(12, 8, 22), true);
19
+
assertEquals(isAgentAsleep(12, 8, 22), false);
20
+
});
21
+
22
+
Deno.test("Normal schedule - should be awake before sleep time (9pm)", () => {
23
+
assertEquals(isAgentAwake(21, 8, 22), true);
24
+
assertEquals(isAgentAsleep(21, 8, 22), false);
25
+
});
26
+
27
+
Deno.test("Normal schedule - should be asleep at sleep time (10pm)", () => {
28
+
assertEquals(isAgentAwake(22, 8, 22), false);
29
+
assertEquals(isAgentAsleep(22, 8, 22), true);
30
+
});
31
+
32
+
Deno.test("Normal schedule - should be asleep late night (11pm)", () => {
33
+
assertEquals(isAgentAwake(23, 8, 22), false);
34
+
assertEquals(isAgentAsleep(23, 8, 22), true);
35
+
});
36
+
37
+
Deno.test("Normal schedule - should be asleep at midnight", () => {
38
+
assertEquals(isAgentAwake(0, 8, 22), false);
39
+
assertEquals(isAgentAsleep(0, 8, 22), true);
40
+
});
41
+
42
+
// Cross-Midnight Schedule Tests (wake=9, sleep=2)
43
+
// Agent should be awake from 9am to 2am (next day)
44
+
45
+
Deno.test("Cross-midnight schedule - should be awake at midnight", () => {
46
+
assertEquals(isAgentAwake(0, 9, 2), true);
47
+
assertEquals(isAgentAsleep(0, 9, 2), false);
48
+
});
49
+
50
+
Deno.test("Cross-midnight schedule - should be awake late night (1am)", () => {
51
+
assertEquals(isAgentAwake(1, 9, 2), true);
52
+
assertEquals(isAgentAsleep(1, 9, 2), false);
53
+
});
54
+
55
+
Deno.test("Cross-midnight schedule - should be asleep at sleep time (2am)", () => {
56
+
assertEquals(isAgentAwake(2, 9, 2), false);
57
+
assertEquals(isAgentAsleep(2, 9, 2), true);
58
+
});
59
+
60
+
Deno.test("Cross-midnight schedule - should be asleep early morning (3am)", () => {
61
+
assertEquals(isAgentAwake(3, 9, 2), false);
62
+
assertEquals(isAgentAsleep(3, 9, 2), true);
63
+
});
64
+
65
+
Deno.test("Cross-midnight schedule - should be asleep before wake (8am)", () => {
66
+
assertEquals(isAgentAwake(8, 9, 2), false);
67
+
assertEquals(isAgentAsleep(8, 9, 2), true);
68
+
});
69
+
70
+
Deno.test("Cross-midnight schedule - should be awake at wake time (9am)", () => {
71
+
assertEquals(isAgentAwake(9, 9, 2), true);
72
+
assertEquals(isAgentAsleep(9, 9, 2), false);
73
+
});
74
+
75
+
Deno.test("Cross-midnight schedule - should be awake during day (12pm)", () => {
76
+
assertEquals(isAgentAwake(12, 9, 2), true);
77
+
assertEquals(isAgentAsleep(12, 9, 2), false);
78
+
});
79
+
80
+
Deno.test("Cross-midnight schedule - should be awake late night (11pm)", () => {
81
+
assertEquals(isAgentAwake(23, 9, 2), true);
82
+
assertEquals(isAgentAsleep(23, 9, 2), false);
83
+
});
84
+
85
+
// Edge Case Tests
86
+
87
+
Deno.test("Edge case - equal wake/sleep times (midnight) should be asleep", () => {
88
+
// When wake == sleep, the agent should be asleep at all hours
89
+
assertEquals(isAgentAwake(0, 0, 0), false);
90
+
assertEquals(isAgentAsleep(0, 0, 0), true);
91
+
assertEquals(isAgentAwake(12, 0, 0), false);
92
+
assertEquals(isAgentAsleep(12, 0, 0), true);
93
+
});
94
+
95
+
Deno.test("Edge case - nearly 24 hours awake (wake=1, sleep=0)", () => {
96
+
// Asleep only from midnight to 1am
97
+
assertEquals(isAgentAwake(0, 1, 0), false);
98
+
assertEquals(isAgentAsleep(0, 1, 0), true);
99
+
assertEquals(isAgentAwake(1, 1, 0), true);
100
+
assertEquals(isAgentAsleep(1, 1, 0), false);
101
+
assertEquals(isAgentAwake(23, 1, 0), true);
102
+
assertEquals(isAgentAsleep(23, 1, 0), false);
103
+
});
104
+
105
+
Deno.test("Edge case - nearly 24 hours asleep (wake=0, sleep=23)", () => {
106
+
// Awake only from midnight to 11pm
107
+
assertEquals(isAgentAwake(0, 0, 23), true);
108
+
assertEquals(isAgentAsleep(0, 0, 23), false);
109
+
assertEquals(isAgentAwake(22, 0, 23), true);
110
+
assertEquals(isAgentAsleep(22, 0, 23), false);
111
+
assertEquals(isAgentAwake(23, 0, 23), false);
112
+
assertEquals(isAgentAsleep(23, 0, 23), true);
113
+
});
114
+
115
+
Deno.test("Edge case - adjacent hours (wake=10, sleep=11)", () => {
116
+
// Awake only from 10am to 11am
117
+
assertEquals(isAgentAwake(9, 10, 11), false);
118
+
assertEquals(isAgentAsleep(9, 10, 11), true);
119
+
assertEquals(isAgentAwake(10, 10, 11), true);
120
+
assertEquals(isAgentAsleep(10, 10, 11), false);
121
+
assertEquals(isAgentAwake(11, 10, 11), false);
122
+
assertEquals(isAgentAsleep(11, 10, 11), true);
123
+
assertEquals(isAgentAwake(12, 10, 11), false);
124
+
assertEquals(isAgentAsleep(12, 10, 11), true);
125
+
});
126
+
127
+
// Inverse Relationship Tests
128
+
129
+
Deno.test("Inverse relationship - awake and asleep are always opposite (normal schedule)", () => {
130
+
const wakeTime = 8;
131
+
const sleepTime = 22;
132
+
133
+
// Test all 24 hours
134
+
for (let hour = 0; hour < 24; hour++) {
135
+
const awake = isAgentAwake(hour, wakeTime, sleepTime);
136
+
const asleep = isAgentAsleep(hour, wakeTime, sleepTime);
137
+
assertEquals(
138
+
awake,
139
+
!asleep,
140
+
`Hour ${hour}: awake=${awake}, asleep=${asleep} should be opposite`,
141
+
);
142
+
}
143
+
});
144
+
145
+
Deno.test("Inverse relationship - awake and asleep are always opposite (cross-midnight)", () => {
146
+
const wakeTime = 9;
147
+
const sleepTime = 2;
148
+
149
+
// Test all 24 hours
150
+
for (let hour = 0; hour < 24; hour++) {
151
+
const awake = isAgentAwake(hour, wakeTime, sleepTime);
152
+
const asleep = isAgentAsleep(hour, wakeTime, sleepTime);
153
+
assertEquals(
154
+
awake,
155
+
!asleep,
156
+
`Hour ${hour}: awake=${awake}, asleep=${asleep} should be opposite`,
157
+
);
158
+
}
159
+
});
160
+
161
+
Deno.test("Inverse relationship - awake and asleep are always opposite (edge case)", () => {
162
+
const wakeTime = 23;
163
+
const sleepTime = 1;
164
+
165
+
// Test all 24 hours
166
+
for (let hour = 0; hour < 24; hour++) {
167
+
const awake = isAgentAwake(hour, wakeTime, sleepTime);
168
+
const asleep = isAgentAsleep(hour, wakeTime, sleepTime);
169
+
assertEquals(
170
+
awake,
171
+
!asleep,
172
+
`Hour ${hour}: awake=${awake}, asleep=${asleep} should be opposite`,
173
+
);
174
+
}
175
+
});
+24
utils/agentContext.ts
+24
utils/agentContext.ts
···
14
14
} from "./const.ts";
15
15
import { msFrom } from "./time.ts";
16
16
import { bsky } from "./bsky.ts";
17
+
import {
18
+
isAgentAsleep as checkIsAsleep,
19
+
isAgentAwake as checkIsAwake,
20
+
} from "./sleepWakeHelpers.ts";
17
21
18
22
export const getLettaApiKey = (): string => {
19
23
const value = Deno.env.get("LETTA_API_KEY")?.trim();
···
242
246
return (value / 100) + 1;
243
247
};
244
248
249
+
const getMaxThreadPosts = (): number => {
250
+
const value = Number(Deno.env.get("MAX_THREAD_POSTS"));
251
+
252
+
if (isNaN(value) || value < 5 || value > 250) {
253
+
return 25;
254
+
}
255
+
256
+
return Math.round(value);
257
+
};
258
+
245
259
const getReflectionDelayMinimum = (): number => {
246
260
const value = msFrom.parse(Deno.env.get("REFLECTION_DELAY_MINIMUM"));
247
261
···
552
566
mentionCount: 0,
553
567
replyCount: 0,
554
568
quoteCount: 0,
569
+
notifCount: 0,
555
570
// required with manual variables
556
571
lettaProjectIdentifier: getLettaProjectID(),
557
572
agentBskyHandle: getAgentBskyHandle(),
···
575
590
timeZone: getTimeZone(),
576
591
responsiblePartyType: getResponsiblePartyType(),
577
592
preserveAgentMemory: getPreserveMemoryBlocks(),
593
+
maxThreadPosts: getMaxThreadPosts(),
578
594
reflectionEnabled: setReflectionEnabled(),
579
595
proactiveEnabled: setProactiveEnabled(),
580
596
sleepEnabled: setSleepEnabled(),
···
626
642
agentContext.replyCount = 0;
627
643
agentContext.quoteCount = 0;
628
644
};
645
+
646
+
export const isAgentAwake = (hour: number): boolean => {
647
+
return checkIsAwake(hour, agentContext.wakeTime, agentContext.sleepTime);
648
+
};
649
+
650
+
export const isAgentAsleep = (hour: number): boolean => {
651
+
return checkIsAsleep(hour, agentContext.wakeTime, agentContext.sleepTime);
652
+
};
+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
+
};
+137
-15
utils/messageAgent.ts
+137
-15
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): 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) {
···
9
9
}
10
10
return Object.entries(parsed)
11
11
.map(([key, value]) => {
12
-
const valueStr = typeof value === "object"
12
+
let valueStr = typeof value === "object"
13
13
? JSON.stringify(value)
14
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
+
}
15
24
return `${key}=${valueStr}`;
16
25
})
17
26
.join(", ");
···
28
37
return `${str.slice(0, maxLength)}... (truncated, ${str.length} total chars)`;
29
38
};
30
39
40
+
// Helper function to extract tool return value from wrapper structure
41
+
const extractToolReturn = (toolReturns: unknown): string => {
42
+
try {
43
+
// If it's already a string, return it
44
+
if (typeof toolReturns === "string") {
45
+
return toolReturns;
46
+
}
47
+
48
+
// If it's an array, extract the tool_return from first element
49
+
if (Array.isArray(toolReturns) && toolReturns.length > 0) {
50
+
const firstReturn = toolReturns[0];
51
+
if (
52
+
typeof firstReturn === "object" &&
53
+
firstReturn !== null &&
54
+
"tool_return" in firstReturn
55
+
) {
56
+
const toolReturn = firstReturn.tool_return;
57
+
// If tool_return is already a string, return it
58
+
if (typeof toolReturn === "string") {
59
+
return toolReturn;
60
+
}
61
+
// Otherwise stringify it
62
+
return JSON.stringify(toolReturn);
63
+
}
64
+
}
65
+
66
+
// Fallback: return stringified version of the whole thing
67
+
return JSON.stringify(toolReturns);
68
+
} catch {
69
+
return String(toolReturns);
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 {
107
+
// Try to parse as JSON - handle both JSON and Python dict syntax
108
+
let parsed;
109
+
try {
110
+
// First try standard JSON
111
+
parsed = JSON.parse(returnValue);
112
+
} catch {
113
+
// Try to parse Python-style dict (with single quotes and None/True/False)
114
+
const pythonToJson = returnValue
115
+
.replace(/'/g, '"')
116
+
.replace(/\bNone\b/g, "null")
117
+
.replace(/\bTrue\b/g, "true")
118
+
.replace(/\bFalse\b/g, "false");
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)
145
+
return returnValue;
146
+
}
147
+
};
148
+
31
149
export const client = new Letta({
32
150
apiKey: Deno.env.get("LETTA_API_KEY"),
33
151
// @ts-ignore: Letta SDK type definition might be slightly off or expecting different casing
···
48
166
stream_tokens: true,
49
167
});
50
168
169
+
let lastToolName = "";
170
+
51
171
for await (const response of reachAgent) {
52
172
if (response.message_type === "reasoning_message") {
53
173
// console.log(`๐ญ reasoningโฆ`);
54
174
} else if (response.message_type === "assistant_message") {
55
175
console.log(`๐ฌ ${agentContext.agentBskyName}: ${response.content}`);
56
176
} else if (response.message_type === "tool_call_message") {
57
-
if (
58
-
Array.isArray(response.tool_calls) && response.tool_calls.length > 0
59
-
) {
60
-
const toolCall = response.tool_calls[0];
61
-
const formattedArgs = formatArgsInline(toolCall.arguments);
62
-
console.log(
63
-
`๐๏ธ tool called: ${toolCall.name} with args: ${formattedArgs}`,
64
-
);
177
+
// Use tool_call (singular) or tool_calls (both are objects, not arrays)
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})`);
65
184
}
66
185
} else if (response.message_type === "tool_return_message") {
67
-
const toolReturn = response.tool_returns;
68
-
const returnStr = typeof toolReturn === "string"
69
-
? toolReturn
70
-
: JSON.stringify(toolReturn);
71
-
console.log(`๐ง tool response: ${truncateString(returnStr)}`);
186
+
const extractedReturn = extractToolReturn(response.tool_returns);
187
+
const formattedResponse = formatToolResponse(extractedReturn);
188
+
189
+
// Determine separator based on format
190
+
const separator = formattedResponse.startsWith("(") ? " " : ": ";
191
+
const logMessage = `โฉ๏ธ tool response: ${lastToolName}${separator}${formattedResponse}`;
192
+
193
+
console.log(truncateString(logMessage, 300));
72
194
} else if (response.message_type === "usage_statistics") {
73
195
console.log(`๐ข total steps: ${response.step_count}`);
74
196
} else if (response.message_type === "hidden_reasoning_message") {
+1
utils/processNotification.ts
+1
utils/processNotification.ts
+40
utils/sleepWakeHelpers.ts
+40
utils/sleepWakeHelpers.ts
···
1
+
/**
2
+
* Helper functions for determining agent sleep/wake status
3
+
* These functions handle both normal schedules and cross-midnight schedules
4
+
*/
5
+
6
+
/**
7
+
* Core logic: Determines if agent should be asleep at given hour
8
+
*/
9
+
export const isAgentAsleep = (
10
+
hour: number,
11
+
wakeTime: number,
12
+
sleepTime: number,
13
+
): boolean => {
14
+
// Edge case: if wake == sleep, agent has zero awake time (always asleep)
15
+
if (wakeTime === sleepTime) {
16
+
return true;
17
+
}
18
+
19
+
// If sleepTime > wakeTime: normal same-day schedule (e.g., wake=8, sleep=22)
20
+
// Agent is asleep from sleepTime until midnight, OR from midnight until wakeTime
21
+
if (sleepTime > wakeTime) {
22
+
return hour >= sleepTime || hour < wakeTime;
23
+
}
24
+
25
+
// If sleepTime < wakeTime: schedule crosses midnight (e.g., wake=9, sleep=2)
26
+
// Agent is asleep from sleepTime until wakeTime (same day)
27
+
return hour >= sleepTime && hour < wakeTime;
28
+
};
29
+
30
+
/**
31
+
* Semantic wrapper: Determines if agent should be awake at given hour
32
+
* Simply the inverse of isAgentAsleep
33
+
*/
34
+
export const isAgentAwake = (
35
+
hour: number,
36
+
wakeTime: number,
37
+
sleepTime: number,
38
+
): boolean => {
39
+
return !isAgentAsleep(hour, wakeTime, sleepTime);
40
+
};
+9
-2
utils/types.ts
+9
-2
utils/types.ts
···
8
8
} from "./const.ts";
9
9
import type {
10
10
AutomationLevel,
11
-
ResponsiblePartyType,
12
11
AutonomyDeclaration,
13
12
ResponsibleParty,
13
+
ResponsiblePartyType,
14
14
} from "@voyager/autonomy-lexicon";
15
15
16
16
export type Notification = AppBskyNotificationListNotifications.Notification;
17
17
18
18
// Re-export types from autonomy-lexicon package
19
-
export type { AutomationLevel, ResponsiblePartyType, AutonomyDeclaration, ResponsibleParty };
19
+
export type {
20
+
AutomationLevel,
21
+
AutonomyDeclaration,
22
+
ResponsibleParty,
23
+
ResponsiblePartyType,
24
+
};
20
25
21
26
export type notifType = typeof validNotifTypes[number];
22
27
···
38
43
mentionCount: number;
39
44
replyCount: number;
40
45
quoteCount: number;
46
+
notifCount: number;
41
47
// required manual variables
42
48
lettaProjectIdentifier: string;
43
49
agentBskyHandle: string;
···
61
67
timeZone: string;
62
68
responsiblePartyType: string; // person / organization
63
69
preserveAgentMemory: boolean; // if true, mount won't update existing memory blocks
70
+
maxThreadPosts: number; // maximum number of posts to include in thread context
64
71
// set automatically
65
72
agentBskyDID: string;
66
73
reflectionEnabled: boolean;