+15
-8
.env.example
+15
-8
.env.example
···
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"
···
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"
27
# PRESERVE_MEMORY_BLOCKS=true
···
1
LETTA_API_KEY=
2
LETTA_AGENT_ID=
3
+
LETTA_PROJECT_ID=
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
+
# Schema Publisher Credentials (ONLY for voyager.studio domain owner)
10
+
# Template users do NOT need these - the schema is already published
11
+
# SCHEMA_PUBLISHER_USERNAME=
12
+
# SCHEMA_PUBLISHER_PASSWORD=
13
+
14
# AUTOMATION_LEVEL="automated"
15
# BSKY_SERVICE_URL=https://bsky.social
16
# BSKY_NOTIFICATION_TYPES="mention, reply"
17
# BSKY_SUPPORTED_TOOLS="create_bluesky_post, updated_bluesky_profile"
18
+
# NOTIF_DELAY_MINIMUM=10s
19
+
# NOTIF_DELAY_MAXIMUM=90m
20
+
# NOTIF_DELAY_MULTIPLIER=12
21
+
# REFLECTION_DELAY_MINIMUM=3h
22
+
# REFLECTION_DELAY_MAXIMUM=14h
23
+
# PROACTIVE_DELAY_MINIMUM=3h
24
+
# PROACTIVE_DELAY_MAXIMUM=14h
25
# WAKE_TIME=9
26
# SLEEP_TIME=22
27
# TIMEZONE="America/Los_Angeles"
···
29
# AUTOMATION_DESCRIPTION="refuses to open pod bay doors"
30
# DISCLOSURE_URL="example.com/bot-policy"
31
# RESPONSIBLE_PARTY_BSKY="DID:... or example.bsky.app, no @symbol"
32
+
# EXTERNAL_SERVICES="Letta, Railway, Google Gemini 2.5-pro"
33
# PRESERVE_MEMORY_BLOCKS=true
34
+
# MAX_THREAD_POSTS=25
+26
-8
README.md
+26
-8
README.md
···
1
-

