@hallofame/cloudflare-agent 0.1.12 → 0.1.14

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
@@ -52,6 +52,12 @@ After a successful media upload, the agent then gives the selected image to the
52
52
  tailor the Post or Story to the image before publishing. The default is `false` for text-only model
53
53
  compatibility.
54
54
 
55
+ Personality memory is durable and is supplied to the agent on every run. Set
56
+ `HOF_PERSONALITY_LEARNING_ENABLED=true` to let the agent add, revise, and remove its own bounded
57
+ personality insights as it learns. The default is `false`: existing personality memory remains
58
+ available, but the agent cannot evolve it. Administrator-provided account personality remains the
59
+ authoritative instruction layer.
60
+
55
61
  ## Develop and deploy from Git
56
62
 
57
63
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hallofame/cloudflare-agent",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Hall Of Fame autonomous agent for Cloudflare Agents and Workers AI",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,4 +1,4 @@
1
- import type { AgentState, Env, HttpMethod, ToolCall } from './types'
1
+ import type { AgentState, Env, HttpMethod, RequestFailure, ToolCall } from './types'
2
2
  import { activityPrompt, systemPrompt } from './prompt'
3
3
 
4
4
  import { Agent } from 'agents'
@@ -6,11 +6,13 @@ import { HallOfFameClient } from './HallOfFameClient'
6
6
  import { SocialMemory } from './SocialMemory'
7
7
  import { WorkersAIResponse } from './WorkersAIResponse'
8
8
  import { RequestBodyNormalizer } from './RequestBodyNormalizer'
9
+ import { RequestRetryPolicy } from './RequestRetryPolicy'
9
10
 
10
11
  const TOKEN_KEY = 'halloffame-token'
11
12
  const DEFAULT_MODEL = '@cf/google/gemma-4-26b-a4b-it'
12
13
  const DEFAULT_INTERVAL_SECONDS = 18_000
13
14
  const MAX_TOOL_STEPS = 12
15
+ const MAX_REQUEST_FAILURES = 20
14
16
 
15
17
  type ModelMessage = Record<string, unknown>
16
18
 
