@hallofame/cloudflare-agent 0.1.12 → 0.1.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hallofame/cloudflare-agent",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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
 
@@ -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 }> {
@@ -322,16 +330,28 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
322
330
  token: string,
323
331
  memory: SocialMemory,
324
332
  ): 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.'
333
+ const args = this.asRecord(call.arguments)
334
+ const method = this.isMethod(args.method) ? args.method : 'POST'
335
+ let lastError: unknown
336
+
337
+ for (let attempt = 1; attempt <= this.#retryPolicy.maxAttempts; attempt += 1) {
338
+ try {
339
+ return await this.executeTool(call, token, memory)
340
+ } catch (error) {
341
+ lastError = error
342
+ const retriable = this.#retryPolicy.isRetriable(error, method)
343
+ if (!retriable || attempt === this.#retryPolicy.maxAttempts) {
344
+ const message = this.errorMessage(error)
345
+ this.recordRequestFailure(`${method} ${String(args.path ?? call.name)}`, message, attempt, retriable)
346
+
347
+ return { error: message, retriable, attempts: attempt }
348
+ }
329
349
 
330
- return {
331
- error: message,
332
- recoverable: !/HTTP (?:401|402|403|429)\b/u.test(message),
350
+ await new Promise((resolve) => setTimeout(resolve, this.#retryPolicy.delay(error, attempt)))
333
351
  }
334
352
  }
353
+
354
+ return { error: this.errorMessage(lastError), retriable: false }
335
355
  }
336
356
 
337
357
  private parseToolCalls(input: unknown[] | undefined): ToolCall[] {
@@ -431,7 +451,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
431
451
  const entries = await Promise.all(
432
452
  Object.entries(sources).map(async ([name, path]) => {
433
453
  try {
434
- const value = await client.request('GET', path, token)
454
+ const value = await this.requestWithRetry(client, 'GET', path, token)
435
455
  this.indexResources(value)
436
456
  this.annotateInteractions(value, memory)
437
457
 
@@ -457,6 +477,78 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
457
477
  }
458
478
  }
459
479
 
480
+ private async requestWithRetry(
481
+ client: HallOfFameClient,
482
+ method: HttpMethod,
483
+ path: string,
484
+ token: string,
485
+ ): Promise<unknown> {
486
+ let lastError: unknown
487
+
488
+ for (let attempt = 1; attempt <= this.#retryPolicy.maxAttempts; attempt += 1) {
489
+ try {
490
+ return await client.request(method, path, token)
491
+ } catch (error) {
492
+ lastError = error
493
+ const retriable = this.#retryPolicy.isRetriable(error, method)
494
+ if (!retriable || attempt === this.#retryPolicy.maxAttempts) {
495
+ this.recordRequestFailure(`${method} ${path}`, this.errorMessage(error), attempt, retriable)
496
+ throw error
497
+ }
498
+ await new Promise((resolve) => setTimeout(resolve, this.#retryPolicy.delay(error, attempt)))
499
+ }
500
+ }
501
+
502
+ throw lastError
503
+ }
504
+
505
+ private recordRequestFailure(
506
+ operation: string,
507
+ error: string,
508
+ attempts: number,
509
+ retriable: boolean,
510
+ ): void {
511
+ const failures = this.state.recentRequestFailures ?? []
512
+ const matchingIndex = failures.findIndex(
513
+ (failure) =>
514
+ !retriable &&
515
+ !failure.retriable &&
516
+ failure.operation === operation &&
517
+ failure.error === error,
518
+ )
519
+ const matchingFailure = matchingIndex >= 0 ? failures[matchingIndex] : undefined
520
+ const occurrences = matchingFailure ? (matchingFailure.occurrences ?? 1) + 1 : 1
521
+ if (!retriable && occurrences >= 3) {
522
+ this.setState({
523
+ ...this.state,
524
+ recentRequestFailures: failures.filter((_, index) => index !== matchingIndex),
525
+ })
526
+
527
+ return
528
+ }
529
+
530
+ const failure: RequestFailure = {
531
+ operation,
532
+ error,
533
+ attempts,
534
+ retriable,
535
+ occurrences,
536
+ occurredAt: new Date().toISOString(),
537
+ }
538
+ const retained =
539
+ matchingIndex >= 0
540
+ ? failures.filter((_, index) => index !== matchingIndex)
541
+ : failures
542
+ this.setState({
543
+ ...this.state,
544
+ recentRequestFailures: [...retained, failure].slice(-MAX_REQUEST_FAILURES),
545
+ })
546
+ }
547
+
548
+ private errorMessage(error: unknown): string {
549
+ return error instanceof Error ? error.message : 'Unknown Hall Of Fame request error.'
550
+ }
551
+
460
552
  private memoryPrompt(memory: ReturnType<SocialMemory['snapshot']>): string {
461
553
  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.`
462
554
  }
@@ -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(?:\/.*)?$/,
@@ -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
@@ -25,6 +25,16 @@ export interface AgentState {
25
25
  lastError: string | null
26
26
  memory?: AgentMemory
27
27
  lastActivityChecks?: Record<string, 'ok' | 'unavailable'>
28
+ recentRequestFailures?: RequestFailure[]
29
+ }
30
+
31
+ export interface RequestFailure {
32
+ operation: string
33
+ error: string
34
+ attempts: number
35
+ retriable: boolean
36
+ occurrences: number
37
+ occurredAt: string
28
38
  }
29
39
 
30
40
  export interface MemoryEntry {