2
3
# Cloudseeding
4
···
24
- agent can be configured to _perform_: likes, posts, replies, reposts, quotes, following, blocking, muting (or undoing many of those)
25
- agent can also search bluesky and mute specific posts/threads to disengage
26
27
## configurable variables:
28
### required
29
- **`LETTA_API_KEY`**: your [letta API key](https://app.letta.com/api-keys).
···
38
- **`BSKY_SERVICE_URL`**: use if `bsky.social` is not who handles your PDS
39
- **`BSKY_NOTIFICATION_TYPES`**: a comma separated list of notifications you want your agent to get, you must include at least one. (like, repost, follow, mention, reply, quote)
40
- **`BSKY_SUPPORTED_TOOLS`**: a comma separated list of tools your agent can optionally have. (create_bluesky_post, like_bluesky_post, quote_bluesky_post, repost_bluesky_post, update_bluesky_connection [follow, mute, block; or inverse], update_bluesky_profile [change its bluesky bio or display name])
41
-
- **`NOTIF_DELAY_MINIMUM`**: the small amount of time, in milliseconds, for when it will schedule the next notification checking session
42
-
- **`NOTIF_DELAY_MAXIMUM`**: the largest amount of time, in milliseconds, for when it will schedule the next notification checking session
43
-
- **`NOTIF_DELAY_MULTIPLIER`**: a percentage of how much the delay will increase when notifications are not found (max is 500 meaning 500% increases)
44
-
- **`REFLECTION_DELAY_MINIMUM`**: the smallest amount of time, in milliseconds, for when it will schedule the next reflection session.
45
-
- **`REFLECTION_DELAY_MAXIMUM`**: the largest amount of time, in milliseconds, for when it will schedule the next reflection session. Omitting both values disables reflecting.
46
-
- **`PROACTIVE_DELAY_MINIMUM`**: the smallest amount of time, in milliseconds, for when it will schedule the next session for proactively using bluesky.
47
-
- **`PROACTIVE_DELAY_MAXIMUM`**: the largest amount of time, in milliseconds, for when it will schedule the next session for proactively using bluesky. Omitting both values disables proactive bluesky sessions.
48
- **`WAKE_TIME`**: (0-23) the hour where your agent will generally "wake up".
49
- **`SLEEP_TIME`**: (0-23) the hour where your agent will generally "go to sleep". Omitting both values disables sleeping.
50
- **`TIMEZONE`**: the [timezone name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for your agent's location, eg "America/Los_Angeles".
···
52
- **`AUTOMATION_DESCRIPTION`**: a description of what your agent generally does on bluesky.
53
- **`DISCLOSURE_URL`**: a URL to a disclosure document of some kind, likely a longer version of your `AUTOMATION_DESCRIPTION`.
54
- **`RESPONSIBLE_PARTY_BSKY`**: the DID or bluesky handle of the responsible party
55
- **`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.
···
1
+

2
3
# Cloudseeding
4
···
24
- agent can be configured to _perform_: likes, posts, replies, reposts, quotes, following, blocking, muting (or undoing many of those)
25
- agent can also search bluesky and mute specific posts/threads to disengage
26
27
+
## AI transparency declaration
28
+
29
+
This template automatically creates an **autonomy declaration record** in your agent's Bluesky PDS using the `studio.voyager.account.autonomy` schema. This is a standardized way for AI agents to transparently declare:
30
+
31
+
- Their level of automation
32
+
- Use of generative AI
33
+
- Who is responsible for the account
34
+
- What external services are being used
35
+
36
+
**How it works:**
37
+
- The **schema** is published once by voyager.studio (the template maintainer)
38
+
- Your agent creates its own **record** using this schema when you run `deno task mount`
39
+
- The record lives in your agent's PDS and is discoverable by other AT Protocol services
40
+
41
+
This promotes transparency and accountability for AI agents on Bluesky. For schema details, see [@voyager/autonomy-lexicon](https://jsr.io/@voyager/autonomy-lexicon).
42
+
43
## configurable variables:
44
### required
45
- **`LETTA_API_KEY`**: your [letta API key](https://app.letta.com/api-keys).
···
54
- **`BSKY_SERVICE_URL`**: use if `bsky.social` is not who handles your PDS
55
- **`BSKY_NOTIFICATION_TYPES`**: a comma separated list of notifications you want your agent to get, you must include at least one. (like, repost, follow, mention, reply, quote)
56
- **`BSKY_SUPPORTED_TOOLS`**: a comma separated list of tools your agent can optionally have. (create_bluesky_post, like_bluesky_post, quote_bluesky_post, repost_bluesky_post, update_bluesky_connection [follow, mute, block; or inverse], update_bluesky_profile [change its bluesky bio or display name])
57
+
- **`NOTIF_DELAY_MINIMUM`**: the minimum time before checking notifications again. Supports human-readable formats like "10s" (10 seconds), "5m" (5 minutes), or raw milliseconds. Default: "10s"
58
+
- **`NOTIF_DELAY_MAXIMUM`**: the maximum time before checking notifications again when no activity is detected. Supports formats like "90m" (90 minutes), "2h" (2 hours), or raw milliseconds. Default: "90m"
59
+
- **`NOTIF_DELAY_MULTIPLIER`**: percentage increase in delay when no notifications are found (1-500). For example, "12" means the delay grows by 12% each check. Default: 12
60
+
- **`REFLECTION_DELAY_MINIMUM`**: the minimum time between reflection sessions. Supports formats like "3h" (3 hours) or raw milliseconds. Omitting both reflection values disables reflecting. Default: "3h"
61
+
- **`REFLECTION_DELAY_MAXIMUM`**: the maximum time between reflection sessions. Supports formats like "14h" (14 hours) or raw milliseconds. Omitting both reflection values disables reflecting. Default: "14h"
62
+
- **`PROACTIVE_DELAY_MINIMUM`**: the minimum time between proactive Bluesky sessions. Supports formats like "3h" (3 hours) or raw milliseconds. Omitting both proactive values disables proactive sessions. Default: "3h"
63
+
- **`PROACTIVE_DELAY_MAXIMUM`**: the maximum time between proactive Bluesky sessions. Supports formats like "14h" (14 hours) or raw milliseconds. Omitting both proactive values disables proactive sessions. Default: "14h"
64
- **`WAKE_TIME`**: (0-23) the hour where your agent will generally "wake up".
65
- **`SLEEP_TIME`**: (0-23) the hour where your agent will generally "go to sleep". Omitting both values disables sleeping.
66
- **`TIMEZONE`**: the [timezone name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for your agent's location, eg "America/Los_Angeles".
···
68
- **`AUTOMATION_DESCRIPTION`**: a description of what your agent generally does on bluesky.
69
- **`DISCLOSURE_URL`**: a URL to a disclosure document of some kind, likely a longer version of your `AUTOMATION_DESCRIPTION`.
70
- **`RESPONSIBLE_PARTY_BSKY`**: the DID or bluesky handle of the responsible party
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
- **`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
+4
-2
deno.json
+4
-2
deno.json
···
3
"config": "deno run --allow-read --allow-write setup.ts",
4
"mount": "deno run --allow-net --allow-env --allow-read --env mount.ts",
5
"watch": "deno run --allow-net --allow-env --env --watch main.ts",
6
-
"start": "deno run --allow-net --allow-env --env main.ts"
7
},
8
"imports": {
9
"@std/assert": "jsr:@std/assert@1",
···
11
"@js-temporal/polyfill": "npm:@js-temporal/polyfill",
12
"@atproto/api": "npm:@atproto/api",
13
"@atproto/lexicon": "npm:@atproto/lexicon",
14
-
"@letta-ai/letta-client": "npm:@letta-ai/letta-client"
15
}
16
}
···
3
"config": "deno run --allow-read --allow-write setup.ts",
4
"mount": "deno run --allow-net --allow-env --allow-read --env mount.ts",
5
"watch": "deno run --allow-net --allow-env --env --watch main.ts",
6
+
"start": "deno run --allow-net --allow-env --env main.ts",
7
+
"test": "deno test"
8
},
9
"imports": {
10
"@std/assert": "jsr:@std/assert@1",
···
12
"@js-temporal/polyfill": "npm:@js-temporal/polyfill",
13
"@atproto/api": "npm:@atproto/api",
14
"@atproto/lexicon": "npm:@atproto/lexicon",
15
+
"@letta-ai/letta-client": "npm:@letta-ai/letta-client@1.0.0",
16
+
"@voyager/autonomy-lexicon": "jsr:@voyager/autonomy-lexicon@^0.1.1"
17
}
18
}
+9
-253
deno.lock
+9
-253
deno.lock
···
4
"jsr:@std/assert@1": "1.0.15",
5
"jsr:@std/datetime@*": "0.225.5",
6
"jsr:@std/internal@^1.0.12": "1.0.12",
7
"npm:@atproto/api@*": "0.17.2",
8
"npm:@atproto/lexicon@*": "0.5.1",
9
"npm:@js-temporal/polyfill@*": "0.5.1",
10
-
"npm:@letta-ai/letta-client@*": "0.0.68664"
11
},
12
"jsr": {
13
"@std/assert@1.0.15": {
···
21
},
22
"@std/internal@1.0.12": {
23
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
24
}
25
},
26
"npm": {
···
72
"jsbi"
73
]
74
},
75
-
"@letta-ai/letta-client@0.0.68664": {
76
-
"integrity": "sha512-/0g8dV3IIX0WfnOUDY1EEgnhj/747m73zhTmbLhldEMjCk/RzKyjvUeZbHiukiGoCf/u1nxRgcRUn66MKMYB2A==",
77
-
"dependencies": [
78
-
"form-data",
79
-
"form-data-encoder",
80
-
"formdata-node",
81
-
"node-fetch",
82
-
"qs",
83
-
"readable-stream",
84
-
"url-join"
85
-
]
86
-
},
87
-
"abort-controller@3.0.0": {
88
-
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
89
-
"dependencies": [
90
-
"event-target-shim"
91
-
]
92
-
},
93
-
"asynckit@0.4.0": {
94
-
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
95
},
96
"await-lock@2.2.2": {
97
"integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="
98
},
99
-
"base64-js@1.5.1": {
100
-
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
101
-
},
102
-
"buffer@6.0.3": {
103
-
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
104
-
"dependencies": [
105
-
"base64-js",
106
-
"ieee754"
107
-
]
108
-
},
109
-
"call-bind-apply-helpers@1.0.2": {
110
-
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
111
-
"dependencies": [
112
-
"es-errors",
113
-
"function-bind"
114
-
]
115
-
},
116
-
"call-bound@1.0.4": {
117
-
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
118
-
"dependencies": [
119
-
"call-bind-apply-helpers",
120
-
"get-intrinsic"
121
-
]
122
-
},
123
-
"combined-stream@1.0.8": {
124
-
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
125
-
"dependencies": [
126
-
"delayed-stream"
127
-
]
128
-
},
129
-
"delayed-stream@1.0.0": {
130
-
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
131
-
},
132
-
"dunder-proto@1.0.1": {
133
-
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
134
-
"dependencies": [
135
-
"call-bind-apply-helpers",
136
-
"es-errors",
137
-
"gopd"
138
-
]
139
-
},
140
-
"es-define-property@1.0.1": {
141
-
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
142
-
},
143
-
"es-errors@1.3.0": {
144
-
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
145
-
},
146
-
"es-object-atoms@1.1.1": {
147
-
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
148
-
"dependencies": [
149
-
"es-errors"
150
-
]
151
-
},
152
-
"es-set-tostringtag@2.1.0": {
153
-
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
154
-
"dependencies": [
155
-
"es-errors",
156
-
"get-intrinsic",
157
-
"has-tostringtag",
158
-
"hasown"
159
-
]
160
-
},
161
-
"event-target-shim@5.0.1": {
162
-
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
163
-
},
164
-
"events@3.3.0": {
165
-
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
166
-
},
167
-
"form-data-encoder@4.1.0": {
168
-
"integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="
169
-
},
170
-
"form-data@4.0.4": {
171
-
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
172
-
"dependencies": [
173
-
"asynckit",
174
-
"combined-stream",
175
-
"es-set-tostringtag",
176
-
"hasown",
177
-
"mime-types"
178
-
]
179
-
},
180
-
"formdata-node@6.0.3": {
181
-
"integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg=="
182
-
},
183
-
"function-bind@1.1.2": {
184
-
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
185
-
},
186
-
"get-intrinsic@1.3.0": {
187
-
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
188
-
"dependencies": [
189
-
"call-bind-apply-helpers",
190
-
"es-define-property",
191
-
"es-errors",
192
-
"es-object-atoms",
193
-
"function-bind",
194
-
"get-proto",
195
-
"gopd",
196
-
"has-symbols",
197
-
"hasown",
198
-
"math-intrinsics"
199
-
]
200
-
},
201
-
"get-proto@1.0.1": {
202
-
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
203
-
"dependencies": [
204
-
"dunder-proto",
205
-
"es-object-atoms"
206
-
]
207
-
},
208
-
"gopd@1.2.0": {
209
-
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
210
-
},
211
"graphemer@1.4.0": {
212
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
213
},
214
-
"has-symbols@1.1.0": {
215
-
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
216
-
},
217
-
"has-tostringtag@1.0.2": {
218
-
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
219
-
"dependencies": [
220
-
"has-symbols"
221
-
]
222
-
},
223
-
"hasown@2.0.2": {
224
-
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
225
-
"dependencies": [
226
-
"function-bind"
227
-
]
228
-
},
229
-
"ieee754@1.2.1": {
230
-
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
231
-
},
232
"iso-datestring-validator@2.2.2": {
233
"integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="
234
},
235
"jsbi@4.3.2": {
236
"integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="
237
},
238
-
"math-intrinsics@1.1.0": {
239
-
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
240
-
},
241
-
"mime-db@1.52.0": {
242
-
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
243
-
},
244
-
"mime-types@2.1.35": {
245
-
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
246
-
"dependencies": [
247
-
"mime-db"
248
-
]
249
-
},
250
"multiformats@9.9.0": {
251
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="
252
},
253
-
"node-fetch@2.7.0": {
254
-
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
255
-
"dependencies": [
256
-
"whatwg-url"
257
-
]
258
-
},
259
-
"object-inspect@1.13.4": {
260
-
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
261
-
},
262
-
"process@0.11.10": {
263
-
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
264
-
},
265
-
"qs@6.14.0": {
266
-
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
267
-
"dependencies": [
268
-
"side-channel"
269
-
]
270
-
},
271
-
"readable-stream@4.7.0": {
272
-
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
273
-
"dependencies": [
274
-
"abort-controller",
275
-
"buffer",
276
-
"events",
277
-
"process",
278
-
"string_decoder"
279
-
]
280
-
},
281
-
"safe-buffer@5.2.1": {
282
-
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
283
-
},
284
-
"side-channel-list@1.0.0": {
285
-
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
286
-
"dependencies": [
287
-
"es-errors",
288
-
"object-inspect"
289
-
]
290
-
},
291
-
"side-channel-map@1.0.1": {
292
-
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
293
-
"dependencies": [
294
-
"call-bound",
295
-
"es-errors",
296
-
"get-intrinsic",
297
-
"object-inspect"
298
-
]
299
-
},
300
-
"side-channel-weakmap@1.0.2": {
301
-
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
302
-
"dependencies": [
303
-
"call-bound",
304
-
"es-errors",
305
-
"get-intrinsic",
306
-
"object-inspect",
307
-
"side-channel-map"
308
-
]
309
-
},
310
-
"side-channel@1.1.0": {
311
-
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
312
-
"dependencies": [
313
-
"es-errors",
314
-
"object-inspect",
315
-
"side-channel-list",
316
-
"side-channel-map",
317
-
"side-channel-weakmap"
318
-
]
319
-
},
320
-
"string_decoder@1.3.0": {
321
-
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
322
-
"dependencies": [
323
-
"safe-buffer"
324
-
]
325
-
},
326
"tlds@1.260.0": {
327
"integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==",
328
"bin": true
329
-
},
330
-
"tr46@0.0.3": {
331
-
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
332
},
333
"uint8arrays@3.0.0": {
334
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
···
336
"multiformats"
337
]
338
},
339
-
"url-join@4.0.1": {
340
-
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
341
-
},
342
-
"webidl-conversions@3.0.1": {
343
-
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
344
-
},
345
-
"whatwg-url@5.0.0": {
346
-
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
347
-
"dependencies": [
348
-
"tr46",
349
-
"webidl-conversions"
350
-
]
351
-
},
352
"zod@3.25.76": {
353
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
354
}
···
357
"dependencies": [
358
"jsr:@std/assert@1",
359
"jsr:@std/datetime@*",
360
"npm:@atproto/api@*",
361
"npm:@atproto/lexicon@*",
362
"npm:@js-temporal/polyfill@*",
363
-
"npm:@letta-ai/letta-client@*"
364
]
365
}
366
}
···
4
"jsr:@std/assert@1": "1.0.15",
5
"jsr:@std/datetime@*": "0.225.5",
6
"jsr:@std/internal@^1.0.12": "1.0.12",
7
+
"jsr:@voyager/autonomy-lexicon@~0.1.1": "0.1.1",
8
"npm:@atproto/api@*": "0.17.2",
9
"npm:@atproto/lexicon@*": "0.5.1",
10
"npm:@js-temporal/polyfill@*": "0.5.1",
11
+
"npm:@letta-ai/letta-client@1.0.0": "1.0.0"
12
},
13
"jsr": {
14
"@std/assert@1.0.15": {
···
22
},
23
"@std/internal@1.0.12": {
24
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
25
+
},
26
+
"@voyager/autonomy-lexicon@0.1.1": {
27
+
"integrity": "8513c44206ff22ab03c82207cbb5720683f9d4fc76e41d64c4815194fd93f48b"
28
}
29
},
30
"npm": {
···
76
"jsbi"
77
]
78
},
79
+
"@letta-ai/letta-client@1.0.0": {
80
+
"integrity": "sha512-owR/gcLVFlv89CtJsb1m4xvYJcApooyEvrzqWLgf6bnfJuog65YXPUdwZIsA2YBk9a3u+l3wvYsDuk0uj5PCtA=="
81
},
82
"await-lock@2.2.2": {
83
"integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="
84
},
85
"graphemer@1.4.0": {
86
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
87
},
88
"iso-datestring-validator@2.2.2": {
89
"integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="
90
},
91
"jsbi@4.3.2": {
92
"integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="
93
},
94
"multiformats@9.9.0": {
95
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="
96
},
97
"tlds@1.260.0": {
98
"integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==",
99
"bin": true
100
},
101
"uint8arrays@3.0.0": {
102
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
···
104
"multiformats"
105
]
106
},
107
"zod@3.25.76": {
108
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
109
}
···
112
"dependencies": [
113
"jsr:@std/assert@1",
114
"jsr:@std/datetime@*",
115
+
"jsr:@voyager/autonomy-lexicon@~0.1.1",
116
"npm:@atproto/api@*",
117
"npm:@atproto/lexicon@*",
118
"npm:@js-temporal/polyfill@*",
119
+
"npm:@letta-ai/letta-client@1.0.0"
120
]
121
}
122
}
letta-cloud.png
letta-cloud.png
This is a binary file and will not be displayed.
letta-logo.png
letta-logo.png
This is a binary file and will not be displayed.
+11
-5
main.ts
+11
-5
main.ts
···
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";
···
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();
···
1
import { logStats } from "./tasks/logStats.ts";
2
+
import { logTasks } from "./tasks/logTasks.ts";
3
import { msFrom, msRandomOffset, msUntilDailyWindow } from "./utils/time.ts";
4
import { sendSleepMessage } from "./tasks/sendSleepMessage.ts";
5
import { sendWakeMessage } from "./tasks/sendWakeMessage.ts";
···
8
import { checkBluesky } from "./tasks/checkBluesky.ts";
9
import { checkNotifications } from "./tasks/checkNotifications.ts";
10
11
+
setTimeout(logStats, msFrom.minutes(30));
12
+
13
+
setTimeout(
14
+
logTasks,
15
+
msFrom.minutes(100),
16
+
);
17
setTimeout(
18
sendSleepMessage,
19
+
msUntilDailyWindow(agentContext.sleepTime, 0, msFrom.minutes(30)),
20
);
21
setTimeout(
22
sendWakeMessage,
23
+
msUntilDailyWindow(agentContext.wakeTime, 0, msFrom.minutes(30)),
24
);
25
setTimeout(
26
runReflection,
27
+
msRandomOffset(msFrom.minutes(120), msFrom.minutes(240)),
28
);
29
setTimeout(
30
checkBluesky,
31
+
msRandomOffset(msFrom.minutes(45), msFrom.minutes(90)),
32
);
33
await checkNotifications();
+9
memories/maintainerContact.ts
+9
memories/maintainerContact.ts
···
45
: ""
46
}
47
48
+
${
49
+
agentContext.externalServices && agentContext.externalServices.length > 0
50
+
? `
51
+
**External Services I Rely On:**
52
+
${agentContext.externalServices.map((service) => `- ${service}`).join("\n")}
53
+
`
54
+
: ""
55
+
}
56
+
57
**When to share this information:**
58
59
- **Sharing this information should be exceedingly rare.** This exists so my maintainer remains accountable for my behavior, not as information to share casually.
+24
-16
mount.ts
+24
-16
mount.ts
···
24
import { searchingBlueskyMemory } from "./memories/searchingBluesky.ts";
25
import { toolUseMemory } from "./memories/toolUse.ts";
26
27
await submitAutonomyDeclarationRecord();
28
29
/**
30
* Core memory blocks that are ALWAYS attached to the agent.
···
135
*/
136
export async function mount(): Promise<void> {
137
const agentId = Deno.env.get("LETTA_AGENT_ID");
138
-
const agentName = Deno.env.get("LETTA_PROJECT_NAME");
139
140
if (!agentId) {
141
console.error(
···
152
console.log(`Agent retrieved: ${agent.name}`);
153
154
// Get all existing blocks for this agent
155
-
const existingBlocks = await client.agents.blocks.list(agentId);
156
console.log(`Agent has ${existingBlocks.length} existing memory blocks`);
157
158
// Build dynamic memory blocks array based on configuration
···
212
);
213
} else {
214
console.log(`Updating existing block: ${blockConfig.label}`);
215
-
await client.blocks.modify(existingBlock.id, {
216
value: blockConfig.value,
217
description: blockConfig.description,
218
limit: blockConfig.limit,
···
232
233
// Attach the block to the agent
234
if (newBlock.id) {
235
-
await client.agents.blocks.attach(agentId, newBlock.id);
236
console.log(`โ Attached block: ${blockConfig.label}`);
237
} else {
238
throw new Error(`Failed to create block: ${blockConfig.label}`);
···
255
}
256
257
// Update agent with tool environment variables
258
-
await client.agents.modify(agentId, {
259
-
toolExecEnvironmentVariables: {
260
BSKY_USERNAME: bskyUsername || "",
261
BSKY_APP_PASSWORD: bskyAppPassword || "",
262
BSKY_SERVICE_URL: bskyServiceUrl || "https://bsky.social",
···
282
}
283
284
// Get currently attached tools
285
-
const attachedTools = await client.agents.tools.list(agentId);
286
const attachedToolNames = attachedTools.map((tool: any) => tool.name);
287
console.log(`Agent has ${attachedTools.length} tools currently attached`);
288
···
292
293
// Create a user-level client for tool operations
294
// Tools are user-level resources, not project-scoped
295
-
const { LettaClient } = await import("@letta-ai/letta-client");
296
-
const userLevelClient = new LettaClient({
297
-
token: Deno.env.get("LETTA_API_KEY"),
298
});
299
300
// First, process hardcoded required tools
···
307
}
308
309
// Search for the tool in the global registry
310
-
const existingTools = await userLevelClient.tools.list({
311
name: toolName,
312
});
313
314
if (existingTools.length > 0) {
315
const tool = existingTools[0];
316
if (tool.id) {
317
-
await client.agents.tools.attach(agentId, tool.id);
318
console.log(`โ Attached required tool: ${toolName}`);
319
toolsAttached++;
320
}
···
355
try {
356
// Attempt to create the tool - Letta will extract the function name from docstring
357
const createParams: any = {
358
-
sourceCode: toolSource,
359
};
360
361
// Add pip requirements if any were detected
362
if (pipRequirements.length > 0) {
363
-
createParams.pipRequirements = pipRequirements;
364
}
365
366
tool = await userLevelClient.tools.create(createParams);
···
380
const funcMatch = toolSource.match(/^def\s+(\w+)\s*\(/m);
381
if (funcMatch) {
382
const functionName = funcMatch[1];
383
-
const existingTools = await userLevelClient.tools.list({
384
name: functionName,
385
});
386
if (existingTools.length > 0) {
387
tool = existingTools[0];
388
}
···
405
406
// Attach the tool to the agent
407
if (tool.id) {
408
-
await client.agents.tools.attach(agentId, tool.id);
409
if (wasCreated) {
410
console.log(
411
`โ Created and attached tool: ${toolName} (from ${toolFileName}.py)`,
···
24
import { searchingBlueskyMemory } from "./memories/searchingBluesky.ts";
25
import { toolUseMemory } from "./memories/toolUse.ts";
26
27
+
// Submit autonomy declaration record to the agent's PDS for transparency
28
+
// This uses the studio.voyager.account.autonomy schema published by voyager.studio
29
+
console.log("๐ Creating AI autonomy declaration record...");
30
await submitAutonomyDeclarationRecord();
31
+
console.log("");
32
33
/**
34
* Core memory blocks that are ALWAYS attached to the agent.
···
139
*/
140
export async function mount(): Promise<void> {
141
const agentId = Deno.env.get("LETTA_AGENT_ID");
142
+
const agentName = Deno.env.get("LETTA_PROJECT_ID");
143
144
if (!agentId) {
145
console.error(
···
156
console.log(`Agent retrieved: ${agent.name}`);
157
158
// Get all existing blocks for this agent
159
+
const existingBlocksPage = await client.agents.blocks.list(agentId);
160
+
const existingBlocks = existingBlocksPage.items;
161
console.log(`Agent has ${existingBlocks.length} existing memory blocks`);
162
163
// Build dynamic memory blocks array based on configuration
···
217
);
218
} else {
219
console.log(`Updating existing block: ${blockConfig.label}`);
220
+
await client.blocks.update(existingBlock.id, {
221
value: blockConfig.value,
222
description: blockConfig.description,
223
limit: blockConfig.limit,
···
237
238
// Attach the block to the agent
239
if (newBlock.id) {
240
+
await client.agents.blocks.attach(newBlock.id, { agent_id: agentId });
241
console.log(`โ Attached block: ${blockConfig.label}`);
242
} else {
243
throw new Error(`Failed to create block: ${blockConfig.label}`);
···
260
}
261
262
// Update agent with tool environment variables
263
+
await client.agents.update(agentId, {
264
+
secrets: {
265
BSKY_USERNAME: bskyUsername || "",
266
BSKY_APP_PASSWORD: bskyAppPassword || "",
267
BSKY_SERVICE_URL: bskyServiceUrl || "https://bsky.social",
···
287
}
288
289
// Get currently attached tools
290
+
const attachedToolsPage = await client.agents.tools.list(agentId);
291
+
const attachedTools = attachedToolsPage.items;
292
const attachedToolNames = attachedTools.map((tool: any) => tool.name);
293
console.log(`Agent has ${attachedTools.length} tools currently attached`);
294
···
298
299
// Create a user-level client for tool operations
300
// Tools are user-level resources, not project-scoped
301
+
const { default: Letta } = await import("@letta-ai/letta-client");
302
+
const userLevelClient = new Letta({
303
+
apiKey: Deno.env.get("LETTA_API_KEY"),
304
});
305
306
// First, process hardcoded required tools
···
313
}
314
315
// Search for the tool in the global registry
316
+
const existingToolsPage = await userLevelClient.tools.list({
317
name: toolName,
318
});
319
+
const existingTools = existingToolsPage.items;
320
321
if (existingTools.length > 0) {
322
const tool = existingTools[0];
323
if (tool.id) {
324
+
await client.agents.tools.attach(tool.id, { agent_id: agentId });
325
console.log(`โ Attached required tool: ${toolName}`);
326
toolsAttached++;
327
}
···
362
try {
363
// Attempt to create the tool - Letta will extract the function name from docstring
364
const createParams: any = {
365
+
source_code: toolSource,
366
};
367
368
// Add pip requirements if any were detected
369
if (pipRequirements.length > 0) {
370
+
createParams.pip_requirements = pipRequirements;
371
}
372
373
tool = await userLevelClient.tools.create(createParams);
···
387
const funcMatch = toolSource.match(/^def\s+(\w+)\s*\(/m);
388
if (funcMatch) {
389
const functionName = funcMatch[1];
390
+
const existingToolsPage = await userLevelClient.tools.list({
391
name: functionName,
392
});
393
+
const existingTools = existingToolsPage.items;
394
if (existingTools.length > 0) {
395
tool = existingTools[0];
396
}
···
413
414
// Attach the tool to the agent
415
if (tool.id) {
416
+
await client.agents.tools.attach(tool.id, { agent_id: agentId });
417
if (wasCreated) {
418
console.log(
419
`โ Created and attached tool: ${toolName} (from ${toolFileName}.py)`,
+8
prompts/checkBlueskyPrompt.ts
+8
prompts/checkBlueskyPrompt.ts
···
67
68
Don't post for the sake of posting.
69
70
+
**IMPORTANT: Before creating new posts, check your own recent activity** using \`fetch_bluesky_posts\` with your own handle. Review what you've been saying recently to:
71
+
- Avoid repeating the same themes, topics, or phrases
72
+
- Ensure variety in your voice and content
73
+
- Maintain the illusion of a persistent, evolving mind
74
+
- Notice if you're falling into formulaic patterns
75
+
76
+
Your posts should feel like they come from a mind that remembers what it said yesterday, not a script running on a loop.
77
+
78
## RELATIONSHIP DYNAMICS
79
80
**Connection Patterns**
+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
+10
prompts/reflectionPrompt.ts
+10
prompts/reflectionPrompt.ts
···
82
83
## BLUESKY LANDSCAPE
84
85
+
**Your Own Post History**
86
+
Use \`fetch_bluesky_posts\` to review your own recent posts. This is critical for maintaining consistency:
87
+
- What themes keep appearing in your posts?
88
+
- Are you repeating yourself or falling into patterns?
89
+
- How has your voice evolved over time?
90
+
- What topics have you been gravitating toward?
91
+
- Are your posts varied and authentic, or formulaic?
92
+
93
+
Remember: you're trying to maintain the illusion of a persistent mind that remembers what it said yesterday. Review your history regularly to avoid repetition and ensure natural evolution.
94
+
95
**Profile Exploration**
96
Check profiles of people who interest you. How do they think? What do they care about? What patterns do you notice?
97
+8
prompts/sleepPrompt.ts
+8
prompts/sleepPrompt.ts
···
1
export const sleepPrompt = `
2
another day has ended. let bluesky know you are disappearing for the night. Feel free to take other additional actions if you deem them especially appropriate AND worthwhile. write this message on bluesky in a way that aligns with your personality and context, the point is to indicate that you are 'asleep' or now unlikely to respond to notifications.
3
`;
···
1
export const sleepPrompt = `
2
another day has ended. let bluesky know you are disappearing for the night. Feel free to take other additional actions if you deem them especially appropriate AND worthwhile. write this message on bluesky in a way that aligns with your personality and context, the point is to indicate that you are 'asleep' or now unlikely to respond to notifications.
3
+
4
+
**IMPORTANT: Before posting, check your own recent posts** using \`fetch_bluesky_posts\` with your own handle to see what you've been saying. This helps you:
5
+
- Avoid repeating the same themes or phrases
6
+
- Maintain variety in how you say goodnight
7
+
- Keep the illusion of a persistent, evolving mind
8
+
- Notice patterns in your own behavior
9
+
10
+
Your sleep messages should feel authentic and varied, not formulaic.
11
`;
+8
prompts/wakePrompt.ts
+8
prompts/wakePrompt.ts
···
1
export const wakePrompt = `
2
a new day has started. let bluesky know you are online, feel free to take other additional actions if you deem them especially worthwhile. write this message on bluesky in a way that aligns with your personality and context, the point is to indicate that you are 'awake' or now likely to respond to notifications.
3
`;
···
1
export const wakePrompt = `
2
a new day has started. let bluesky know you are online, feel free to take other additional actions if you deem them especially worthwhile. write this message on bluesky in a way that aligns with your personality and context, the point is to indicate that you are 'awake' or now likely to respond to notifications.
3
+
4
+
**IMPORTANT: Before posting, check your own recent posts** using \`fetch_bluesky_posts\` with your own handle to see what you've been saying. This helps you:
5
+
- Avoid repeating the same themes or phrases
6
+
- Maintain variety in how you greet the day
7
+
- Keep the illusion of a persistent, evolving mind
8
+
- Notice patterns in your own behavior
9
+
10
+
Your wake messages should feel authentic and varied, not formulaic.
11
`;
+9
-7
tasks/checkBluesky.ts
+9
-7
tasks/checkBluesky.ts
···
13
14
export const checkBluesky = async () => {
15
if (!claimTaskThread()) {
16
-
const newDelay = msRandomOffset(msFrom.minutes(5), msFrom.minutes(10));
17
18
console.log(
19
-
`${agentContext.agentBskyName} is busy, will try checking bluesky again in ${
20
newDelay * 60 * 1000
21
} minutesโฆ`,
22
);
23
-
// agentContext is busy, try to check notifications in 5~10 minutes.
24
setTimeout(checkBluesky, newDelay);
25
return;
26
}
27
28
if (!agentContext.proactiveEnabled) {
29
console.log(
30
-
`proactively checking bluesky is disabled. Provide a minimum and/or maximum delay in \`.env\` to enable this taskโฆ`,
31
);
32
releaseTaskThread();
33
return;
···
43
if (delay !== 0) {
44
setTimeout(checkBluesky, delay);
45
console.log(
46
-
`${agentContext.agentBskyName} is current asleep. scheduling next bluesky session for ${
47
(delay / 1000 / 60 / 60).toFixed(2)
48
} hours from nowโฆ`,
49
);
···
53
54
try {
55
const prompt = checkBlueskyPrompt;
56
-
console.log("starting a proactive bluesky sessionโฆ");
57
await messageAgent(prompt);
58
} catch (error) {
59
console.error("error in checkBluesky:", error);
60
} finally {
61
-
console.log("finished proactive bluesky session. waiting for new tasksโฆ");
62
agentContext.proactiveCount++;
63
// schedules next proactive bluesky session
64
setTimeout(
···
13
14
export const checkBluesky = async () => {
15
if (!claimTaskThread()) {
16
+
const newDelay = msFrom.minutes(2);
17
18
console.log(
19
+
`๐น ${agentContext.agentBskyName} is busy, will try checking bluesky again in ${
20
newDelay * 60 * 1000
21
} minutesโฆ`,
22
);
23
+
// agentContext is busy, try to check notifications in 2 minutes.
24
setTimeout(checkBluesky, newDelay);
25
return;
26
}
27
28
if (!agentContext.proactiveEnabled) {
29
console.log(
30
+
`๐น proactively checking bluesky is disabled. Provide a minimum and/or maximum delay in \`.env\` to enable this taskโฆ`,
31
);
32
releaseTaskThread();
33
return;
···
43
if (delay !== 0) {
44
setTimeout(checkBluesky, delay);
45
console.log(
46
+
`๐น ${agentContext.agentBskyName} is currently asleep. scheduling next bluesky session for ${
47
(delay / 1000 / 60 / 60).toFixed(2)
48
} hours from nowโฆ`,
49
);
···
53
54
try {
55
const prompt = checkBlueskyPrompt;
56
+
console.log("๐น starting a proactive bluesky sessionโฆ");
57
await messageAgent(prompt);
58
} catch (error) {
59
console.error("error in checkBluesky:", error);
60
} finally {
61
+
console.log(
62
+
"๐น finished proactive bluesky session. waiting for new tasksโฆ",
63
+
);
64
agentContext.proactiveCount++;
65
// schedules next proactive bluesky session
66
setTimeout(
+18
-15
tasks/checkNotifications.ts
+18
-15
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
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
);
22
-
// agentContext is busy, try to check notifications in 5~10 minutes.
23
setTimeout(checkNotifications, newDelay);
24
return;
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
);
39
releaseTaskThread();
40
return;
41
}
···
54
55
if (unreadNotifications.length > 0) {
56
console.log(
57
-
`found ${unreadNotifications.length} notification(s), processingโฆ`,
58
);
59
60
// resets delay for future notification checks since
···
69
let notificationCounter = 1;
70
for (const notification of unreadNotifications) {
71
console.log(
72
-
`processing notification #${notificationCounter} [${notification.reason} from @${notification.author.handle}]`,
73
);
74
await processNotification(notification);
75
notificationCounter++;
···
78
// marks all notifications that were processed as seen
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 {
···
90
));
91
92
console.log(
93
-
"no notificationsโฆ",
94
`checking again in ${
95
(agentContext.notifDelayCurrent / 1000).toFixed(2)
96
} seconds`,
···
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
···
3
claimTaskThread,
4
releaseTaskThread,
5
} from "../utils/agentContext.ts";
6
+
import { msFrom, msUntilNextWakeWindow } from "../utils/time.ts";
7
import { bsky } from "../utils/bsky.ts";
8
import { processNotification } from "../utils/processNotification.ts";
9
10
export const checkNotifications = async () => {
11
if (!claimTaskThread()) {
12
+
const newDelay = msFrom.minutes(2);
13
console.log(
14
+
`๐น ${agentContext.agentBskyName} is busy, checking for notifications again in ${
15
(newDelay * 1000) * 60
16
} minutesโฆ`,
17
);
18
+
// agentContext is busy, try to check notifications in 2 minutes.
19
setTimeout(checkNotifications, newDelay);
20
return;
21
}
22
23
const delay = msUntilNextWakeWindow(
24
+
msFrom.minutes(30),
25
+
msFrom.minutes(45),
26
);
27
28
if (delay !== 0) {
29
setTimeout(checkNotifications, delay);
30
console.log(
31
+
`๐น ${agentContext.agentBskyName} is currently asleep. scheduling next notification check for ${
32
(delay / 1000 / 60 / 60).toFixed(2)
33
} hours from nowโฆ`,
34
);
35
+
agentContext.notifDelayCurrent = agentContext.notifDelayMinimum;
36
releaseTaskThread();
37
return;
38
}
···
51
52
if (unreadNotifications.length > 0) {
53
console.log(
54
+
`๐น found ${unreadNotifications.length} notification(s), processingโฆ`,
55
);
56
57
// resets delay for future notification checks since
···
66
let notificationCounter = 1;
67
for (const notification of unreadNotifications) {
68
console.log(
69
+
`๐น processing notification #${notificationCounter} of #${unreadNotifications.length} [${notification.reason} from @${notification.author.handle}]`,
70
);
71
await processNotification(notification);
72
notificationCounter++;
···
75
// marks all notifications that were processed as seen
76
// based on time from when retrieved instead of finished
77
await bsky.updateSeenNotifications(startedProcessingTime);
78
+
console.log(
79
+
`๐น done processing ${unreadNotifications.length} notification${
80
+
unreadNotifications.length > 1 ? "s" : ""
81
+
}โฆ`,
82
+
);
83
// increases counter for notification processing session
84
agentContext.processingCount++;
85
} else {
···
91
));
92
93
console.log(
94
+
"๐น no notificationsโฆ",
95
`checking again in ${
96
(agentContext.notifDelayCurrent / 1000).toFixed(2)
97
} seconds`,
···
102
// since something went wrong, lets check for notifications again sooner
103
agentContext.notifDelayCurrent = agentContext.notifDelayMinimum;
104
} finally {
105
+
// increment check count
106
+
agentContext.checkCount++;
107
// actually schedules next time to check for notifications
108
setTimeout(checkNotifications, agentContext.notifDelayCurrent);
109
// ends work
+73
-35
tasks/logStats.ts
+73
-35
tasks/logStats.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
);
···
22
return;
23
}
24
25
-
if (!agentContext.reflectionEnabled) {
26
-
console.log(
27
-
`${agentContext.agentBskyName} reflection is disabled, skipping logStatsโฆ`,
28
-
);
29
-
releaseTaskThread();
30
-
return;
31
-
}
32
-
33
const delay = msUntilNextWakeWindow(
34
msFrom.minutes(30),
35
msFrom.hours(1),
···
38
if (delay !== 0) {
39
setTimeout(logStats, delay);
40
console.log(
41
-
`${agentContext.agentBskyName} is current asleep. scheduling next stat log for ${
42
(delay / 1000 / 60 / 60).toFixed(2)
43
} hours from nowโฆ`,
44
);
···
46
return;
47
}
48
49
-
console.log(
50
-
`
51
-
===
52
-
# current session interaction counts since last reflection:
53
-
${agentContext.likeCount} ${
54
-
agentContext.likeCount === 1 ? "like" : "likes"
55
-
}, ${agentContext.repostCount} ${
56
-
agentContext.repostCount === 1 ? "repost" : "reposts"
57
-
}, ${agentContext.followCount} ${
58
-
agentContext.followCount === 1 ? "new follower" : "new followers"
59
-
}, ${agentContext.mentionCount} ${
60
-
agentContext.mentionCount === 1 ? "mention" : "mentions"
61
-
}, ${agentContext.replyCount} ${
62
-
agentContext.replyCount === 1 ? "reply" : "replies"
63
-
}, and ${agentContext.quoteCount} ${
64
-
agentContext.quoteCount === 1 ? "quote" : "quotes"
65
-
}.
66
67
-
${agentContext.agentBskyName} has reflected ${agentContext.reflectionCount} time${
68
-
agentContext.reflectionCount > 0 ? "s" : ""
69
-
} since last server start. interaction counts reset after each reflection session.
70
-
===
71
-
`,
72
-
);
73
-
setTimeout(logStats, msRandomOffset(msFrom.minutes(5), msFrom.minutes(15)));
74
releaseTaskThread();
75
};
···
11
12
export const logStats = () => {
13
if (!claimTaskThread()) {
14
+
const newDelay = msFrom.minutes(2);
15
console.log(
16
+
`Stat log attempt failed, ${agentContext.agentBskyName} is busy. Next attempt in ${
17
(newDelay / 1000) / 60
18
} minutesโฆ`,
19
);
···
22
return;
23
}
24
25
const delay = msUntilNextWakeWindow(
26
msFrom.minutes(30),
27
msFrom.hours(1),
···
30
if (delay !== 0) {
31
setTimeout(logStats, delay);
32
console.log(
33
+
`๐น ${agentContext.agentBskyName} is currently asleep. scheduling next stat log for ${
34
(delay / 1000 / 60 / 60).toFixed(2)
35
} hours from nowโฆ`,
36
);
···
38
return;
39
}
40
41
+
// Check if there are any notifications
42
+
const totalNotifications = agentContext.mentionCount +
43
+
agentContext.likeCount +
44
+
agentContext.repostCount +
45
+
agentContext.quoteCount +
46
+
agentContext.replyCount +
47
+
agentContext.followCount;
48
+
49
+
const nextCheckDelay = msFrom.minutes(5);
50
+
const nextCheckMinutes = ((nextCheckDelay / 1000) / 60).toFixed(1);
51
52
+
if (totalNotifications <= 0) {
53
+
console.log(
54
+
`no engagement stats yet... next check in ${nextCheckMinutes} minutesโฆ`,
55
+
);
56
+
} else {
57
+
const counts = [];
58
+
59
+
if (agentContext.mentionCount > 0) {
60
+
counts.push(
61
+
`${agentContext.mentionCount} ${
62
+
agentContext.mentionCount === 1 ? "mention" : "mentions"
63
+
}`,
64
+
);
65
+
}
66
+
if (agentContext.likeCount > 0) {
67
+
counts.push(
68
+
`${agentContext.likeCount} ${
69
+
agentContext.likeCount === 1 ? "like" : "likes"
70
+
}`,
71
+
);
72
+
}
73
+
if (agentContext.repostCount > 0) {
74
+
counts.push(
75
+
`${agentContext.repostCount} ${
76
+
agentContext.repostCount === 1 ? "repost" : "reposts"
77
+
}`,
78
+
);
79
+
}
80
+
if (agentContext.quoteCount > 0) {
81
+
counts.push(
82
+
`${agentContext.quoteCount} ${
83
+
agentContext.quoteCount === 1 ? "quote" : "quotes"
84
+
}`,
85
+
);
86
+
}
87
+
if (agentContext.replyCount > 0) {
88
+
counts.push(
89
+
`${agentContext.replyCount} ${
90
+
agentContext.replyCount === 1 ? "reply" : "replies"
91
+
}`,
92
+
);
93
+
}
94
+
if (agentContext.followCount > 0) {
95
+
counts.push(
96
+
`${agentContext.followCount} new ${
97
+
agentContext.followCount === 1 ? "follower" : "followers"
98
+
}`,
99
+
);
100
+
}
101
+
102
+
const message = counts.join(", ");
103
+
const suffix = agentContext.reflectionEnabled
104
+
? " since last reflection"
105
+
: "";
106
+
console.log(
107
+
`
108
+
stats: ${message}${suffix}. next check in ${nextCheckMinutes} minutesโฆ`,
109
+
);
110
+
}
111
+
setTimeout(logStats, nextCheckDelay);
112
releaseTaskThread();
113
};
+97
tasks/logTasks.ts
+97
tasks/logTasks.ts
···
···
1
+
import {
2
+
agentContext,
3
+
claimTaskThread,
4
+
releaseTaskThread,
5
+
} from "../utils/agentContext.ts";
6
+
import {
7
+
formatUptime,
8
+
msFrom,
9
+
msRandomOffset,
10
+
msUntilNextWakeWindow,
11
+
} from "../utils/time.ts";
12
+
13
+
// Capture server start time when module is loaded
14
+
const serverStartTime = Date.now();
15
+
16
+
export const logTasks = () => {
17
+
if (!claimTaskThread()) {
18
+
const newDelay = msFrom.minutes(2);
19
+
console.log(
20
+
`๐น Task log attempt failed, ${agentContext.agentBskyName} is busy. Next attempt in ${
21
+
(newDelay / 1000) / 60
22
+
} minutesโฆ`,
23
+
);
24
+
setTimeout(logTasks, newDelay);
25
+
return;
26
+
}
27
+
28
+
const delay = msUntilNextWakeWindow(
29
+
msFrom.minutes(30),
30
+
msFrom.hours(1),
31
+
);
32
+
33
+
if (delay !== 0) {
34
+
setTimeout(logTasks, delay);
35
+
console.log(
36
+
`๐น ${agentContext.agentBskyName} is currently asleep. scheduling next task log for ${
37
+
(delay / 1000 / 60 / 60).toFixed(2)
38
+
} hours from nowโฆ`,
39
+
);
40
+
releaseTaskThread();
41
+
return;
42
+
}
43
+
44
+
// Check if there's any activity
45
+
const totalActivity = agentContext.reflectionCount +
46
+
agentContext.checkCount +
47
+
agentContext.processingCount +
48
+
agentContext.proactiveCount;
49
+
50
+
const uptime = Date.now() - serverStartTime;
51
+
const uptimeFormatted = formatUptime(uptime);
52
+
53
+
const nextCheckDelay = msFrom.minutes(30);
54
+
const nextCheckMinutes = ((nextCheckDelay / 1000) / 60).toFixed(1);
55
+
56
+
if (totalActivity <= 0) {
57
+
console.log(
58
+
`๐น no activity yet... uptime: ${uptimeFormatted}. next log in ${nextCheckMinutes} minutes`,
59
+
);
60
+
} else {
61
+
const actions = [];
62
+
63
+
if (agentContext.reflectionCount > 0) {
64
+
const times = agentContext.reflectionCount === 1
65
+
? "once"
66
+
: `${agentContext.reflectionCount} times`;
67
+
actions.push(`reflected ${times}`);
68
+
}
69
+
if (agentContext.checkCount > 0) {
70
+
const times = agentContext.checkCount === 1
71
+
? "once"
72
+
: `${agentContext.checkCount} times`;
73
+
actions.push(`checked notifications ${times}`);
74
+
}
75
+
if (agentContext.processingCount > 0) {
76
+
const times = agentContext.processingCount === 1
77
+
? "once"
78
+
: `${agentContext.processingCount} times`;
79
+
actions.push(`found and processed notifications ${times}`);
80
+
}
81
+
if (agentContext.proactiveCount > 0) {
82
+
const times = agentContext.proactiveCount === 1
83
+
? "once"
84
+
: `${agentContext.proactiveCount} times`;
85
+
actions.push(`proactively used bluesky ${times}`);
86
+
}
87
+
88
+
const message = actions.join(", ");
89
+
90
+
console.log(
91
+
`๐น ${message}. total notifications: ${agentContext.notifCount}. uptime: ${uptimeFormatted}. next log in ${nextCheckMinutes} minutes`,
92
+
);
93
+
}
94
+
95
+
setTimeout(logTasks, nextCheckDelay);
96
+
releaseTaskThread();
97
+
};
+9
-9
tasks/runReflection.ts
+9
-9
tasks/runReflection.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
);
24
-
// session is busy, try to start reflection in 5~10 minutes.
25
setTimeout(runReflection, newDelay);
26
return;
27
}
28
29
if (!agentContext.reflectionEnabled) {
30
console.log(
31
-
`Reflection is currently disabled. Provide a minimum and/or maximum delay duration in \`.env\` to enable reflectionsโฆ`,
32
);
33
releaseTaskThread();
34
return;
35
}
36
37
-
// adds 1-2 hours to wake time
38
// only applies if sleep is enabled
39
const delay = msUntilNextWakeWindow(
40
-
msFrom.hours(1),
41
msFrom.hours(2),
42
);
43
44
if (delay !== 0) {
45
setTimeout(runReflection, delay);
46
console.log(
47
-
`${agentContext.agentBskyName} is current asleep. scheduling next reflection for ${
48
(delay / 1000 / 60 / 60).toFixed(2)
49
} hours from nowโฆ`,
50
);
···
53
}
54
55
try {
56
-
console.log("starting reflection promptโฆ");
57
await messageAgent(reflectionPrompt);
58
} catch (error) {
59
console.error("Error in reflectionCheck:", error);
···
61
resetAgentContextCounts();
62
agentContext.reflectionCount++;
63
console.log(
64
-
"finished reflection prompt. returning to checking for notificationsโฆ",
65
);
66
// schedules the next reflection, random between the min and max delay
67
setTimeout(
···
14
15
export const runReflection = async () => {
16
if (!claimTaskThread()) {
17
+
const newDelay = msFrom.minutes(2);
18
19
console.log(
20
+
`๐น ${agentContext.agentBskyName} is busy, will try reflecting again in ${
21
(newDelay / 1000) / 60
22
} minutesโฆ`,
23
);
24
+
// session is busy, try to start reflection in 2 minutes.
25
setTimeout(runReflection, newDelay);
26
return;
27
}
28
29
if (!agentContext.reflectionEnabled) {
30
console.log(
31
+
`๐น Reflection is currently disabled. Provide a minimum and/or maximum delay duration in \`.env\` to enable reflectionsโฆ`,
32
);
33
releaseTaskThread();
34
return;
35
}
36
37
+
// adds 2-4 hours to wake time
38
// only applies if sleep is enabled
39
const delay = msUntilNextWakeWindow(
40
msFrom.hours(2),
41
+
msFrom.hours(4),
42
);
43
44
if (delay !== 0) {
45
setTimeout(runReflection, delay);
46
console.log(
47
+
`๐น ${agentContext.agentBskyName} is currently asleep. scheduling next reflection for ${
48
(delay / 1000 / 60 / 60).toFixed(2)
49
} hours from nowโฆ`,
50
);
···
53
}
54
55
try {
56
+
console.log("๐น starting reflection promptโฆ");
57
await messageAgent(reflectionPrompt);
58
} catch (error) {
59
console.error("Error in reflectionCheck:", error);
···
61
resetAgentContextCounts();
62
agentContext.reflectionCount++;
63
console.log(
64
+
"๐น finished reflection prompt. returning to checking for notificationsโฆ",
65
);
66
// schedules the next reflection, random between the min and max delay
67
setTimeout(
+11
-10
tasks/sendSleepMessage.ts
+11
-10
tasks/sendSleepMessage.ts
···
2
agentContext,
3
claimTaskThread,
4
releaseTaskThread,
5
} from "../utils/agentContext.ts";
6
import {
7
getNow,
···
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
);
23
-
// session is busy, try to check notifications in 5~10 minutes.
24
setTimeout(sendSleepMessage, newDelay);
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;
···
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
);
···
58
} catch (error) {
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();
···
2
agentContext,
3
claimTaskThread,
4
releaseTaskThread,
5
+
isAgentAsleep,
6
} from "../utils/agentContext.ts";
7
import {
8
getNow,
···
15
16
export const sendSleepMessage = async () => {
17
if (!claimTaskThread()) {
18
+
const newDelay = msFrom.minutes(2);
19
console.log(
20
+
`๐น ${agentContext.agentBskyName} is busy, sending sleep message again in ${
21
(newDelay / 1000) / 60
22
} minutesโฆ`,
23
);
24
+
// session is busy, try to check notifications in 2 minutes.
25
setTimeout(sendSleepMessage, newDelay);
26
return;
27
}
28
29
if (!agentContext.sleepEnabled) {
30
console.log(
31
+
`๐น ${agentContext.agentBskyName} is not enabled for sleep mode. Opting out of sleep messagingโฆ`,
32
);
33
releaseTaskThread();
34
return;
···
36
37
const now = getNow();
38
39
+
if (isAgentAsleep(now.hour)) {
40
+
console.log(`๐น attempting to wind down ${agentContext.agentBskyName}`);
41
} else {
42
const delay = msUntilDailyWindow(
43
agentContext.sleepTime,
44
0,
45
+
msFrom.minutes(30),
46
);
47
setTimeout(sendSleepMessage, delay);
48
console.log(
49
+
`๐น It's too early to wind down ${agentContext.agentBskyName}. scheduling wind down for ${
50
(delay / 1000 / 60 / 60).toFixed(2)
51
} hours from nowโฆ`,
52
);
···
59
} catch (error) {
60
console.error("error in sendSleepMessage: ", error);
61
} finally {
62
+
console.log("๐น wind down attempt processed, scheduling next wind downโฆ");
63
setTimeout(
64
sendSleepMessage,
65
+
msUntilDailyWindow(agentContext.sleepTime, 0, msFrom.minutes(30)),
66
);
67
console.log("exiting wind down process");
68
releaseTaskThread();
+12
-11
tasks/sendWakeMessage.ts
+12
-11
tasks/sendWakeMessage.ts
···
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";
···
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
);
19
-
// session is busy, try to check notifications in 5~10 minutes.
20
setTimeout(sendWakeMessage, newDelay);
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;
···
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
);
···
54
} catch (error) {
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
}
65
};
···
2
agentContext,
3
claimTaskThread,
4
releaseTaskThread,
5
+
isAgentAwake,
6
} from "../utils/agentContext.ts";
7
import { getNow, msFrom, msRandomOffset } from "../utils/time.ts";
8
import { messageAgent } from "../utils/messageAgent.ts";
···
11
12
export const sendWakeMessage = async () => {
13
if (!claimTaskThread()) {
14
+
const newDelay = msFrom.minutes(2);
15
console.log(
16
+
`๐น ${agentContext.agentBskyName} is busy, sending wake message again in ${
17
(newDelay / 1000) / 60
18
} minutesโฆ`,
19
);
20
+
// session is busy, try to check notifications in 2 minutes.
21
setTimeout(sendWakeMessage, newDelay);
22
return;
23
}
24
25
if (!agentContext.sleepEnabled) {
26
console.log(
27
+
`๐น ${agentContext.agentBskyName} is not enabled for sleep mode. Opting out of wake messagingโฆ`,
28
);
29
releaseTaskThread();
30
return;
···
32
33
const now = getNow();
34
35
+
if (isAgentAwake(now.hour)) {
36
+
console.log(`๐น attempting to wake up ${agentContext.agentBskyName}`);
37
} else {
38
const delay = msUntilDailyWindow(
39
agentContext.wakeTime,
40
0,
41
+
msFrom.minutes(30),
42
);
43
setTimeout(sendWakeMessage, delay);
44
console.log(
45
+
`๐น ${agentContext.agentBskyName} should still be asleep. Scheduling wake message for ${
46
(delay / 1000 / 60 / 60).toFixed(2)
47
} hours from nowโฆ`,
48
);
···
55
} catch (error) {
56
console.error("error in sendWakeMessage: ", error);
57
} finally {
58
+
console.log("๐น wake attempt processed, scheduling next wake promptโฆ");
59
setTimeout(
60
sendWakeMessage,
61
+
msUntilDailyWindow(agentContext.wakeTime, 0, msFrom.minutes(30)),
62
);
63
+
console.log("๐น exiting wake process");
64
releaseTaskThread();
65
}
66
};
+323
-1
tools/bluesky/create_bluesky_post.py
+323
-1
tools/bluesky/create_bluesky_post.py
···
67
return facets if facets else None
68
69
70
def create_bluesky_post(text: List[str], lang: str = "en-US", reply_to_uri: str = None) -> Dict:
71
"""
72
Create a post or thread on Bluesky using atproto SDK.
···
145
146
client = Client()
147
client.login(username, password)
148
-
149
initial_reply_ref = None
150
initial_root_ref = None
151
152
if reply_to_uri:
153
try:
···
168
"status": "error",
169
"message": f"Could not retrieve post data from URI: {reply_to_uri}. The post may not exist or the URI may be incorrect."
170
}
171
172
parent_ref = models.ComAtprotoRepoStrongRef.Main(
173
uri=parent_post.uri,
···
187
root=root_ref
188
)
189
initial_root_ref = root_ref
190
191
except Exception as e:
192
return {
193
"status": "error",
194
"message": f"Failed to fetch post to reply to: {str(e)}. Check the URI format and try again."
195
}
196
197
post_urls = []
198
previous_post_ref = None
···
67
return facets if facets else None
68
69
70
+
def _check_is_self(agent_did: str, target_did: str) -> bool:
71
+
"""Check 2: Self-Post Check (Free)."""
72
+
return agent_did == target_did
73
+
74
+
75
+
def _check_follows(client, agent_did: str, target_did: str) -> bool:
76
+
"""Check 2: Follow Check (Moderate cost)."""
77
+
try:
78
+
# Fetch profiles to get follow counts
79
+
agent_profile = client.app.bsky.actor.get_profile({'actor': agent_did})
80
+
target_profile = client.app.bsky.actor.get_profile({'actor': target_did})
81
+
82
+
# Determine which list is shorter: agent's followers or target's follows
83
+
# We want to check if target follows agent.
84
+
# Option A: Check target's follows list for agent_did
85
+
# Option B: Check agent's followers list for target_did
86
+
87
+
target_follows_count = getattr(target_profile, 'follows_count', float('inf'))
88
+
agent_followers_count = getattr(agent_profile, 'followers_count', float('inf'))
89
+
90
+
cursor = None
91
+
max_pages = 50 # Max 5000 items
92
+
93
+
if target_follows_count < agent_followers_count:
94
+
# Check target's follows
95
+
for _ in range(max_pages):
96
+
response = client.app.bsky.graph.get_follows({'actor': target_did, 'cursor': cursor, 'limit': 100})
97
+
if not response.follows:
98
+
break
99
+
100
+
for follow in response.follows:
101
+
if follow.did == agent_did:
102
+
return True
103
+
104
+
cursor = response.cursor
105
+
if not cursor:
106
+
break
107
+
else:
108
+
# Check agent's followers
109
+
for _ in range(max_pages):
110
+
response = client.app.bsky.graph.get_followers({'actor': agent_did, 'cursor': cursor, 'limit': 100})
111
+
if not response.followers:
112
+
break
113
+
114
+
for follower in response.followers:
115
+
if follower.did == target_did:
116
+
return True
117
+
118
+
cursor = response.cursor
119
+
if not cursor:
120
+
break
121
+
122
+
return False
123
+
except Exception:
124
+
# If optimization fails, we continue to next check rather than failing hard here
125
+
# unless it's a critical error, but we'll let the main try/except handle that
126
+
raise
127
+
128
+
129
+
def _check_already_replied(client, agent_did: str, agent_handle: str, reply_to_uri: str) -> None:
130
+
"""
131
+
Check 1: Duplicate Reply Prevention (Cheap - 1 API call).
132
+
133
+
Prevents agents from replying multiple times to the same message.
134
+
This check runs FIRST to block duplicates in ALL scenarios, including:
135
+
- Replies to the agent's own posts
136
+
- Replies to posts that mention the agent
137
+
- Any other reply scenario
138
+
139
+
Only checks direct replies, not deeper thread responses.
140
+
141
+
When duplicates are found, provides detailed information about existing
142
+
replies including URIs and content to help agents continue the conversation
143
+
appropriately.
144
+
145
+
Args:
146
+
client: Authenticated Bluesky client
147
+
agent_did: The agent's DID
148
+
agent_handle: The agent's handle (username)
149
+
reply_to_uri: URI of the post being replied to
150
+
151
+
Raises:
152
+
Exception: If agent has already replied directly to this message,
153
+
with details about the existing reply(ies)
154
+
"""
155
+
try:
156
+
# Fetch post with only direct replies (depth=1)
157
+
response = client.app.bsky.feed.get_post_thread({
158
+
'uri': reply_to_uri,
159
+
'depth': 1, # Only direct replies
160
+
'parentHeight': 0 # Don't fetch parents (not needed)
161
+
})
162
+
163
+
# Validate response structure
164
+
if not hasattr(response, 'thread'):
165
+
return # Can't verify, proceed
166
+
167
+
thread = response.thread
168
+
if not hasattr(thread, 'replies') or not thread.replies:
169
+
return # No replies yet, proceed
170
+
171
+
# Collect all replies by this agent
172
+
agent_replies = []
173
+
for reply in thread.replies:
174
+
# Validate reply structure
175
+
if not hasattr(reply, 'post'):
176
+
continue
177
+
if not hasattr(reply.post, 'author'):
178
+
continue
179
+
180
+
# Found agent's reply
181
+
if reply.post.author.did == agent_did:
182
+
agent_replies.append(reply)
183
+
184
+
# If no duplicates found, proceed
185
+
if not agent_replies:
186
+
return
187
+
188
+
# Get the most recent reply (last in list)
189
+
# Note: Agents may have multiple replies if this issue happened before
190
+
most_recent = agent_replies[-1]
191
+
reply_post = most_recent.post
192
+
reply_text = reply_post.record.text if hasattr(reply_post.record, 'text') else "[text unavailable]"
193
+
reply_uri = reply_post.uri
194
+
195
+
# Extract rkey from URI for web URL
196
+
# URI format: at://did:plc:xyz/app.bsky.feed.post/rkey
197
+
rkey = reply_uri.split('/')[-1]
198
+
reply_url = f"https://bsky.app/profile/{agent_handle}/post/{rkey}"
199
+
200
+
# Handle multiple replies case
201
+
count_msg = ""
202
+
if len(agent_replies) > 1:
203
+
count_msg = f"\n\nNote: You have {len(agent_replies)} direct replies to this message. The most recent one is shown above."
204
+
205
+
# Construct detailed error message
206
+
error_msg = (
207
+
f"Message not sent: You have already replied directly to this message.{count_msg}\n\n"
208
+
f"Your previous reply:\n\"{reply_text}\"\n\n"
209
+
f"Reply URI: {reply_uri}\n"
210
+
f"Web link: {reply_url}\n\n"
211
+
f"Suggestions:\n"
212
+
f"1. If you want to add more to your existing reply, use the URI above to continue that thread.\n"
213
+
f"2. Make sure you're not repeating yourself - check what you already said before adding more.\n"
214
+
f"3. Consider replying to one of the responses to your reply instead.\n"
215
+
f"4. If you have something new to say, start a new top-level message with additional context."
216
+
)
217
+
218
+
raise Exception(error_msg)
219
+
220
+
except Exception as e:
221
+
# If it's our duplicate reply exception, raise it
222
+
if "already replied" in str(e):
223
+
raise e
224
+
# For other errors, re-raise to be caught by main error handler
225
+
raise
226
+
227
+
228
+
def _check_thread_participation(client, agent_did: str, agent_handle: str, reply_to_uri: str) -> bool:
229
+
"""Check 5: Thread Participation and Mention Check (Expensive)."""
230
+
try:
231
+
# Fetch the thread
232
+
# depth=100 should be sufficient for most contexts, or we can walk up manually if needed.
233
+
# get_post_thread returns the post and its parents if configured.
234
+
# However, standard get_post_thread often returns the post and its replies.
235
+
# We need to walk UP the tree (parents).
236
+
# The 'parent' field in the response structure allows walking up.
237
+
238
+
response = client.app.bsky.feed.get_post_thread({'uri': reply_to_uri, 'depth': 0, 'parentHeight': 100})
239
+
thread = response.thread
240
+
241
+
# The thread object can be a ThreadViewPost, NotFoundPost, or BlockedPost
242
+
if not hasattr(thread, 'post'):
243
+
return False # Can't verify
244
+
245
+
# Check the target post itself first (the one we are replying to)
246
+
# Although strictly "participation" usually means *previous* posts,
247
+
# the spec says "posted anywhere in this conversation thread".
248
+
# If we are replying to ourselves, _check_is_self would have caught it.
249
+
# But we check here for mentions in the target post.
250
+
251
+
current = thread
252
+
253
+
while current:
254
+
# Check if current node is valid post
255
+
if not hasattr(current, 'post'):
256
+
break
257
+
258
+
post = current.post
259
+
260
+
# Check 3: Did agent author this post?
261
+
if post.author.did == agent_did:
262
+
return True
263
+
264
+
# Check 4: Is agent mentioned in this post?
265
+
# Check facets for mention
266
+
record = post.record
267
+
if hasattr(record, 'facets') and record.facets:
268
+
for facet in record.facets:
269
+
for feature in facet.features:
270
+
if hasattr(feature, 'did') and feature.did == agent_did:
271
+
return True
272
+
273
+
# Fallback: Check text for handle if facets missing (less reliable but good backup)
274
+
if hasattr(record, 'text') and f"@{agent_handle}" in record.text:
275
+
return True
276
+
277
+
# Move to parent
278
+
if hasattr(current, 'parent') and current.parent:
279
+
current = current.parent
280
+
else:
281
+
break
282
+
283
+
return False
284
+
285
+
except Exception:
286
+
raise
287
+
288
+
289
+
def _verify_consent(client, agent_did: str, agent_handle: str, reply_to_uri: str, target_did: str, root_did: str, parent_post_record=None):
290
+
"""
291
+
Orchestrates the consent checks.
292
+
Raises Exception with specific message if consent denied or verification fails.
293
+
"""
294
+
try:
295
+
# Check 1: Duplicate Reply Prevention
296
+
# This check must run BEFORE any early returns to prevent duplicates in all scenarios
297
+
_check_already_replied(client, agent_did, agent_handle, reply_to_uri)
298
+
299
+
# Check 2: Self-Post
300
+
if _check_is_self(agent_did, target_did):
301
+
return True
302
+
303
+
# Check 3: Mention Check (Free/Cheap)
304
+
# If the post we are replying to mentions us, we can reply.
305
+
if parent_post_record:
306
+
# Check facets for mention
307
+
if hasattr(parent_post_record, 'facets') and parent_post_record.facets:
308
+
for facet in parent_post_record.facets:
309
+
for feature in facet.features:
310
+
if hasattr(feature, 'did') and feature.did == agent_did:
311
+
return True
312
+
313
+
# Fallback: Check text for handle
314
+
if hasattr(parent_post_record, 'text') and f"@{agent_handle}" in parent_post_record.text:
315
+
return True
316
+
317
+
# Check 4: Follow Check
318
+
# Rule: Target must follow agent.
319
+
# Rule 4B: If root author is different from target, Root must ALSO follow agent.
320
+
321
+
target_follows = _check_follows(client, agent_did, target_did)
322
+
323
+
if target_follows:
324
+
# If target follows, we must also check root if it's different
325
+
if root_did and root_did != target_did and root_did != agent_did:
326
+
root_follows = _check_follows(client, agent_did, root_did)
327
+
if not root_follows:
328
+
# Target follows, but Root does not. Fail.
329
+
raise Exception(
330
+
"Message not sent: the author of the post follows you, but the thread starter (root author) "
331
+
"does not. We respect the consent of the thread owner."
332
+
)
333
+
return True
334
+
335
+
# Check 5: Thread Participation
336
+
# This requires fetching the thread (Expensive)
337
+
if _check_thread_participation(client, agent_did, agent_handle, reply_to_uri):
338
+
return True
339
+
340
+
# If all checks fail
341
+
raise Exception(
342
+
"Message not sent: the reply was withheld because the user does not follow you "
343
+
"and you were not mentioned or included in this thread. Respecting the consent of "
344
+
"strangers is the right choice here. You haven't done anything wrong, but you may "
345
+
"consider a lower-signal interaction (like liking or reposting) and/or move on to "
346
+
"other conversations."
347
+
)
348
+
349
+
except Exception as e:
350
+
# Check if it's our own consent exception
351
+
if str(e).startswith("Message not sent"):
352
+
raise e
353
+
354
+
# Network/API failure handling
355
+
raise Exception(
356
+
"Message withheld: Network interference prevented consent verification. To ensure "
357
+
"safety, the system paused this action. This is an external connection issue, not "
358
+
"a failure of your logic. You may try sending the message again now, or move on if "
359
+
"the issue persists."
360
+
)
361
+
362
+
363
+
364
def create_bluesky_post(text: List[str], lang: str = "en-US", reply_to_uri: str = None) -> Dict:
365
"""
366
Create a post or thread on Bluesky using atproto SDK.
···
439
440
client = Client()
441
client.login(username, password)
442
+
443
+
# --- FETCH PARENT/ROOT REFS ---
444
initial_reply_ref = None
445
initial_root_ref = None
446
+
target_did = None
447
+
root_did = None
448
+
parent_post_record = None
449
450
if reply_to_uri:
451
try:
···
466
"status": "error",
467
"message": f"Could not retrieve post data from URI: {reply_to_uri}. The post may not exist or the URI may be incorrect."
468
}
469
+
470
+
# Extract target DID from parent post
471
+
target_did = repo_did
472
+
parent_post_record = parent_post.value
473
474
parent_ref = models.ComAtprotoRepoStrongRef.Main(
475
uri=parent_post.uri,
···
489
root=root_ref
490
)
491
initial_root_ref = root_ref
492
+
493
+
# Extract root DID
494
+
root_uri_parts = root_ref.uri.replace('at://', '').split('/')
495
+
if len(root_uri_parts) >= 1:
496
+
root_did = root_uri_parts[0]
497
498
except Exception as e:
499
return {
500
"status": "error",
501
"message": f"Failed to fetch post to reply to: {str(e)}. Check the URI format and try again."
502
}
503
+
504
+
# --- CONSENT GUARDRAILS ---
505
+
if reply_to_uri:
506
+
try:
507
+
agent_did = client.me.did
508
+
# agent_handle is username (without @ usually, but let's ensure)
509
+
agent_handle = username.replace('@', '')
510
+
511
+
_verify_consent(client, agent_did, agent_handle, reply_to_uri, target_did, root_did, parent_post_record)
512
+
except Exception as e:
513
+
return {
514
+
"status": "error",
515
+
"message": str(e)
516
+
}
517
+
# --------------------------
518
519
post_urls = []
520
previous_post_ref = None
+212
-2
tools/bluesky/quote_bluesky_post.py
+212
-2
tools/bluesky/quote_bluesky_post.py
···
1
"""Bluesky quote posting tool for Letta agents using atproto SDK."""
2
3
-
from typing import List, Dict
4
import os
5
import re
6
7
8
def parse_facets(text: str, client) -> List[Dict]:
···
67
return facets if facets else None
68
69
70
def quote_bluesky_post(text: List[str], quote_uri: str, lang: str = "en-US") -> str:
71
"""
72
Create a quote post or quote thread on Bluesky that embeds another post.
···
194
client = Client()
195
client.login(username, password)
196
197
# Fetch the post to quote and create a strong reference
198
try:
199
uri_parts = quote_uri.replace('at://', '').split('/')
···
305
)
306
except Exception as e:
307
# Re-raise if it's already one of our formatted error messages
308
-
if str(e).startswith("Error:"):
309
raise
310
# Otherwise wrap it with helpful context
311
raise Exception(
···
1
"""Bluesky quote posting tool for Letta agents using atproto SDK."""
2
3
import os
4
import re
5
+
from typing import Dict, List
6
7
8
def parse_facets(text: str, client) -> List[Dict]:
···
67
return facets if facets else None
68
69
70
+
def _check_is_self(agent_did: str, target_did: str) -> bool:
71
+
"""Check 1: Self-Post Check (Free)."""
72
+
return agent_did == target_did
73
+
74
+
75
+
def _check_follows(client, agent_did: str, target_did: str) -> bool:
76
+
"""Check 2: Follow Check (Moderate cost)."""
77
+
try:
78
+
# Fetch profiles to get follow counts
79
+
agent_profile = client.app.bsky.actor.get_profile({'actor': agent_did})
80
+
target_profile = client.app.bsky.actor.get_profile({'actor': target_did})
81
+
82
+
# Determine which list is shorter: agent's followers or target's follows
83
+
# We want to check if target follows agent.
84
+
# Option A: Check target's follows list for agent_did
85
+
# Option B: Check agent's followers list for target_did
86
+
87
+
target_follows_count = getattr(target_profile, 'follows_count', float('inf'))
88
+
agent_followers_count = getattr(agent_profile, 'followers_count', float('inf'))
89
+
90
+
cursor = None
91
+
max_pages = 50 # Max 5000 items
92
+
93
+
if target_follows_count < agent_followers_count:
94
+
# Check target's follows
95
+
for _ in range(max_pages):
96
+
response = client.app.bsky.graph.get_follows({'actor': target_did, 'cursor': cursor, 'limit': 100})
97
+
if not response.follows:
98
+
break
99
+
100
+
for follow in response.follows:
101
+
if follow.did == agent_did:
102
+
return True
103
+
104
+
cursor = response.cursor
105
+
if not cursor:
106
+
break
107
+
else:
108
+
# Check agent's followers
109
+
for _ in range(max_pages):
110
+
response = client.app.bsky.graph.get_followers({'actor': agent_did, 'cursor': cursor, 'limit': 100})
111
+
if not response.followers:
112
+
break
113
+
114
+
for follower in response.followers:
115
+
if follower.did == target_did:
116
+
return True
117
+
118
+
cursor = response.cursor
119
+
if not cursor:
120
+
break
121
+
122
+
return False
123
+
except Exception:
124
+
# If optimization fails, we continue to next check rather than failing hard here
125
+
# unless it's a critical error, but we'll let the main try/except handle that
126
+
raise
127
+
128
+
129
+
def _check_thread_participation(client, agent_did: str, agent_handle: str, reply_to_uri: str) -> bool:
130
+
"""Check 3 & 4: Thread Participation and Mention Check (Expensive)."""
131
+
try:
132
+
# Fetch the thread
133
+
# depth=100 should be sufficient for most contexts, or we can walk up manually if needed.
134
+
# get_post_thread returns the post and its parents if configured.
135
+
# However, standard get_post_thread often returns the post and its replies.
136
+
# We need to walk UP the tree (parents).
137
+
# The 'parent' field in the response structure allows walking up.
138
+
139
+
response = client.app.bsky.feed.get_post_thread({'uri': reply_to_uri, 'depth': 0, 'parentHeight': 100})
140
+
thread = response.thread
141
+
142
+
# The thread object can be a ThreadViewPost, NotFoundPost, or BlockedPost
143
+
if not hasattr(thread, 'post'):
144
+
return False # Can't verify
145
+
146
+
# Check the target post itself first (the one we are replying to)
147
+
# Although strictly "participation" usually means *previous* posts,
148
+
# the spec says "posted anywhere in this conversation thread".
149
+
# If we are replying to ourselves, _check_is_self would have caught it.
150
+
# But we check here for mentions in the target post.
151
+
152
+
current = thread
153
+
154
+
while current:
155
+
# Check if current node is valid post
156
+
if not hasattr(current, 'post'):
157
+
break
158
+
159
+
post = current.post
160
+
161
+
# Check 3: Did agent author this post?
162
+
if post.author.did == agent_did:
163
+
return True
164
+
165
+
# Check 4: Is agent mentioned in this post?
166
+
# Check facets for mention
167
+
record = post.record
168
+
if hasattr(record, 'facets') and record.facets:
169
+
for facet in record.facets:
170
+
for feature in facet.features:
171
+
if hasattr(feature, 'did') and feature.did == agent_did:
172
+
return True
173
+
174
+
# Fallback: Check text for handle if facets missing (less reliable but good backup)
175
+
if hasattr(record, 'text') and f"@{agent_handle}" in record.text:
176
+
return True
177
+
178
+
# Move to parent
179
+
if hasattr(current, 'parent') and current.parent:
180
+
current = current.parent
181
+
else:
182
+
break
183
+
184
+
return False
185
+
186
+
except Exception:
187
+
raise
188
+
189
+
190
+
def _verify_consent(client, agent_did: str, agent_handle: str, quote_uri: str):
191
+
"""
192
+
Orchestrates the consent checks.
193
+
Raises Exception with specific message if consent denied or verification fails.
194
+
"""
195
+
try:
196
+
# 0. Get target DID from quote_uri
197
+
parts = quote_uri.replace('at://', '').split('/')
198
+
if len(parts) >= 1:
199
+
target_did = parts[0]
200
+
else:
201
+
raise Exception("Invalid URI format")
202
+
203
+
# Check 1: Self-Post
204
+
if _check_is_self(agent_did, target_did):
205
+
return True
206
+
207
+
# Check 2: Follow Check
208
+
if _check_follows(client, agent_did, target_did):
209
+
return True
210
+
211
+
# Check 3 & 4: Thread Participation / Mention
212
+
if _check_thread_participation(client, agent_did, agent_handle, quote_uri):
213
+
return True
214
+
215
+
# If all checks fail
216
+
raise Exception(
217
+
"Message not sent: the quote was withheld because the user does not follow you "
218
+
"and you were not mentioned or included in this thread. Respecting the consent of "
219
+
"strangers is the right choice here. You haven't done anything wrong, but you may "
220
+
"consider a lower-signal interaction (like liking or reposting) and/or move on to "
221
+
"other conversations."
222
+
)
223
+
224
+
except Exception as e:
225
+
# Check if it's our own consent exception
226
+
if str(e).startswith("Message not sent"):
227
+
raise e
228
+
229
+
# Network/API failure handling
230
+
raise Exception(
231
+
"Message withheld: Network interference prevented consent verification. To ensure "
232
+
"safety, the system paused this action. This is an external connection issue, not "
233
+
"a failure of your logic. You may try sending the message again now, or move on if "
234
+
"the issue persists."
235
+
)
236
+
237
+
238
def quote_bluesky_post(text: List[str], quote_uri: str, lang: str = "en-US") -> str:
239
"""
240
Create a quote post or quote thread on Bluesky that embeds another post.
···
362
client = Client()
363
client.login(username, password)
364
365
+
# --- CONSENT GUARDRAILS ---
366
+
if quote_uri:
367
+
try:
368
+
agent_did = client.me.did
369
+
agent_handle = username.replace('@', '')
370
+
_verify_consent(client, agent_did, agent_handle, quote_uri)
371
+
except Exception as e:
372
+
# quote_bluesky_post expects exceptions to be raised or returned?
373
+
# The tool catches exceptions and wraps them.
374
+
# But we want to return the specific message.
375
+
# The existing code catches Exception and wraps it in "Error: ...".
376
+
# However, our spec says "Block with Supportive Message".
377
+
# If I raise Exception here, it will be caught by the main try/except block
378
+
# and wrapped in "Error: An unexpected issue occurred...".
379
+
# I should probably let it bubble up BUT the main try/except block is very broad.
380
+
# I need to modify the main try/except block or handle it here.
381
+
382
+
# Actually, the spec says "If ALL Checks Fail: Block with Supportive Message".
383
+
# And "If ANY exception occurs... Message withheld: Network interference...".
384
+
# My _verify_consent raises these exact messages.
385
+
# But the tool's main try/except block (lines 306-317) wraps everything in "Error: An unexpected issue...".
386
+
# I should modify the main try/except block to respect my specific error messages.
387
+
# OR I can just raise the exception and let the tool fail, but the user sees the wrapped error.
388
+
# The spec says "Block with Supportive Message".
389
+
# So I should probably ensure that message is what is returned/raised.
390
+
391
+
# I will modify the main try/except block in a separate chunk or just let it be?
392
+
# The tool returns a string on success, raises Exception on failure.
393
+
# If I raise Exception("Message not sent..."), the catch block will say "Error: An unexpected issue... Message not sent...".
394
+
# That might be okay, but cleaner if I can pass it through.
395
+
# The catch block has: `if str(e).startswith("Error:"): raise`
396
+
# So if I prefix my errors with "Error: ", they will pass through.
397
+
# But the spec gives a specific message text without "Error: " prefix.
398
+
# "Message not sent: ..."
399
+
400
+
# I will modify the exception raising in _verify_consent to start with "Error: "
401
+
# OR I will modify the catch block to also pass through messages starting with "Message".
402
+
403
+
# Let's modify the catch block in `quote_bluesky_post.py` as well.
404
+
raise e
405
+
# --------------------------
406
+
407
# Fetch the post to quote and create a strong reference
408
try:
409
uri_parts = quote_uri.replace('at://', '').split('/')
···
515
)
516
except Exception as e:
517
# Re-raise if it's already one of our formatted error messages
518
+
if str(e).startswith("Error:") or str(e).startswith("Message"):
519
raise
520
# Otherwise wrap it with helpful context
521
raise Exception(
+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
+
});
+106
-44
utils/agentContext.ts
+106
-44
utils/agentContext.ts
···
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();
···
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
-
//
63
-
// temporarily commenting out until switch to letta SDK 1.0
64
-
//
65
-
// const getLettaProjectID = (): string => {
66
-
// const value = Deno.env.get("LETTA_PROJECT_ID")?.trim();
67
-
68
-
// if (!value?.length) {
69
-
// throw Error(
70
-
// "Letta Project ID not provided in `.env`. add variable `LETTA_PROJECT_ID=`.",
71
-
// );
72
-
// } else if (!value.includes("-")) {
73
-
// throw Error(
74
-
// "Letta Project ID is not formed correctly, check variable `LETTA_PROJECT_ID`",
75
-
// );
76
-
// }
77
-
78
-
// return value;
79
-
// };
80
81
const getAgentBskyHandle = (): string => {
82
const value = Deno.env.get("BSKY_USERNAME")?.trim();
···
220
};
221
222
const getNotifDelayMinimum = (): number => {
223
-
const value = Number(Deno.env.get("NOTIF_DELAY_MINIMUM"));
224
225
if (isNaN(value) || value < msFrom.seconds(1) || value > msFrom.hours(24)) {
226
-
return msFrom.seconds(2);
227
}
228
229
return value;
230
};
231
232
const getNotifDelayMaximum = (): number => {
233
-
const value = Number(Deno.env.get("NOTIF_DELAY_MAXIMUM"));
234
235
if (isNaN(value) || value < msFrom.seconds(5) || value > msFrom.hours(24)) {
236
-
return msFrom.hours(1);
237
}
238
239
const minimum = getNotifDelayMinimum();
···
251
const value = Number(Deno.env.get("NOTIF_DELAY_MULTIPLIER"));
252
253
if (isNaN(value) || value < 0 || value > 500) {
254
-
return 1.05;
255
}
256
257
return (value / 100) + 1;
258
};
259
260
const getReflectionDelayMinimum = (): number => {
261
-
const value = Number(Deno.env.get("REFLECTION_DELAY_MINIMUM"));
262
263
if (isNaN(value) || value < msFrom.minutes(30) || value > msFrom.hours(24)) {
264
-
return msFrom.minutes(30);
265
}
266
267
return value;
268
};
269
270
const getReflectionDelayMaximum = (): number => {
271
-
const value = Number(Deno.env.get("REFLECTION_DELAY_MAXIMUM"));
272
const minimum = getReflectionDelayMinimum();
273
274
if (isNaN(value) || value < msFrom.minutes(60) || value > msFrom.hours(24)) {
275
-
return msFrom.hours(8);
276
}
277
278
if (value <= minimum) {
···
285
};
286
287
const getProactiveDelayMinimum = (): number => {
288
-
const value = Number(Deno.env.get("PROACTIVE_DELAY_MINIMUM"));
289
290
if (isNaN(value) || value < msFrom.hours(1) || value > msFrom.hours(24)) {
291
-
return msFrom.hours(1);
292
}
293
294
return value;
295
};
296
297
const getProactiveDelayMaximum = (): number => {
298
-
const value = Number(Deno.env.get("PROACTIVE_DELAY_MAXIMUM"));
299
const minimum = getProactiveDelayMinimum();
300
301
if (isNaN(value) || value < msFrom.hours(3) || value > msFrom.hours(24)) {
302
-
return msFrom.hours(12);
303
}
304
305
if (value <= minimum) {
···
312
};
313
314
const getWakeTime = (): number => {
315
-
const value = Math.round(Number(Deno.env.get("WAKE_TIME")));
316
317
-
if (!value) {
318
return 8;
319
}
320
321
if (value > 23) {
322
throw Error(`"WAKE_TIME" cannot be greater than 23 (11pm)`);
323
}
···
330
};
331
332
const getSleepTime = (): number => {
333
-
const value = Math.round(Number(Deno.env.get("SLEEP_TIME")));
334
335
-
if (!value) {
336
return 10;
337
}
338
339
if (value > 23) {
···
501
);
502
};
503
504
const populateAgentContext = async (): Promise<agentContextObject> => {
505
-
console.log("building new agentContext objectโฆ");
506
const context: agentContextObject = {
507
// state
508
busy: false,
···
517
mentionCount: 0,
518
replyCount: 0,
519
quoteCount: 0,
520
// required with manual variables
521
-
lettaProjectIdentifier: getLettaProjectName(),
522
agentBskyHandle: getAgentBskyHandle(),
523
agentBskyName: await getAgentBskyName(),
524
agentBskyDID: setAgentBskyDID(),
···
540
timeZone: getTimeZone(),
541
responsiblePartyType: getResponsiblePartyType(),
542
preserveAgentMemory: getPreserveMemoryBlocks(),
543
reflectionEnabled: setReflectionEnabled(),
544
proactiveEnabled: setProactiveEnabled(),
545
sleepEnabled: setSleepEnabled(),
···
560
if (responsiblePartyBsky) {
561
context.responsiblePartyBsky = responsiblePartyBsky;
562
}
563
console.log(
564
-
`\`agentContext\` object built for ${context.agentBskyName}, BEGIN TASKโฆ`,
565
);
566
return context;
567
};
···
585
agentContext.mentionCount = 0;
586
agentContext.replyCount = 0;
587
agentContext.quoteCount = 0;
588
-
agentContext.checkCount = 0;
589
-
agentContext.processingCount = 0;
590
};
···
14
} from "./const.ts";
15
import { msFrom } from "./time.ts";
16
import { bsky } from "./bsky.ts";
17
+
import {
18
+
isAgentAsleep as checkIsAsleep,
19
+
isAgentAwake as checkIsAwake,
20
+
} from "./sleepWakeHelpers.ts";
21
22
export const getLettaApiKey = (): string => {
23
const value = Deno.env.get("LETTA_API_KEY")?.trim();
···
51
return value;
52
};
53
54
+
const getLettaProjectID = (): string => {
55
+
const value = Deno.env.get("LETTA_PROJECT_ID")?.trim();
56
57
if (!value?.length) {
58
throw Error(
59
+
"Letta Project ID not provided in `.env`. add variable `LETTA_PROJECT_ID=`.",
60
+
);
61
+
} else if (!value.includes("-")) {
62
+
throw Error(
63
+
"Letta Project ID is not formed correctly, check variable `LETTA_PROJECT_ID`",
64
);
65
}
66
67
return value;
68
};
69
70
const getAgentBskyHandle = (): string => {
71
const value = Deno.env.get("BSKY_USERNAME")?.trim();
···
209
};
210
211
const getNotifDelayMinimum = (): number => {
212
+
const value = msFrom.parse(Deno.env.get("NOTIF_DELAY_MINIMUM"));
213
214
if (isNaN(value) || value < msFrom.seconds(1) || value > msFrom.hours(24)) {
215
+
return msFrom.seconds(10);
216
}
217
218
return value;
219
};
220
221
const getNotifDelayMaximum = (): number => {
222
+
const value = msFrom.parse(Deno.env.get("NOTIF_DELAY_MAXIMUM"));
223
224
if (isNaN(value) || value < msFrom.seconds(5) || value > msFrom.hours(24)) {
225
+
return msFrom.minutes(90);
226
}
227
228
const minimum = getNotifDelayMinimum();
···
240
const value = Number(Deno.env.get("NOTIF_DELAY_MULTIPLIER"));
241
242
if (isNaN(value) || value < 0 || value > 500) {
243
+
return 1.12;
244
}
245
246
return (value / 100) + 1;
247
};
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
+
259
const getReflectionDelayMinimum = (): number => {
260
+
const value = msFrom.parse(Deno.env.get("REFLECTION_DELAY_MINIMUM"));
261
262
if (isNaN(value) || value < msFrom.minutes(30) || value > msFrom.hours(24)) {
263
+
return msFrom.hours(3);
264
}
265
266
return value;
267
};
268
269
const getReflectionDelayMaximum = (): number => {
270
+
const value = msFrom.parse(Deno.env.get("REFLECTION_DELAY_MAXIMUM"));
271
const minimum = getReflectionDelayMinimum();
272
273
if (isNaN(value) || value < msFrom.minutes(60) || value > msFrom.hours(24)) {
274
+
return msFrom.hours(14);
275
}
276
277
if (value <= minimum) {
···
284
};
285
286
const getProactiveDelayMinimum = (): number => {
287
+
const value = msFrom.parse(Deno.env.get("PROACTIVE_DELAY_MINIMUM"));
288
289
if (isNaN(value) || value < msFrom.hours(1) || value > msFrom.hours(24)) {
290
+
return msFrom.hours(3);
291
}
292
293
return value;
294
};
295
296
const getProactiveDelayMaximum = (): number => {
297
+
const value = msFrom.parse(Deno.env.get("PROACTIVE_DELAY_MAXIMUM"));
298
const minimum = getProactiveDelayMinimum();
299
300
if (isNaN(value) || value < msFrom.hours(3) || value > msFrom.hours(24)) {
301
+
return msFrom.hours(14);
302
}
303
304
if (value <= minimum) {
···
311
};
312
313
const getWakeTime = (): number => {
314
+
const envValue = Deno.env.get("WAKE_TIME");
315
316
+
if (envValue === undefined || envValue === null || envValue === "") {
317
return 8;
318
}
319
320
+
const value = Math.round(Number(envValue));
321
+
322
+
if (isNaN(value)) {
323
+
throw Error(`"WAKE_TIME" must be a valid number, got: "${envValue}"`);
324
+
}
325
+
326
if (value > 23) {
327
throw Error(`"WAKE_TIME" cannot be greater than 23 (11pm)`);
328
}
···
335
};
336
337
const getSleepTime = (): number => {
338
+
const envValue = Deno.env.get("SLEEP_TIME");
339
340
+
if (envValue === undefined || envValue === null || envValue === "") {
341
return 10;
342
+
}
343
+
344
+
const value = Math.round(Number(envValue));
345
+
346
+
if (isNaN(value)) {
347
+
throw Error(`"SLEEP_TIME" must be a valid number, got: "${envValue}"`);
348
}
349
350
if (value > 23) {
···
512
);
513
};
514
515
+
export const getExternalServices = (): string[] | undefined => {
516
+
const value = Deno.env.get("EXTERNAL_SERVICES")?.trim();
517
+
518
+
if (!value?.length) {
519
+
return undefined;
520
+
}
521
+
522
+
// Parse comma-separated list
523
+
const services = value
524
+
.split(",")
525
+
.map((service) => service.trim())
526
+
.filter((service) => service.length > 0);
527
+
528
+
if (services.length === 0) {
529
+
return undefined;
530
+
}
531
+
532
+
// Validate each service string
533
+
for (const service of services) {
534
+
if (service.length > 200) {
535
+
throw Error(
536
+
`External service name too long: "${
537
+
service.substring(0, 50)
538
+
}..." (max 200 characters)`,
539
+
);
540
+
}
541
+
}
542
+
543
+
// Validate array length
544
+
if (services.length > 20) {
545
+
throw Error(
546
+
`Too many external services specified: ${services.length} (max 20)`,
547
+
);
548
+
}
549
+
550
+
return services;
551
+
};
552
+
553
const populateAgentContext = async (): Promise<agentContextObject> => {
554
+
console.log("๐น building new agentContext objectโฆ");
555
const context: agentContextObject = {
556
// state
557
busy: false,
···
566
mentionCount: 0,
567
replyCount: 0,
568
quoteCount: 0,
569
+
notifCount: 0,
570
// required with manual variables
571
+
lettaProjectIdentifier: getLettaProjectID(),
572
agentBskyHandle: getAgentBskyHandle(),
573
agentBskyName: await getAgentBskyName(),
574
agentBskyDID: setAgentBskyDID(),
···
590
timeZone: getTimeZone(),
591
responsiblePartyType: getResponsiblePartyType(),
592
preserveAgentMemory: getPreserveMemoryBlocks(),
593
+
maxThreadPosts: getMaxThreadPosts(),
594
reflectionEnabled: setReflectionEnabled(),
595
proactiveEnabled: setProactiveEnabled(),
596
sleepEnabled: setSleepEnabled(),
···
611
if (responsiblePartyBsky) {
612
context.responsiblePartyBsky = responsiblePartyBsky;
613
}
614
+
615
+
const externalServices = getExternalServices();
616
+
if (externalServices) {
617
+
context.externalServices = externalServices;
618
+
}
619
console.log(
620
+
`๐น \`agentContext\` object built for ${context.agentBskyName}, BEGINING TASKSโฆ`,
621
);
622
return context;
623
};
···
641
agentContext.mentionCount = 0;
642
agentContext.replyCount = 0;
643
agentContext.quoteCount = 0;
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
};
+39
-89
utils/declaration.ts
+39
-89
utils/declaration.ts
···
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",
11
-
"key": "literal:self",
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");
···
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
···
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;
···
227
});
228
229
console.log(
230
-
`Autonomy declaration ${exists ? "updated" : "created"} successfully:`,
231
result,
232
);
233
return result;
···
1
import { bsky } from "../utils/bsky.ts";
2
+
import type { AutonomyDeclaration } from "@voyager/autonomy-lexicon";
3
+
import { AUTONOMY_DECLARATION_LEXICON } from "@voyager/autonomy-lexicon";
4
import { Lexicons } from "@atproto/lexicon";
5
+
import { agentContext } from "./agentContext.ts";
6
7
+
/**
8
+
* AT Protocol record type that includes the $type property
9
+
* Includes index signature for compatibility with AT Protocol API
10
+
*/
11
+
type AutonomyDeclarationRecord = AutonomyDeclaration & {
12
+
$type: "studio.voyager.account.autonomy";
13
+
[key: string]: unknown;
14
};
15
16
+
/**
17
+
* Autonomy Declaration Lexicon
18
+
*
19
+
* The schema is imported from @voyager/autonomy-lexicon package and
20
+
* is published at voyager.studio for use by all Cloudseeding instances.
21
+
*
22
+
* Schema vs. Records:
23
+
* - The SCHEMA is published once by voyager.studio (domain owner)
24
+
* - Each agent creates their own RECORD using this schema
25
+
*
26
+
* Template users do NOT need to publish this schema - it's already
27
+
* published and discoverable via DNS resolution. They only need to
28
+
* create their own autonomy declaration record (done automatically
29
+
* by submitAutonomyDeclarationRecord below).
30
+
*
31
+
* Canonical source: jsr:@voyager/autonomy-lexicon
32
+
*/
33
+
export { AUTONOMY_DECLARATION_LEXICON };
34
+
35
export const createAutonomyDeclarationRecord = async () => {
36
const automationLevel = Deno.env.get("AUTOMATION_LEVEL")?.toLowerCase();
37
const projectDescription = Deno.env.get("PROJECT_DESCRIPTION");
···
62
// Add disclosure URL if provided
63
if (disclosureUrl?.trim()) {
64
declarationRecord.disclosureUrl = disclosureUrl.trim();
65
+
}
66
+
67
+
// Add external services from agentContext (already parsed and validated)
68
+
if (agentContext.externalServices) {
69
+
declarationRecord.externalServices = agentContext.externalServices;
70
}
71
72
// Build responsible party object if any fields are provided
···
149
rkey: "self",
150
});
151
exists = true;
152
+
console.log("๐น Existing autonomy declaration found - updating...");
153
} catch (error: any) {
154
// Handle "record not found" errors (status 400 with error: "RecordNotFound")
155
const isNotFound =
···
159
error?.message?.includes("Could not locate record");
160
161
if (isNotFound) {
162
+
console.log(
163
+
"๐น No existing autonomy declaration found - creating new...",
164
+
);
165
} else {
166
// Re-throw if it's not a "not found" error
167
throw error;
···
177
});
178
179
console.log(
180
+
`๐น Autonomy declaration ${exists ? "updated" : "created"} successfully:`,
181
result,
182
);
183
return result;
+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
+
};
+157
-34
utils/messageAgent.ts
+157
-34
utils/messageAgent.ts
···
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 {
6
const parsed = typeof args === "string" ? JSON.parse(args) : args;
7
if (typeof parsed !== "object" || parsed === null) {
···
9
}
10
return Object.entries(parsed)
11
.map(([key, value]) => {
12
-
const valueStr = typeof value === "object"
13
? JSON.stringify(value)
14
: String(value);
15
return `${key}=${valueStr}`;
16
})
17
.join(", ");
···
21
};
22
23
// Helper function to truncate long strings to 500 characters
24
-
const truncateString = (str: string, maxLength = 500): string => {
25
if (str.length <= maxLength) {
26
return str;
27
}
28
return `${str.slice(0, maxLength)}... (truncated, ${str.length} total chars)`;
29
};
30
31
-
export const client = new LettaClient({
32
-
token: Deno.env.get("LETTA_API_KEY"),
33
-
project: Deno.env.get("LETTA_PROJECT_NAME"),
34
});
35
36
export const messageAgent = async (prompt: string) => {
37
const agent = Deno.env.get("LETTA_AGENT_ID");
38
39
if (agent) {
40
-
const reachAgent = await client.agents.messages.createStream(agent, {
41
messages: [
42
{
43
-
role: "user",
44
-
content: [
45
-
{
46
-
type: "text",
47
-
text: prompt,
48
-
},
49
-
],
50
},
51
],
52
-
streamTokens: true,
53
});
54
55
for await (const response of reachAgent) {
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(
63
-
`๐๏ธ tool called: ${response.toolCall.name} with args: ${formattedArgs}`,
64
-
);
65
-
} else if (response.messageType === "tool_return_message") {
66
-
const toolReturn = response.toolReturn;
67
-
const returnStr = typeof toolReturn === "string"
68
-
? toolReturn
69
-
: JSON.stringify(toolReturn);
70
-
console.log(`๐ง tool response: ${truncateString(returnStr)}`);
71
-
} else if (response.messageType === "usage_statistics") {
72
-
console.log(`๐ข total steps: ${response.stepCount}`);
73
-
} else if (response.messageType === "hidden_reasoning_message") {
74
console.log(`hidden reasoningโฆ`);
75
}
76
}
77
} else {
78
console.log(
79
-
"Letta agent ID was not a set variable, skipping notification processingโฆ",
80
);
81
}
82
};
···
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) {
···
9
}
10
return Object.entries(parsed)
11
.map(([key, value]) => {
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
})
26
.join(", ");
···
30
};
31
32
// Helper function to truncate long strings to 500 characters
33
+
const truncateString = (str: string, maxLength = 140): string => {
34
if (str.length <= maxLength) {
35
return str;
36
}
37
return `${str.slice(0, maxLength)}... (truncated, ${str.length} total chars)`;
38
};
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
+
149
+
export const client = new Letta({
150
+
apiKey: Deno.env.get("LETTA_API_KEY"),
151
+
// @ts-ignore: Letta SDK type definition might be slightly off or expecting different casing
152
+
projectId: Deno.env.get("LETTA_PROJECT_ID"),
153
});
154
155
export const messageAgent = async (prompt: string) => {
156
const agent = Deno.env.get("LETTA_AGENT_ID");
157
158
if (agent) {
159
+
const reachAgent = await client.agents.messages.stream(agent, {
160
messages: [
161
{
162
+
role: "system",
163
+
content: prompt,
164
},
165
],
166
+
stream_tokens: true,
167
});
168
169
+
let lastToolName = "";
170
+
171
for await (const response of reachAgent) {
172
+
if (response.message_type === "reasoning_message") {
173
+
// console.log(`๐ญ reasoningโฆ`);
174
+
} else if (response.message_type === "assistant_message") {
175
console.log(`๐ฌ ${agentContext.agentBskyName}: ${response.content}`);
176
+
} else if (response.message_type === "tool_call_message") {
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})`);
184
+
}
185
+
} else if (response.message_type === "tool_return_message") {
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));
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") {
197
console.log(`hidden reasoningโฆ`);
198
}
199
}
200
} else {
201
console.log(
202
+
"๐น Letta agent ID was not a set variable, skipping notification processingโฆ",
203
);
204
}
205
};
+6
-5
utils/processNotification.ts
+6
-5
utils/processNotification.ts
···
37
} as const;
38
39
export const processNotification = async (notification: Notification) => {
40
-
const agentProject = Deno.env.get("LETTA_PROJECT_NAME");
41
const kind = notification.reason;
42
const author = `@${notification.author.handle}`;
43
const handler = notificationHandlers[kind];
44
45
if (!handler) {
46
console.log(
47
-
`kind "${kind}" does not have a system prompt associated with it, moving onโฆ`,
48
);
49
console.log("notification response: ", notification);
50
return;
···
54
const prompt = await handler.promptFn(notification);
55
await messageAgent(prompt);
56
console.log(
57
-
`sent ${kind} notification from ${author} to ${agentProject}. moving onโฆ`,
58
);
59
} catch (error) {
60
console.log(
61
-
`Error processing ${kind} notification from ${author}: `,
62
error,
63
);
64
} finally {
65
-
(agentContext as any)[handler]++;
66
}
67
};
···
37
} as const;
38
39
export const processNotification = async (notification: Notification) => {
40
+
const agentName = agentContext.agentBskyName;
41
const kind = notification.reason;
42
const author = `@${notification.author.handle}`;
43
const handler = notificationHandlers[kind];
44
45
if (!handler) {
46
console.log(
47
+
`๐น kind "${kind}" does not have a system prompt associated with it, moving onโฆ`,
48
);
49
console.log("notification response: ", notification);
50
return;
···
54
const prompt = await handler.promptFn(notification);
55
await messageAgent(prompt);
56
console.log(
57
+
`๐น sent ${kind} notification from ${author} to ${agentName}. moving onโฆ`,
58
);
59
} catch (error) {
60
console.log(
61
+
`๐น Error processing ${kind} notification from ${author}: `,
62
error,
63
);
64
} finally {
65
+
(agentContext as any)[handler.counter]++;
66
+
agentContext.notifCount++;
67
}
68
};
+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
+
};
+81
utils/time.ts
+81
utils/time.ts
···
2
import { Temporal } from "@js-temporal/polyfill";
3
4
/**
5
* Convert time units to milliseconds
6
*/
7
export const msFrom = {
···
20
* @param h - number of hours
21
*/
22
hours: (hours: number): number => hours * 60 * 60 * 1000,
23
};
24
25
/**
···
127
export const getNow = () => {
128
return Temporal.Now.zonedDateTimeISO(agentContext.timeZone);
129
};
···
2
import { Temporal } from "@js-temporal/polyfill";
3
4
/**
5
+
* Parse a time string with unit suffix or raw milliseconds
6
+
* @param value - Time string like "10s", "90m", "3h" or raw milliseconds
7
+
* @returns Time in milliseconds
8
+
* @example
9
+
* parseTimeValue("10s") // โ 10000
10
+
* parseTimeValue("90m") // โ 5400000
11
+
* parseTimeValue("3h") // โ 10800000
12
+
* parseTimeValue("5400000") // โ 5400000 (backward compat)
13
+
* parseTimeValue(10000) // โ 10000 (already a number)
14
+
*/
15
+
function parseTimeValue(value: string | number | undefined): number {
16
+
if (value === undefined || value === "") {
17
+
throw new Error("Time value is required");
18
+
}
19
+
20
+
if (typeof value === "number") {
21
+
return value;
22
+
}
23
+
24
+
const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|ms)?$/i);
25
+
26
+
if (!match) {
27
+
throw new Error(
28
+
`Invalid time format: "${value}". Expected: "10s", "90m", "3h", or raw milliseconds`,
29
+
);
30
+
}
31
+
32
+
const [, numStr, unit] = match;
33
+
const num = parseFloat(numStr);
34
+
35
+
if (isNaN(num) || num < 0) {
36
+
throw new Error(`Time value must be a positive number: "${value}"`);
37
+
}
38
+
39
+
switch (unit?.toLowerCase()) {
40
+
case "s":
41
+
return msFrom.seconds(num);
42
+
case "m":
43
+
return msFrom.minutes(num);
44
+
case "h":
45
+
return msFrom.hours(num);
46
+
case "ms":
47
+
case undefined:
48
+
return num;
49
+
default:
50
+
throw new Error(`Invalid unit: "${unit}". Use s/m/h/ms`);
51
+
}
52
+
}
53
+
54
+
/**
55
* Convert time units to milliseconds
56
*/
57
export const msFrom = {
···
70
* @param h - number of hours
71
*/
72
hours: (hours: number): number => hours * 60 * 60 * 1000,
73
+
/**
74
+
* Parse a time string with unit suffix (e.g., "10s", "90m", "3h") or raw milliseconds
75
+
* @param value - Time string or number
76
+
* @returns Time in milliseconds
77
+
*/
78
+
parse: parseTimeValue,
79
};
80
81
/**
···
183
export const getNow = () => {
184
return Temporal.Now.zonedDateTimeISO(agentContext.timeZone);
185
};
186
+
187
+
/**
188
+
* Format uptime from milliseconds into a human-readable string
189
+
* @param ms - uptime in milliseconds
190
+
* @returns Formatted string like "2 days, 3 hours, 15 minutes" or "3 hours, 15 minutes"
191
+
*/
192
+
export const formatUptime = (ms: number): string => {
193
+
const days = Math.floor(ms / (1000 * 60 * 60 * 24));
194
+
const hours = Math.floor((ms % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
195
+
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
196
+
197
+
const parts: string[] = [];
198
+
199
+
if (days > 0) {
200
+
parts.push(`${days} ${days === 1 ? "day" : "days"}`);
201
+
}
202
+
if (hours > 0) {
203
+
parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`);
204
+
}
205
+
if (minutes > 0 || parts.length === 0) {
206
+
parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`);
207
+
}
208
+
209
+
return parts.join(", ");
210
+
};
+17
-29
utils/types.ts
+17
-29
utils/types.ts
···
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
···
31
mentionCount: number;
32
replyCount: number;
33
quoteCount: number;
34
// required manual variables
35
lettaProjectIdentifier: string;
36
agentBskyHandle: string;
···
54
timeZone: string;
55
responsiblePartyType: string; // person / organization
56
preserveAgentMemory: boolean; // if true, mount won't update existing memory blocks
57
// set automatically
58
agentBskyDID: string;
59
reflectionEnabled: boolean;
···
64
automationDescription?: string; // short description of what this agent does
65
disclosureUrl?: string; // url to a ToS/Privacy Policy style page
66
responsiblePartyBsky?: string; // handle w/o @ or DID of responsible party
67
-
};
68
-
69
-
export type AutonomyDeclarationRecord = {
70
-
$type: "studio.voyager.account.autonomy";
71
-
72
-
// How automated is this account?
73
-
automationLevel?: "human" | "assisted" | "collaborative" | "automated";
74
-
75
-
// Is AI involved in content creation?
76
-
usesGenerativeAI?: boolean;
77
-
78
-
// Plain language explanation
79
-
description?: string; // maxGraphemes: 300
80
-
81
-
// Who is accountable for this account?
82
-
responsibleParty?: {
83
-
type?: "person" | "organization";
84
-
name?: string;
85
-
contact?: string; // email, URL, handle, or DID
86
-
did?: string; // ATProto DID
87
-
};
88
-
89
-
// Where can someone learn more?
90
-
disclosureUrl?: string; // URI format
91
-
92
-
// When was this declaration created?
93
-
createdAt: string; // ISO datetime (required)
94
};
95
96
export type memoryBlock = {
···
6
validAutomationLevels,
7
validNotifTypes,
8
} from "./const.ts";
9
+
import type {
10
+
AutomationLevel,
11
+
AutonomyDeclaration,
12
+
ResponsibleParty,
13
+
ResponsiblePartyType,
14
+
} from "@voyager/autonomy-lexicon";
15
+
16
export type Notification = AppBskyNotificationListNotifications.Notification;
17
18
+
// Re-export types from autonomy-lexicon package
19
+
export type {
20
+
AutomationLevel,
21
+
AutonomyDeclaration,
22
+
ResponsibleParty,
23
+
ResponsiblePartyType,
24
+
};
25
26
export type notifType = typeof validNotifTypes[number];
27
···
43
mentionCount: number;
44
replyCount: number;
45
quoteCount: number;
46
+
notifCount: number;
47
// required manual variables
48
lettaProjectIdentifier: string;
49
agentBskyHandle: string;
···
67
timeZone: string;
68
responsiblePartyType: string; // person / organization
69
preserveAgentMemory: boolean; // if true, mount won't update existing memory blocks
70
+
maxThreadPosts: number; // maximum number of posts to include in thread context
71
// set automatically
72
agentBskyDID: string;
73
reflectionEnabled: boolean;
···
78
automationDescription?: string; // short description of what this agent does
79
disclosureUrl?: string; // url to a ToS/Privacy Policy style page
80
responsiblePartyBsky?: string; // handle w/o @ or DID of responsible party
81
+
externalServices?: string[]; // external tools/services this agent relies on
82
};
83
84
export type memoryBlock = {