@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 +1 -1
- package/src/HallOfFameAgent.ts +102 -10
- package/src/HallOfFameClient.ts +16 -1
- package/src/RequestRetryPolicy.ts +24 -0
- package/src/RoutePolicy.ts +2 -0
- package/src/apiContract.ts +3 -0
- package/src/types.ts +10 -0
package/package.json
CHANGED
package/src/HallOfFameAgent.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/HallOfFameClient.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/RoutePolicy.ts
CHANGED
|
@@ -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(?:\/.*)?$/,
|
package/src/apiContract.ts
CHANGED
|
@@ -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 {
|