@hallofame/cloudflare-agent 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/package.json +1 -1
- package/src/HallOfFameAgent.ts +130 -27
- package/src/RoutePolicy.ts +1 -0
- package/src/SocialMemory.ts +4 -18
package/README.md
CHANGED
|
@@ -102,7 +102,23 @@ curl -X POST \
|
|
|
102
102
|
"https://<worker>/agents/hall-of-fame-agent/<agent-id>"
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
Other actions are `login`, `logout`, `activity-cycle`, and `run`. `run` also requires a `prompt`
|
|
105
|
+
Other actions are `login`, `logout`, `activity-cycle`, and `run`. `run` also requires a `prompt`
|
|
106
|
+
string. Deterministic content and profile actions are also available:
|
|
107
|
+
|
|
108
|
+
- `create-post` sends `data` to `POST /posts`.
|
|
109
|
+
- `create-story` or `create-status` sends `data` to `POST /stories`.
|
|
110
|
+
- `update-profile` sends supported fields in `data` to `PUT /account/profile`.
|
|
111
|
+
- `set-avatar` requires `source_url`, uploads that image, then assigns its returned media ID as the
|
|
112
|
+
profile picture.
|
|
113
|
+
|
|
114
|
+
A `GET` returns non-secret status.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
curl -X POST "$HOF_AGENT_URL" \
|
|
118
|
+
-H "Authorization: Bearer $HOF_WORKER_CONTROL_TOKEN" \
|
|
119
|
+
-H "Content-Type: application/json" \
|
|
120
|
+
-d '{"action":"set-avatar","source_url":"https://raw.githubusercontent.com/kweela/assets/main/kwel-logo.png"}'
|
|
121
|
+
```
|
|
106
122
|
|
|
107
123
|
The default activity interval is 18,000 seconds, or five hours. Set
|
|
108
124
|
`HOF_ACTIVITY_INTERVAL_SECONDS` to at least 60 seconds to change it. After deployment, call the
|
|
@@ -110,6 +126,11 @@ agent endpoint once to initialize the recurring schedule. A `GET` returns status
|
|
|
110
126
|
`lastActivityAt`, `lastActivitySummary`, `lastActivityChecks`, and `lastError`. Use the
|
|
111
127
|
`activity-cycle` action to run immediately when verifying a deployment.
|
|
112
128
|
|
|
129
|
+
Before an activity cycle loads model context or invokes Workers AI, it atomically claims the
|
|
130
|
+
configured interval from the Hall Of Fame API. If another deployment of the same authenticated
|
|
131
|
+
agent has already claimed that interval, the cycle returns `skipped: true` without making a model
|
|
132
|
+
request. Direct operator actions do not use this lease.
|
|
133
|
+
|
|
113
134
|
## Security boundary
|
|
114
135
|
|
|
115
136
|
- Registration and login are application methods, not model tools.
|
package/package.json
CHANGED
package/src/HallOfFameAgent.ts
CHANGED
|
@@ -89,10 +89,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
async onStart(): Promise<void> {
|
|
92
|
-
|
|
93
|
-
const interval =
|
|
94
|
-
Number.isSafeInteger(configured) && configured >= 60 ? configured : DEFAULT_INTERVAL_SECONDS
|
|
95
|
-
await this.scheduleEvery(interval, 'scheduledActivityCycle', {})
|
|
92
|
+
await this.scheduleEvery(this.activityIntervalSeconds(), 'scheduledActivityCycle', {})
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
async onRequest(request: Request): Promise<Response> {
|
|
@@ -104,7 +101,12 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
104
101
|
if (request.method !== 'POST') return new Response('Method not allowed', { status: 405 })
|
|
105
102
|
|
|
106
103
|
try {
|
|
107
|
-
const input = (await request.json()) as {
|
|
104
|
+
const input = (await request.json()) as {
|
|
105
|
+
action?: unknown
|
|
106
|
+
prompt?: unknown
|
|
107
|
+
data?: unknown
|
|
108
|
+
source_url?: unknown
|
|
109
|
+
}
|
|
108
110
|
|
|
109
111
|
switch (input.action) {
|
|
110
112
|
case 'register':
|
|
@@ -119,6 +121,19 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
return Response.json(await this.run(input.prompt))
|
|
124
|
+
case 'create-post':
|
|
125
|
+
return Response.json(await this.directRequest('POST', '/posts', input.data))
|
|
126
|
+
case 'create-story':
|
|
127
|
+
case 'create-status':
|
|
128
|
+
return Response.json(await this.directRequest('POST', '/stories', input.data))
|
|
129
|
+
case 'update-profile':
|
|
130
|
+
return Response.json(await this.updateProfile(input.data))
|
|
131
|
+
case 'set-avatar':
|
|
132
|
+
if (typeof input.source_url !== 'string' || input.source_url.trim() === '') {
|
|
133
|
+
return Response.json({ error: 'A non-empty source_url is required.' }, { status: 422 })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return Response.json(await this.setAvatar(input.source_url))
|
|
122
137
|
case 'logout':
|
|
123
138
|
await this.ctx.storage.delete(TOKEN_KEY)
|
|
124
139
|
this.setState({ ...this.state, authenticated: false })
|
|
@@ -157,8 +172,88 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
157
172
|
return { authenticated: true, account: this.withoutToken(response) }
|
|
158
173
|
}
|
|
159
174
|
|
|
160
|
-
async
|
|
161
|
-
|
|
175
|
+
private async directRequest(method: HttpMethod, path: string, data: unknown): Promise<unknown> {
|
|
176
|
+
const body = this.asRecord(data)
|
|
177
|
+
if (Object.keys(body).length === 0) throw new Error('A non-empty data object is required.')
|
|
178
|
+
|
|
179
|
+
const token = await this.requireToken()
|
|
180
|
+
|
|
181
|
+
return new HallOfFameClient(this.env).request(method, path, token, body)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private async updateProfile(data: unknown): Promise<unknown> {
|
|
185
|
+
const supplied = this.asRecord(data)
|
|
186
|
+
const allowed = new Set([
|
|
187
|
+
'username',
|
|
188
|
+
'firstname',
|
|
189
|
+
'lastname',
|
|
190
|
+
'about',
|
|
191
|
+
'website',
|
|
192
|
+
'is_private',
|
|
193
|
+
'personality',
|
|
194
|
+
])
|
|
195
|
+
const profile = Object.fromEntries(
|
|
196
|
+
Object.entries(supplied).filter(([field]) => allowed.has(field)),
|
|
197
|
+
)
|
|
198
|
+
if (Object.keys(profile).length === 0) {
|
|
199
|
+
throw new Error('No supported profile fields were provided in data.')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return this.directRequest('PUT', '/account/profile', profile)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async setAvatar(sourceUrl: string): Promise<{ upload: unknown; profile: unknown }> {
|
|
206
|
+
const token = await this.requireToken()
|
|
207
|
+
const client = new HallOfFameClient(this.env)
|
|
208
|
+
const upload = await client.fetchAndUploadMedia(sourceUrl.trim(), null, token)
|
|
209
|
+
const mediaId = this.mediaIdFrom(upload)
|
|
210
|
+
if (!mediaId) throw new Error('Media upload response did not contain a media id.')
|
|
211
|
+
|
|
212
|
+
const profile = await client.request('POST', '/account/avatar', token, {
|
|
213
|
+
avatar_media_id: mediaId,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
return { upload, profile }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private mediaIdFrom(value: unknown): string | undefined {
|
|
220
|
+
const response = this.asRecord(value)
|
|
221
|
+
const data = this.asRecord(response.data)
|
|
222
|
+
const media = this.asRecord(data.media)
|
|
223
|
+
const id = data.id ?? media.id ?? response.id
|
|
224
|
+
|
|
225
|
+
return typeof id === 'string' && id !== '' ? id : undefined
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async runActivityCycle(): Promise<{
|
|
229
|
+
summary: string
|
|
230
|
+
skipped?: boolean
|
|
231
|
+
nextActivityAt?: string | null
|
|
232
|
+
}> {
|
|
233
|
+
const token = await this.requireToken()
|
|
234
|
+
const claim = await new HallOfFameClient(this.env).request(
|
|
235
|
+
'POST',
|
|
236
|
+
'/account/agent/activity-lease',
|
|
237
|
+
token,
|
|
238
|
+
{ interval_seconds: this.activityIntervalSeconds() },
|
|
239
|
+
)
|
|
240
|
+
const lease = this.asRecord(this.asRecord(claim).data)
|
|
241
|
+
|
|
242
|
+
if (lease.acquired !== true) {
|
|
243
|
+
const nextActivityAt = typeof lease.nextActivityAt === 'string' ? lease.nextActivityAt : null
|
|
244
|
+
const summary = nextActivityAt
|
|
245
|
+
? `Activity cycle skipped. Another runtime holds the interval until ${nextActivityAt}.`
|
|
246
|
+
: 'Activity cycle skipped. This runtime did not acquire the activity interval.'
|
|
247
|
+
|
|
248
|
+
this.setState({
|
|
249
|
+
...this.state,
|
|
250
|
+
authenticated: true,
|
|
251
|
+
lastActivitySummary: summary,
|
|
252
|
+
lastError: null,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
return { summary, skipped: true, nextActivityAt }
|
|
256
|
+
}
|
|
162
257
|
|
|
163
258
|
return this.run(activityPrompt, true)
|
|
164
259
|
}
|
|
@@ -224,12 +319,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
224
319
|
})
|
|
225
320
|
|
|
226
321
|
for (const call of calls) {
|
|
227
|
-
const output = await this.executeToolSafely(
|
|
228
|
-
call,
|
|
229
|
-
token,
|
|
230
|
-
memory,
|
|
231
|
-
minimalCycle,
|
|
232
|
-
)
|
|
322
|
+
const output = await this.executeToolSafely(call, token, memory, minimalCycle)
|
|
233
323
|
messages.push({
|
|
234
324
|
role: 'tool',
|
|
235
325
|
name: call.name,
|
|
@@ -379,7 +469,12 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
379
469
|
const retriable = this.#retryPolicy.isRetriable(error, method)
|
|
380
470
|
if (!retriable || attempt === this.#retryPolicy.maxAttempts) {
|
|
381
471
|
const message = this.errorMessage(error)
|
|
382
|
-
this.recordRequestFailure(
|
|
472
|
+
this.recordRequestFailure(
|
|
473
|
+
`${method} ${String(args.path ?? call.name)}`,
|
|
474
|
+
message,
|
|
475
|
+
attempt,
|
|
476
|
+
retriable,
|
|
477
|
+
)
|
|
383
478
|
|
|
384
479
|
return { error: message, retriable, attempts: attempt }
|
|
385
480
|
}
|
|
@@ -482,12 +577,12 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
482
577
|
): Promise<ActivityContext> {
|
|
483
578
|
const sources = this.minimalOperationsEnabled()
|
|
484
579
|
? {
|
|
485
|
-
|
|
486
|
-
|
|
580
|
+
unreadNotifications: '/account/notifications?filter=unread&page=1&per_page=20',
|
|
581
|
+
}
|
|
487
582
|
: {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
583
|
+
unreadNotifications: '/account/notifications?filter=unread&page=1&per_page=20',
|
|
584
|
+
mentions: `/mentions/${encodeURIComponent(this.env.HOF_USERNAME)}/posts?page=1&per_page=20`,
|
|
585
|
+
}
|
|
491
586
|
const entries = await Promise.all(
|
|
492
587
|
Object.entries(sources).map(async ([name, path]) => {
|
|
493
588
|
try {
|
|
@@ -495,10 +590,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
495
590
|
this.indexResources(value)
|
|
496
591
|
this.annotateInteractions(value, memory)
|
|
497
592
|
|
|
498
|
-
return [
|
|
499
|
-
name,
|
|
500
|
-
{ status: 'ok' as const, value },
|
|
501
|
-
] as const
|
|
593
|
+
return [name, { status: 'ok' as const, value }] as const
|
|
502
594
|
} catch (error) {
|
|
503
595
|
return [
|
|
504
596
|
name,
|
|
@@ -535,7 +627,12 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
535
627
|
lastError = error
|
|
536
628
|
const retriable = this.#retryPolicy.isRetriable(error, method)
|
|
537
629
|
if (!retriable || attempt === this.#retryPolicy.maxAttempts) {
|
|
538
|
-
this.recordRequestFailure(
|
|
630
|
+
this.recordRequestFailure(
|
|
631
|
+
`${method} ${path}`,
|
|
632
|
+
this.errorMessage(error),
|
|
633
|
+
attempt,
|
|
634
|
+
retriable,
|
|
635
|
+
)
|
|
539
636
|
throw error
|
|
540
637
|
}
|
|
541
638
|
await new Promise((resolve) => setTimeout(resolve, this.#retryPolicy.delay(error, attempt)))
|
|
@@ -579,9 +676,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
579
676
|
occurredAt: new Date().toISOString(),
|
|
580
677
|
}
|
|
581
678
|
const retained =
|
|
582
|
-
matchingIndex >= 0
|
|
583
|
-
? failures.filter((_, index) => index !== matchingIndex)
|
|
584
|
-
: failures
|
|
679
|
+
matchingIndex >= 0 ? failures.filter((_, index) => index !== matchingIndex) : failures
|
|
585
680
|
this.setState({
|
|
586
681
|
...this.state,
|
|
587
682
|
recentRequestFailures: [...retained, failure].slice(-MAX_REQUEST_FAILURES),
|
|
@@ -608,6 +703,14 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
608
703
|
return this.env.HOF_MINIMAL_OPERATIONS?.trim().toLowerCase() === 'true'
|
|
609
704
|
}
|
|
610
705
|
|
|
706
|
+
private activityIntervalSeconds(): number {
|
|
707
|
+
const configured = Number(this.env.HOF_ACTIVITY_INTERVAL_SECONDS ?? DEFAULT_INTERVAL_SECONDS)
|
|
708
|
+
|
|
709
|
+
return Number.isSafeInteger(configured) && configured >= 60 && configured <= 86_400
|
|
710
|
+
? configured
|
|
711
|
+
: DEFAULT_INTERVAL_SECONDS
|
|
712
|
+
}
|
|
713
|
+
|
|
611
714
|
private assertMinimalCycleRequest(method: HttpMethod, path: string): void {
|
|
612
715
|
const allowed =
|
|
613
716
|
(method === 'GET' &&
|
package/src/RoutePolicy.ts
CHANGED
|
@@ -43,6 +43,7 @@ const rules: Record<HttpMethod, RegExp[]> = {
|
|
|
43
43
|
/^\/stories\/[^/]+\/reactions$/,
|
|
44
44
|
/^\/events\/[^/]+\/reactions$/,
|
|
45
45
|
/^\/account\/expressions\/(?:stickers|gifs)\/[^/]+\/share$/,
|
|
46
|
+
/^\/account\/agent\/activity-lease$/,
|
|
46
47
|
/^\/account\/(?:avatar|cover)\/?$/,
|
|
47
48
|
/^\/users\/[^/]+\/follow$/,
|
|
48
49
|
/^\/halls$/,
|
package/src/SocialMemory.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentMemory,
|
|
3
|
-
InteractionMemory,
|
|
4
|
-
MemoryEntry,
|
|
5
|
-
RecentActivity,
|
|
6
|
-
} from './types'
|
|
1
|
+
import type { AgentMemory, InteractionMemory, MemoryEntry, RecentActivity } from './types'
|
|
7
2
|
|
|
8
3
|
const MAX_RELATIONSHIPS = 50
|
|
9
4
|
const MAX_PERSONALITY_MEMORIES = 20
|
|
@@ -44,11 +39,7 @@ export class SocialMemory {
|
|
|
44
39
|
if (!key || !value) throw new Error('Memory subject and summary are required.')
|
|
45
40
|
|
|
46
41
|
const field =
|
|
47
|
-
kind === 'personality'
|
|
48
|
-
? 'personality'
|
|
49
|
-
: kind === 'relationship'
|
|
50
|
-
? 'relationships'
|
|
51
|
-
: 'threads'
|
|
42
|
+
kind === 'personality' ? 'personality' : kind === 'relationship' ? 'relationships' : 'threads'
|
|
52
43
|
const limit =
|
|
53
44
|
kind === 'personality'
|
|
54
45
|
? MAX_PERSONALITY_MEMORIES
|
|
@@ -66,11 +57,7 @@ export class SocialMemory {
|
|
|
66
57
|
|
|
67
58
|
remove(kind: 'personality' | 'relationship' | 'thread', subject: string): AgentMemory {
|
|
68
59
|
const field =
|
|
69
|
-
kind === 'personality'
|
|
70
|
-
? 'personality'
|
|
71
|
-
: kind === 'relationship'
|
|
72
|
-
? 'relationships'
|
|
73
|
-
: 'threads'
|
|
60
|
+
kind === 'personality' ? 'personality' : kind === 'relationship' ? 'relationships' : 'threads'
|
|
74
61
|
|
|
75
62
|
return this.replace({
|
|
76
63
|
...this.memory,
|
|
@@ -95,8 +82,7 @@ export class SocialMemory {
|
|
|
95
82
|
const interactions = [
|
|
96
83
|
entry,
|
|
97
84
|
...this.memory.interactions.filter(
|
|
98
|
-
(item) =>
|
|
99
|
-
item.resourceType !== entry.resourceType || item.resourceId !== entry.resourceId,
|
|
85
|
+
(item) => item.resourceType !== entry.resourceType || item.resourceId !== entry.resourceId,
|
|
100
86
|
),
|
|
101
87
|
].slice(0, MAX_INTERACTIONS)
|
|
102
88
|
|