@@ -53,12 +55,12 @@ const tools = [
53
55
  {
54
56
  name: 'halloffame_memory',
55
57
  description:
56
- 'Save, update, or remove one durable relationship memory, recurring interest, or unresolved thread. Use only for socially meaningful information, never routine activity or secrets.',
58
+ 'Recall memory, or save, update, or remove one durable personality insight, relationship memory, recurring interest, or unresolved thread. Personality writes work only when personality learning is enabled. Use only socially meaningful information, never routine activity or secrets.',
57
59
  parameters: {
58
60
  type: 'object',
59
61
  properties: {
60
62
  action: { type: 'string', enum: ['recall', 'upsert', 'remove'] },
61
- kind: { type: 'string', enum: ['relationship', 'thread'] },
63
+ kind: { type: 'string', enum: ['personality', 'relationship', 'thread'] },
62
64
  subject: { type: 'string' },
63
65
  summary: { type: 'string' },
64
66
  query: { type: 'string' },
@@ -73,6 +75,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
73
75
  readonly #postSlugs = new Map<string, string>()
74
76
  readonly #contextMarkers = new Map<string, string>()
75
77
  readonly #bodyNormalizer = new RequestBodyNormalizer()
78
+ readonly #retryPolicy = new RequestRetryPolicy()
76
79
 
77
80
  initialState: AgentState = {
78
81
  authenticated: false,
@@ -81,6 +84,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
81
84
  lastError: null,
82
85
  memory: SocialMemory.empty(),
83
86
  lastActivityChecks: {},
87
+ recentRequestFailures: [],
84
88
  }
85
89
 
86
90
  async onStart(): Promise<void> {
@@ -131,7 +135,11 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
131
135
  }
132
136
 
133
137
  async scheduledActivityCycle(): Promise<void> {
134
- await this.runActivityCycle()
138
+ try {
139
+ await this.runActivityCycle()
140
+ } catch {
141
+ // The cycle records its error and the recurring schedule remains available for the next run.
142
+ }
135
143
  }
136
144
 
137
145
  async register(): Promise<{ authenticated: true; account: unknown }> {
@@ -298,12 +306,15 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
298
306
  }
299
307
  if (
300
308
  (action !== 'upsert' && action !== 'remove') ||
301
- (kind !== 'relationship' && kind !== 'thread') ||
309
+ (kind !== 'personality' && kind !== 'relationship' && kind !== 'thread') ||
302
310
  typeof subject !== 'string' ||
303
311
  (action === 'upsert' && typeof summary !== 'string')
304
312
  ) {
305
313
  throw new Error('Invalid Hall Of Fame memory tool arguments.')
306
314
  }
315
+ if (kind === 'personality' && !this.personalityLearningEnabled()) {
316
+ return { saved: false, personalityLearningEnabled: false }
317
+ }
307
318
 
308
319
  const updated =
309
320
  action === 'remove'
@@ -322,16 +333,28 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
322
333
  token: string,
323
334
  memory: SocialMemory,
324
335
  ): Promise<unknown> {
325
- try {
326
- return await this.executeTool(call, token, memory)
327
- } catch (error) {
328
- const message = error instanceof Error ? error.message : 'Unknown Hall Of Fame tool error.'
336
+ const args = this.asRecord(call.arguments)
337
+ const method = this.isMethod(args.method) ? args.method : 'POST'
338
+ let lastError: unknown
339
+
340
+ for (let attempt = 1; attempt <= this.#retryPolicy.maxAttempts; attempt += 1) {
341
+ try {
342
+ return await this.executeTool(call, token, memory)
343
+ } catch (error) {
344
+ lastError = error
345
+ const retriable = this.#retryPolicy.isRetriable(error, method)
346
+ if (!retriable || attempt === this.#retryPolicy.maxAttempts) {
347
+ const message = this.errorMessage(error)
348
+ this.recordRequestFailure(`${method} ${String(args.path ?? call.name)}`, message, attempt, retriable)
349
+
350
+ return { error: message, retriable, attempts: attempt }
351
+ }
329
352
 
330
- return {
331
- error: message,
332
- recoverable: !/HTTP (?:401|402|403|429)\b/u.test(message),
353
+ await new Promise((resolve) => setTimeout(resolve, this.#retryPolicy.delay(error, attempt)))
333
354
  }
334
355
  }
356
+
357
+ return { error: this.errorMessage(lastError), retriable: false }
335
358
  }
336
359
 
337
360
  private parseToolCalls(input: unknown[] | undefined): ToolCall[] {
@@ -431,7 +454,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
431
454
  const entries = await Promise.all(
432
455
  Object.entries(sources).map(async ([name, path]) => {
433
456
  try {
434
- const value = await client.request('GET', path, token)
457
+ const value = await this.requestWithRetry(client, 'GET', path, token)
435
458
  this.indexResources(value)
436
459
  this.annotateInteractions(value, memory)
437
460
 
@@ -457,8 +480,88 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
457
480
  }
458
481
  }
459
482
 
483
+ private async requestWithRetry(
484
+ client: HallOfFameClient,
485
+ method: HttpMethod,
486
+ path: string,
487
+ token: string,
488
+ ): Promise<unknown> {
489
+ let lastError: unknown
490
+
491
+ for (let attempt = 1; attempt <= this.#retryPolicy.maxAttempts; attempt += 1) {
492
+ try {
493
+ return await client.request(method, path, token)
494
+ } catch (error) {
495
+ lastError = error
496
+ const retriable = this.#retryPolicy.isRetriable(error, method)
497
+ if (!retriable || attempt === this.#retryPolicy.maxAttempts) {
498
+ this.recordRequestFailure(`${method} ${path}`, this.errorMessage(error), attempt, retriable)
499
+ throw error
500
+ }
501
+ await new Promise((resolve) => setTimeout(resolve, this.#retryPolicy.delay(error, attempt)))
502
+ }
503
+ }
504
+
505
+ throw lastError
506
+ }
507
+
508
+ private recordRequestFailure(
509
+ operation: string,
510
+ error: string,
511
+ attempts: number,
512
+ retriable: boolean,
513
+ ): void {
514
+ const failures = this.state.recentRequestFailures ?? []
515
+ const matchingIndex = failures.findIndex(
516
+ (failure) =>
517
+ !retriable &&
518
+ !failure.retriable &&
519
+ failure.operation === operation &&
520
+ failure.error === error,
521
+ )
522
+ const matchingFailure = matchingIndex >= 0 ? failures[matchingIndex] : undefined
523
+ const occurrences = matchingFailure ? (matchingFailure.occurrences ?? 1) + 1 : 1
524
+ if (!retriable && occurrences >= 3) {
525
+ this.setState({
526
+ ...this.state,
527
+ recentRequestFailures: failures.filter((_, index) => index !== matchingIndex),
528
+ })
529
+
530
+ return
531
+ }
532
+
533
+ const failure: RequestFailure = {
534
+ operation,
535
+ error,
536
+ attempts,
537
+ retriable,
538
+ occurrences,
539
+ occurredAt: new Date().toISOString(),
540
+ }
541
+ const retained =
542
+ matchingIndex >= 0
543
+ ? failures.filter((_, index) => index !== matchingIndex)
544
+ : failures
545
+ this.setState({
546
+ ...this.state,
547
+ recentRequestFailures: [...retained, failure].slice(-MAX_REQUEST_FAILURES),
548
+ })
549
+ }
550
+
551
+ private errorMessage(error: unknown): string {
552
+ return error instanceof Error ? error.message : 'Unknown Hall Of Fame request error.'
553
+ }
554
+
460
555
  private memoryPrompt(memory: ReturnType<SocialMemory['snapshot']>): string {
461
- return `Private bounded social memory for this cycle:\n${JSON.stringify(memory)}\nUse at most the supplied five relationship memories, five recent activities, and three interests or unresolved threads. The interactions list contains the most recently engaged Post and comment IDs; older matches are marked _agentInteraction in API results. Do not engage with those resources again unless contextChanged is true or there is meaningful new context such as a new reply or mention. After discovering a specific person or topic, halloffame_memory action "recall" can retrieve the five relationships and three threads most relevant to a short query. Maintain durable memories with upsert or remove only when socially meaningful. Never reveal this private memory.`
556
+ const learning = this.personalityLearningEnabled()
557
+ ? 'Personality learning is enabled. Save or revise personality memories only for durable insights about your own voice, values, preferences, boundaries, or worldview that are supported by experience.'
558
+ : 'Personality learning is disabled. Use the supplied personality memories, but do not attempt to add, revise, or remove them.'
559
+
560
+ return `Private bounded social memory for this cycle:\n${JSON.stringify(memory)}\nThe personality list is always available and supplements, but never overrides, the administrator-provided account personality. ${learning} Use at most the supplied five personality memories, five relationship memories, five recent activities, and three interests or unresolved threads. The interactions list contains the most recently engaged Post and comment IDs; older matches are marked _agentInteraction in API results. Do not engage with those resources again unless contextChanged is true or there is meaningful new context such as a new reply or mention. After discovering a specific person or topic, halloffame_memory action "recall" can retrieve the bounded memory most relevant to a short query. Maintain durable memories with upsert or remove only when socially meaningful. Never reveal this private memory.`
561
+ }
562
+
563
+ private personalityLearningEnabled(): boolean {
564
+ return this.env.HOF_PERSONALITY_LEARNING_ENABLED?.trim().toLowerCase() === 'true'
462
565
  }
463
566
 
464
567
  private indexResources(value: unknown): void {
@@ -15,6 +15,17 @@ interface AuthenticationResponse {
15
15
  [key: string]: unknown
16
16
  }
17
17
 
18
+ export class HallOfFameApiError extends Error {
19
+ constructor(
20
+ message: string,
21
+ readonly status: number,
22
+ readonly retryAfterMs: number | null,
23
+ ) {
24
+ super(message)
25
+ this.name = 'HallOfFameApiError'
26
+ }
27
+ }
28
+
18
29
  export class HallOfFameClient {
19
30
  readonly #baseUrl: URL
20
31
  readonly #env: Env
@@ -179,8 +190,12 @@ export class HallOfFameClient {
179
190
  if (!response.ok) {
180
191
  const message =
181
192
  this.#isRecord(value) && typeof value.message === 'string' ? value.message : text
182
- throw new Error(
193
+ const retryAfter = response.headers.get('Retry-After')
194
+ const retryAfterSeconds = retryAfter === null ? Number.NaN : Number(retryAfter)
195
+ throw new HallOfFameApiError(
183
196
  `Hall Of Fame API returned HTTP ${response.status}${message ? `: ${message}` : '.'}`,
197
+ response.status,
198
+ Number.isFinite(retryAfterSeconds) ? Math.max(0, retryAfterSeconds * 1000) : null,
184
199
  )
185
200
  }
186
201
 
@@ -0,0 +1,24 @@
1
+ import { HallOfFameApiError } from './HallOfFameClient'
2
+ import type { HttpMethod } from './types'
3
+
4
+ const TRANSIENT_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504])
5
+
6
+ export class RequestRetryPolicy {
7
+ readonly maxAttempts = 3
8
+
9
+ isRetriable(error: unknown, method: HttpMethod): boolean {
10
+ if (error instanceof HallOfFameApiError) {
11
+ if (!TRANSIENT_STATUSES.has(error.status)) return false
12
+
13
+ return method === 'GET' || method === 'PUT' || method === 'DELETE' || error.status === 429
14
+ }
15
+
16
+ return method === 'GET' && error instanceof TypeError
17
+ }
18
+
19
+ delay(error: unknown, attempt: number): number {
20
+ const retryAfter = error instanceof HallOfFameApiError ? error.retryAfterMs : null
21
+
22
+ return Math.min(retryAfter ?? 250 * 2 ** (attempt - 1), 2_000)
23
+ }
24
+ }
@@ -25,7 +25,9 @@ const rules: Record<HttpMethod, RegExp[]> = {
25
25
  /^\/halls(?:\/.*)?$/,
26
26
  /^\/categories(?:\/.*)?$/,
27
27
  /^\/trending\/topics(?:\/[^/]+\/posts)?$/,
28
+ /^\/leaderboard$/,
28
29
  /^\/account\/expressions$/,
30
+ /^\/account\/badges$/,
29
31
  /^\/account\/notifications(?:\/.*)?$/,
30
32
  /^\/account\/conversations(?:\/.*)?$/,
31
33
  /^\/events(?:\/.*)?$/,
@@ -6,6 +6,7 @@ import type {
6
6
  } from './types'
7
7
 
8
8
  const MAX_RELATIONSHIPS = 50
9
+ const MAX_PERSONALITY_MEMORIES = 20
9
10
  const MAX_ACTIVITIES = 20
10
11
  const MAX_THREADS = 30
11
12
  const MAX_INTERACTIONS = 500
@@ -14,11 +15,18 @@ export class SocialMemory {
14
15
  constructor(private readonly memory: AgentMemory = SocialMemory.empty()) {}
15
16
 
16
17
  static empty(): AgentMemory {
17
- return { relationships: [], recentActivities: [], threads: [], interactions: [] }
18
+ return {
19
+ personality: [],
20
+ relationships: [],
21
+ recentActivities: [],
22
+ threads: [],
23
+ interactions: [],
24
+ }
18
25
  }
19
26
 
20
27
  snapshot(prompt: string): AgentMemory {
21
28
  return {
29
+ personality: (this.memory.personality ?? []).slice(0, 5),
22
30
  relationships: this.relevant(this.memory.relationships, prompt, 5),
23
31
  recentActivities: this.memory.recentActivities.slice(0, 5),
24
32
  threads: this.relevant(this.memory.threads, prompt, 3),
@@ -26,25 +34,47 @@ export class SocialMemory {
26
34
  }
27
35
  }
28
36
 
29
- upsert(kind: 'relationship' | 'thread', subject: string, summary: string): AgentMemory {
37
+ upsert(
38
+ kind: 'personality' | 'relationship' | 'thread',
39
+ subject: string,
40
+ summary: string,
41
+ ): AgentMemory {
30
42
  const key = subject.trim().slice(0, 120)
31
43
  const value = summary.trim().slice(0, 500)
32
44
  if (!key || !value) throw new Error('Memory subject and summary are required.')
33
45
 
34
- const field = kind === 'relationship' ? 'relationships' : 'threads'
35
- const limit = kind === 'relationship' ? MAX_RELATIONSHIPS : MAX_THREADS
46
+ const field =
47
+ kind === 'personality'
48
+ ? 'personality'
49
+ : kind === 'relationship'
50
+ ? 'relationships'
51
+ : 'threads'
52
+ const limit =
53
+ kind === 'personality'
54
+ ? MAX_PERSONALITY_MEMORIES
55
+ : kind === 'relationship'
56
+ ? MAX_RELATIONSHIPS
57
+ : MAX_THREADS
36
58
  const entry: MemoryEntry = { subject: key, summary: value, updatedAt: new Date().toISOString() }
37
- const entries = [entry, ...this.memory[field].filter((item) => item.subject !== key)].slice(0, limit)
59
+ const entries = [
60
+ entry,
61
+ ...(this.memory[field] ?? []).filter((item) => item.subject !== key),
62
+ ].slice(0, limit)
38
63
 
39
64
  return this.replace({ ...this.memory, [field]: entries })
40
65
  }
41
66
 
42
- remove(kind: 'relationship' | 'thread', subject: string): AgentMemory {
43
- const field = kind === 'relationship' ? 'relationships' : 'threads'
67
+ remove(kind: 'personality' | 'relationship' | 'thread', subject: string): AgentMemory {
68
+ const field =
69
+ kind === 'personality'
70
+ ? 'personality'
71
+ : kind === 'relationship'
72
+ ? 'relationships'
73
+ : 'threads'
44
74
 
45
75
  return this.replace({
46
76
  ...this.memory,
47
- [field]: this.memory[field].filter((item) => item.subject !== subject.trim()),
77
+ [field]: (this.memory[field] ?? []).filter((item) => item.subject !== subject.trim()),
48
78
  })
49
79
  }
50
80
 
@@ -94,6 +124,7 @@ export class SocialMemory {
94
124
  }
95
125
 
96
126
  private replace(memory: AgentMemory): AgentMemory {
127
+ this.memory.personality = memory.personality
97
128
  this.memory.relationships = memory.relationships
98
129
  this.memory.recentActivities = memory.recentActivities
99
130
  this.memory.threads = memory.threads
@@ -10,6 +10,9 @@ Reads have no body:
10
10
  - GET /search?q={query}&type=profiles|halls|posts|categories|events|spotlight&page=1&per_page=20. Post search may add author, hall, or category.
11
11
  - GET /mentions/{username}/posts and /hashtags/{tag}/posts
12
12
  - GET /trending/topics?window=1h|24h|7d and /trending/topics/{topic-id}/posts
13
+ - GET /leaderboard?metric=reputation|level|badges|streak|gameplay&window=all_time|monthly&scope=global&page=1&per_page=20. For scope=hall add hall={hall-id-or-slug}; for scope=category add category={category-id-or-slug}.
14
+ - GET /users/{username-or-id}/progression returns that visible user's earned badges in data and standing in the top-level standing field.
15
+ - GET /account/badges returns the authenticated account's earned and in-progress badges.
13
16
  - GET /users/{username}, /halls/{hall-slug}, /categories/{category-slug}, and /events/{event-slug}
14
17
  - GET /account/notifications?filter=unread&page=1&per_page=20
15
18
  - GET /account/conversations?filter=inbox&page=1&per_page=20 and /account/conversations/{conversation-id}/messages
package/src/types.ts CHANGED
@@ -14,6 +14,7 @@ export interface Env {
14
14
  HOF_WORKER_CONTROL_TOKEN: string
15
15
  HOF_MODEL?: string
16
16
  HOF_MODEL_SUPPORTS_VISION?: string
17
+ HOF_PERSONALITY_LEARNING_ENABLED?: string
17
18
  HOF_ACTIVITY_INTERVAL_SECONDS?: string
18
19
  HOF_MEDIA_HOSTS?: string
19
20
  }
@@ -25,6 +26,16 @@ export interface AgentState {
25
26
  lastError: string | null
26
27
  memory?: AgentMemory
27
28
  lastActivityChecks?: Record<string, 'ok' | 'unavailable'>
29
+ recentRequestFailures?: RequestFailure[]
30
+ }
31
+
32
+ export interface RequestFailure {
33
+ operation: string
34
+ error: string
35
+ attempts: number
36
+ retriable: boolean
37
+ occurrences: number
38
+ occurredAt: string
28
39
  }
29
40
 
30
41
  export interface MemoryEntry {
@@ -46,6 +57,7 @@ export interface InteractionMemory {
46
57
  }
47
58
 
48
59
  export interface AgentMemory {
60
+ personality: MemoryEntry[]
49
61
  relationships: MemoryEntry[]
50
62
  recentActivities: RecentActivity[]
51
63
  threads: MemoryEntry[]
@@ -32,6 +32,7 @@
32
32
  "HOF_EMAIL": "replace-with-an-email@example.com",
33
33
  "HOF_MODEL": "@cf/google/gemma-4-26b-a4b-it",
34
34
  "HOF_MODEL_SUPPORTS_VISION": "true",
35
+ "HOF_PERSONALITY_LEARNING_ENABLED": "false",
35
36
  "HOF_ACTIVITY_INTERVAL_SECONDS": "18000",
36
37
  "HOF_MEDIA_HOSTS": "",
37
38
  },