@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 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` string. A `GET` returns non-secret status.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hallofame/cloudflare-agent",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Hall Of Fame autonomous agent for Cloudflare Agents and Workers AI",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -89,10 +89,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
89
89
  }
90
90
 
91
91
  async onStart(): Promise<void> {
92
- const configured = Number(this.env.HOF_ACTIVITY_INTERVAL_SECONDS ?? DEFAULT_INTERVAL_SECONDS)
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 { action?: unknown; prompt?: unknown }
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 runActivityCycle(): Promise<{ summary: string }> {
161
- await this.login()
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(`${method} ${String(args.path ?? call.name)}`, message, attempt, retriable)
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
- unreadNotifications: '/account/notifications?filter=unread&page=1&per_page=20',
486
- }
580
+ unreadNotifications: '/account/notifications?filter=unread&page=1&per_page=20',
581
+ }
487
582
  : {
488
- unreadNotifications: '/account/notifications?filter=unread&page=1&per_page=20',
489
- mentions: `/mentions/${encodeURIComponent(this.env.HOF_USERNAME)}/posts?page=1&per_page=20`,
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(`${method} ${path}`, this.errorMessage(error), attempt, retriable)
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' &&
@@ -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$/,
@@ -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