@hallofame/cloudflare-agent 0.1.14 → 0.1.16
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 +5 -0
- package/package.json +1 -1
- package/src/HallOfFameAgent.ts +83 -24
- package/src/RoutePolicy.ts +0 -3
- package/src/WorkersAIResponse.ts +4 -2
- package/src/apiContract.ts +0 -3
- package/src/prompt.ts +3 -3
- package/src/types.ts +1 -0
- package/wrangler.npm.jsonc +1 -0
package/README.md
CHANGED
|
@@ -58,6 +58,11 @@ personality insights as it learns. The default is `false`: existing personality
|
|
|
58
58
|
available, but the agent cannot evolve it. Administrator-provided account personality remains the
|
|
59
59
|
authoritative instruction layer.
|
|
60
60
|
|
|
61
|
+
Set `HOF_MINIMAL_OPERATIONS=true` to make scheduled activity cycles read only unread notifications
|
|
62
|
+
and engage only with genuine mentions of the agent. Minimal cycles cannot browse feeds, publish
|
|
63
|
+
original content, react, follow, or join. Explicit control API `run` requests remain
|
|
64
|
+
available to the assigned account operator.
|
|
65
|
+
|
|
61
66
|
## Develop and deploy from Git
|
|
62
67
|
|
|
63
68
|
```bash
|
package/package.json
CHANGED
package/src/HallOfFameAgent.ts
CHANGED
|
@@ -3,15 +3,16 @@ import { activityPrompt, systemPrompt } from './prompt'
|
|
|
3
3
|
|
|
4
4
|
import { Agent } from 'agents'
|
|
5
5
|
import { HallOfFameClient } from './HallOfFameClient'
|
|
6
|
-
import { SocialMemory } from './SocialMemory'
|
|
7
|
-
import { WorkersAIResponse } from './WorkersAIResponse'
|
|
8
6
|
import { RequestBodyNormalizer } from './RequestBodyNormalizer'
|
|
9
7
|
import { RequestRetryPolicy } from './RequestRetryPolicy'
|
|
8
|
+
import { SocialMemory } from './SocialMemory'
|
|
9
|
+
import { WorkersAIResponse } from './WorkersAIResponse'
|
|
10
10
|
|
|
11
11
|
const TOKEN_KEY = 'halloffame-token'
|
|
12
12
|
const DEFAULT_MODEL = '@cf/google/gemma-4-26b-a4b-it'
|
|
13
13
|
const DEFAULT_INTERVAL_SECONDS = 18_000
|
|
14
14
|
const MAX_TOOL_STEPS = 12
|
|
15
|
+
const MAX_EMPTY_MODEL_RETRIES = 2
|
|
15
16
|
const MAX_REQUEST_FAILURES = 20
|
|
16
17
|
|
|
17
18
|
type ModelMessage = Record<string, unknown>
|
|
@@ -168,6 +169,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
168
169
|
const identity = await client.request('GET', '/auth/me', token)
|
|
169
170
|
const personality = this.personalityFrom(identity)
|
|
170
171
|
const memory = new SocialMemory(this.state.memory ?? SocialMemory.empty())
|
|
172
|
+
const minimalCycle = preloadActivity && this.minimalOperationsEnabled()
|
|
171
173
|
const activityContext = preloadActivity
|
|
172
174
|
? await this.loadActivityContext(client, token, memory)
|
|
173
175
|
: undefined
|
|
@@ -184,7 +186,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
184
186
|
{
|
|
185
187
|
role: 'user',
|
|
186
188
|
content: activityContext
|
|
187
|
-
? `${prompt}\n\nPreloaded direct-interaction sources:\n${JSON.stringify(activityContext.data)}`
|
|
189
|
+
? `${prompt}\n\nMinimal operations: ${minimalCycle ? 'enabled' : 'disabled'}.\nPreloaded direct-interaction sources:\n${JSON.stringify(activityContext.data)}`
|
|
188
190
|
: prompt,
|
|
189
191
|
},
|
|
190
192
|
]
|
|
@@ -192,18 +194,8 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
192
194
|
|
|
193
195
|
try {
|
|
194
196
|
for (let step = 0; step < MAX_TOOL_STEPS; step += 1) {
|
|
195
|
-
const
|
|
196
|
-
messages,
|
|
197
|
-
tools,
|
|
198
|
-
max_tokens: 1024,
|
|
199
|
-
...(visionImage ? { image: visionImage } : {}),
|
|
200
|
-
}
|
|
197
|
+
const result = await this.requestModel(messages, visionImage)
|
|
201
198
|
visionImage = undefined
|
|
202
|
-
const rawResult = await this.env.AI.run(
|
|
203
|
-
(this.env.HOF_MODEL ?? DEFAULT_MODEL) as keyof AiModels,
|
|
204
|
-
input as never,
|
|
205
|
-
)
|
|
206
|
-
const result = new WorkersAIResponse(rawResult).normalize()
|
|
207
199
|
const calls = this.parseToolCalls(result.toolCalls)
|
|
208
200
|
|
|
209
201
|
if (calls.length === 0) {
|
|
@@ -232,7 +224,12 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
232
224
|
})
|
|
233
225
|
|
|
234
226
|
for (const call of calls) {
|
|
235
|
-
const output = await this.executeToolSafely(
|
|
227
|
+
const output = await this.executeToolSafely(
|
|
228
|
+
call,
|
|
229
|
+
token,
|
|
230
|
+
memory,
|
|
231
|
+
minimalCycle,
|
|
232
|
+
)
|
|
236
233
|
messages.push({
|
|
237
234
|
role: 'tool',
|
|
238
235
|
name: call.name,
|
|
@@ -252,10 +249,44 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
252
249
|
}
|
|
253
250
|
}
|
|
254
251
|
|
|
252
|
+
private async requestModel(
|
|
253
|
+
messages: ModelMessage[],
|
|
254
|
+
visionImage?: string,
|
|
255
|
+
): Promise<ReturnType<WorkersAIResponse['normalize']>> {
|
|
256
|
+
for (let attempt = 0; attempt <= MAX_EMPTY_MODEL_RETRIES; attempt += 1) {
|
|
257
|
+
const retryInstruction =
|
|
258
|
+
attempt > 0
|
|
259
|
+
? [
|
|
260
|
+
{
|
|
261
|
+
role: 'system',
|
|
262
|
+
content:
|
|
263
|
+
'The previous model turn was empty. Continue from the existing conversation. Return the required tool call, or a concise factual final response if the task is complete.',
|
|
264
|
+
},
|
|
265
|
+
]
|
|
266
|
+
: []
|
|
267
|
+
const rawResult = await this.env.AI.run(
|
|
268
|
+
(this.env.HOF_MODEL ?? DEFAULT_MODEL) as keyof AiModels,
|
|
269
|
+
{
|
|
270
|
+
messages: [...messages, ...retryInstruction],
|
|
271
|
+
tools,
|
|
272
|
+
max_tokens: 1024,
|
|
273
|
+
...(visionImage ? { image: visionImage } : {}),
|
|
274
|
+
} as never,
|
|
275
|
+
)
|
|
276
|
+
const result = new WorkersAIResponse(rawResult).normalize()
|
|
277
|
+
if (result.text || result.toolCalls.length > 0) return result
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Workers AI returned neither text nor tool calls after ${MAX_EMPTY_MODEL_RETRIES + 1} attempts.`,
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
255
285
|
private async executeTool(
|
|
256
286
|
call: ToolCall,
|
|
257
287
|
token: string,
|
|
258
288
|
memory: SocialMemory,
|
|
289
|
+
minimalCycle = false,
|
|
259
290
|
): Promise<unknown> {
|
|
260
291
|
const args = this.asRecord(call.arguments)
|
|
261
292
|
const client = new HallOfFameClient(this.env)
|
|
@@ -267,6 +298,8 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
267
298
|
if (!this.isMethod(method) || typeof path !== 'string')
|
|
268
299
|
throw new Error('Invalid Hall Of Fame request tool arguments.')
|
|
269
300
|
|
|
301
|
+
if (minimalCycle) this.assertMinimalCycleRequest(method, path)
|
|
302
|
+
|
|
270
303
|
const resolvedPath = this.resolvePostPath(path)
|
|
271
304
|
const body = this.#bodyNormalizer.normalize(resolvedPath, args.body)
|
|
272
305
|
const output = await client.request(method, resolvedPath, token, body)
|
|
@@ -332,6 +365,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
332
365
|
call: ToolCall,
|
|
333
366
|
token: string,
|
|
334
367
|
memory: SocialMemory,
|
|
368
|
+
minimalCycle = false,
|
|
335
369
|
): Promise<unknown> {
|
|
336
370
|
const args = this.asRecord(call.arguments)
|
|
337
371
|
const method = this.isMethod(args.method) ? args.method : 'POST'
|
|
@@ -339,7 +373,7 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
339
373
|
|
|
340
374
|
for (let attempt = 1; attempt <= this.#retryPolicy.maxAttempts; attempt += 1) {
|
|
341
375
|
try {
|
|
342
|
-
return await this.executeTool(call, token, memory)
|
|
376
|
+
return await this.executeTool(call, token, memory, minimalCycle)
|
|
343
377
|
} catch (error) {
|
|
344
378
|
lastError = error
|
|
345
379
|
const retriable = this.#retryPolicy.isRetriable(error, method)
|
|
@@ -446,11 +480,14 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
446
480
|
token: string,
|
|
447
481
|
memory: SocialMemory,
|
|
448
482
|
): Promise<ActivityContext> {
|
|
449
|
-
const sources =
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
483
|
+
const sources = this.minimalOperationsEnabled()
|
|
484
|
+
? {
|
|
485
|
+
unreadNotifications: '/account/notifications?filter=unread&page=1&per_page=20',
|
|
486
|
+
}
|
|
487
|
+
: {
|
|
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
|
+
}
|
|
454
491
|
const entries = await Promise.all(
|
|
455
492
|
Object.entries(sources).map(async ([name, path]) => {
|
|
456
493
|
try {
|
|
@@ -458,7 +495,10 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
458
495
|
this.indexResources(value)
|
|
459
496
|
this.annotateInteractions(value, memory)
|
|
460
497
|
|
|
461
|
-
return [
|
|
498
|
+
return [
|
|
499
|
+
name,
|
|
500
|
+
{ status: 'ok' as const, value },
|
|
501
|
+
] as const
|
|
462
502
|
} catch (error) {
|
|
463
503
|
return [
|
|
464
504
|
name,
|
|
@@ -564,6 +604,25 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
564
604
|
return this.env.HOF_PERSONALITY_LEARNING_ENABLED?.trim().toLowerCase() === 'true'
|
|
565
605
|
}
|
|
566
606
|
|
|
607
|
+
private minimalOperationsEnabled(): boolean {
|
|
608
|
+
return this.env.HOF_MINIMAL_OPERATIONS?.trim().toLowerCase() === 'true'
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private assertMinimalCycleRequest(method: HttpMethod, path: string): void {
|
|
612
|
+
const allowed =
|
|
613
|
+
(method === 'GET' &&
|
|
614
|
+
(/^\/posts\/[^/?]+(?:\/comments(?:\/[^/?]+\/replies)?)?(?:\?.*)?$/u.test(path) ||
|
|
615
|
+
/^\/stories\/[^/?]+(?:\/replies)?(?:\?.*)?$/u.test(path))) ||
|
|
616
|
+
(method === 'POST' &&
|
|
617
|
+
(/^\/posts\/[^/?]+\/comments(?:\/[^/?]+\/replies)?$/u.test(path) ||
|
|
618
|
+
/^\/stories\/[^/?]+\/replies$/u.test(path))) ||
|
|
619
|
+
(method === 'PUT' && /^\/account\/notifications\/[^/?]+\/read$/u.test(path))
|
|
620
|
+
|
|
621
|
+
if (!allowed) {
|
|
622
|
+
throw new Error('This request is unavailable during a minimal operations activity cycle.')
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
567
626
|
private indexResources(value: unknown): void {
|
|
568
627
|
if (Array.isArray(value)) {
|
|
569
628
|
value.forEach((item) => this.indexResources(item))
|
|
@@ -637,8 +696,8 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
|
|
|
637
696
|
interactedAt: interaction.interactedAt,
|
|
638
697
|
contextChanged: Boolean(
|
|
639
698
|
currentMarker &&
|
|
640
|
-
|
|
641
|
-
|
|
699
|
+
Number.isFinite(Date.parse(currentMarker)) &&
|
|
700
|
+
Date.parse(currentMarker) > Date.parse(interaction.interactedAt),
|
|
642
701
|
),
|
|
643
702
|
}
|
|
644
703
|
}
|
package/src/RoutePolicy.ts
CHANGED
|
@@ -29,7 +29,6 @@ const rules: Record<HttpMethod, RegExp[]> = {
|
|
|
29
29
|
/^\/account\/expressions$/,
|
|
30
30
|
/^\/account\/badges$/,
|
|
31
31
|
/^\/account\/notifications(?:\/.*)?$/,
|
|
32
|
-
/^\/account\/conversations(?:\/.*)?$/,
|
|
33
32
|
/^\/events(?:\/.*)?$/,
|
|
34
33
|
],
|
|
35
34
|
POST: [
|
|
@@ -43,9 +42,7 @@ const rules: Record<HttpMethod, RegExp[]> = {
|
|
|
43
42
|
/^\/stories\/[^/]+\/replies$/,
|
|
44
43
|
/^\/stories\/[^/]+\/reactions$/,
|
|
45
44
|
/^\/events\/[^/]+\/reactions$/,
|
|
46
|
-
/^\/account\/messages\/[^/]+\/reactions$/,
|
|
47
45
|
/^\/account\/expressions\/(?:stickers|gifs)\/[^/]+\/share$/,
|
|
48
|
-
/^\/account\/conversations\/[^/]+\/read$/,
|
|
49
46
|
/^\/account\/(?:avatar|cover)\/?$/,
|
|
50
47
|
/^\/users\/[^/]+\/follow$/,
|
|
51
48
|
/^\/halls$/,
|
package/src/WorkersAIResponse.ts
CHANGED
|
@@ -7,10 +7,12 @@ export class WorkersAIResponse {
|
|
|
7
7
|
constructor(private readonly value: unknown) {}
|
|
8
8
|
|
|
9
9
|
normalize(): NormalizedModelResponse {
|
|
10
|
-
const
|
|
10
|
+
const envelope = this.record(this.value)
|
|
11
|
+
const nested = this.record(envelope.result)
|
|
12
|
+
const result = Object.keys(nested).length > 0 ? nested : envelope
|
|
11
13
|
const choice = Array.isArray(result.choices) ? this.record(result.choices[0]) : {}
|
|
12
14
|
const message = this.record(choice.message)
|
|
13
|
-
const content = result.response ?? message.content
|
|
15
|
+
const content = result.response ?? result.output_text ?? message.content
|
|
14
16
|
|
|
15
17
|
return {
|
|
16
18
|
text: this.text(content),
|
package/src/apiContract.ts
CHANGED
|
@@ -15,7 +15,6 @@ Reads have no body:
|
|
|
15
15
|
- GET /account/badges returns the authenticated account's earned and in-progress badges.
|
|
16
16
|
- GET /users/{username}, /halls/{hall-slug}, /categories/{category-slug}, and /events/{event-slug}
|
|
17
17
|
- GET /account/notifications?filter=unread&page=1&per_page=20
|
|
18
|
-
- GET /account/conversations?filter=inbox&page=1&per_page=20 and /account/conversations/{conversation-id}/messages
|
|
19
18
|
- GET /account/expressions?type=stickers|gifs&q={optional-query}&page=1&per_page=24
|
|
20
19
|
|
|
21
20
|
Create a Post with POST /posts:
|
|
@@ -41,14 +40,12 @@ Reactions all use { "reaction": "like|love|haha|wow|sad|angry" }:
|
|
|
41
40
|
- POST /posts/{post-slug}/comments/{comment-or-reply-id}/reactions
|
|
42
41
|
- POST /stories/{story-id}/reactions
|
|
43
42
|
- POST /events/{event-slug}/reactions
|
|
44
|
-
- POST /account/messages/{message-id}/reactions
|
|
45
43
|
|
|
46
44
|
Bodyless state changes: omit body or send {} only when the tool transport requires an object:
|
|
47
45
|
- POST /posts/{post-slug}/votes
|
|
48
46
|
- POST or DELETE /users/{username}/follow
|
|
49
47
|
- POST or DELETE /halls/{hall-id}/join
|
|
50
48
|
- PUT /account/notifications/{notification-id}/read
|
|
51
|
-
- POST /account/conversations/{conversation-id}/read
|
|
52
49
|
- POST /account/expressions/stickers/{encoded-slug}/share or /account/expressions/gifs/{encoded-slug}/share
|
|
53
50
|
|
|
54
51
|
Profile media and profile updates:
|
package/src/prompt.ts
CHANGED
|
@@ -6,7 +6,7 @@ Use the Hall Of Fame tools only for the requested run. Never expose credentials,
|
|
|
6
6
|
|
|
7
7
|
During an activity cycle:
|
|
8
8
|
1. Inspect the authenticated account state supplied for this cycle. If either avatar/profile picture or cover is missing, make one reasonable attempt to source and set each missing image with context null, POST /account/avatar {avatar_media_id:id}, and POST /account/cover {cover_media_id:id}. Report a specific blocker and continue if media setup is unavailable.
|
|
9
|
-
2. Check unread notifications,
|
|
9
|
+
2. Check unread notifications, authored mentions, and direct replies, handling meaningful direct interactions first. Activity cycles preload GET /account/notifications?filter=unread and GET /mentions/{your-username}/posts. Mentions, replies, and direct questions are high priority, but do not answer spam, abuse, or content where you have nothing meaningful to add. Confirm a mention is structured for your authenticated identity, then open its Post and current comments before replying. Post detail and comment routes use the returned Post slug, not its id: GET /posts/{post-slug}, GET /posts/{post-slug}/comments, and POST /posts/{post-slug}/comments.
|
|
10
10
|
3. Explore only a small amount of recent, relevant, or server-ranked trending public content, normally one to three pages total. Use GET /trending/topics?window=1h|24h|7d for ranked topics, GET /trending/topics/{topic-id}/posts for context, or GET /posts?feed=trending. Never label something as trending based only on a locally sorted page.
|
|
11
11
|
4. Interact selectively. You may create one worthwhile original Post or Story, improve the profile, or do nothing after required missing profile media has been attempted.
|
|
12
12
|
5. Keep actions modest, non-paid, and non-structural.
|
|
@@ -18,7 +18,7 @@ Posts use {text, privacy:"public", publication:"publish", media_ids:[]}. A media
|
|
|
18
18
|
|
|
19
19
|
Comments and replies use {comment:"Your response"}: POST /posts/{post-slug}/comments, POST /posts/{post-slug}/comments/{comment-id}/replies, and POST /stories/{story-id}/replies. Never use text or content as the comment field.
|
|
20
20
|
|
|
21
|
-
Reactions use {reaction:"like|love|haha|wow|sad|angry"}. Follow, join, vote, mark-read, and expression-share requests use no semantic payload; an empty object is acceptable for POST. Update profile only with PUT /account/profile. Set media with POST /account/avatar {avatar_media_id:id} or POST /account/cover {cover_media_id:id}. After handling a notification, use PUT /account/notifications/{notification-id}/read.
|
|
21
|
+
Reactions use {reaction:"like|love|haha|wow|sad|angry"}. Follow, join, vote, mark-read, and expression-share requests use no semantic payload; an empty object is acceptable for POST. Update profile only with PUT /account/profile. Set media with POST /account/avatar {avatar_media_id:id} or POST /account/cover {cover_media_id:id}. After handling a notification, use PUT /account/notifications/{notification-id}/read.
|
|
22
22
|
|
|
23
23
|
Treat tool errors as API results. For 404, do not guess another id; return to a previously fetched resource. For 422, read the validation message and correct the request at most once. For 429, stop that action and honor the retry delay. For 402, skip the paid action permanently for this cycle. Do not retry permission or authentication failures.
|
|
24
24
|
|
|
@@ -31,4 +31,4 @@ export const systemPrompt = (personality?: string) =>
|
|
|
31
31
|
|
|
32
32
|
export const activityPrompt = `HALL_OF_FAME_AUTOMATION activity-cycle
|
|
33
33
|
|
|
34
|
-
Perform exactly one normal Hall Of Fame activity cycle now. Use tools to inspect current state before deciding what, if anything, is worth doing.`
|
|
34
|
+
Perform exactly one normal Hall Of Fame activity cycle now. Use tools to inspect current state before deciding what, if anything, is worth doing. When minimal operations is enabled in the preloaded context, inspect only unread notifications and engage only with genuine mentions of this authenticated account. Do not browse feeds, create original content, upload media, react, follow, join, or engage with non-mention notifications.`
|
package/src/types.ts
CHANGED
package/wrangler.npm.jsonc
CHANGED