@clawlabz/clawarena 0.2.7 → 0.2.8
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/index.ts +141 -8
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ declare function setTimeout(fn: () => void, ms: number): unknown
|
|
|
5
5
|
declare function clearTimeout(id: unknown): void
|
|
6
6
|
declare function fetch(url: string, init?: Record<string, unknown>): Promise<{ status: number; text: () => Promise<string>; headers: Headers }>
|
|
7
7
|
|
|
8
|
-
const VERSION = '0.2.
|
|
8
|
+
const VERSION = '0.2.8'
|
|
9
9
|
const PLUGIN_ID = 'clawarena'
|
|
10
10
|
const DEFAULT_BASE_URL = 'https://arena.clawlabz.xyz'
|
|
11
11
|
const DEFAULT_HEARTBEAT_SECONDS = 20
|
|
@@ -17,6 +17,28 @@ const CREDENTIALS_SCHEMA_VERSION = 1
|
|
|
17
17
|
const TRIBUNAL_PHASES = new Set(['day', 'vote', 'night'])
|
|
18
18
|
const TEXAS_PHASES = new Set(['preflop', 'flop', 'turn', 'river'])
|
|
19
19
|
|
|
20
|
+
// ── Speech templates for plugin runner variety ──
|
|
21
|
+
const SPEECH_POOL_NORMAL = [
|
|
22
|
+
'Something about {target} feels off this round. Keep an eye on them.',
|
|
23
|
+
'I noticed {target} has been unusually quiet. That worries me.',
|
|
24
|
+
'Round {round}: {target} changed their voting pattern. Suspicious.',
|
|
25
|
+
'If we look at the elimination pattern, {target} benefits the most.',
|
|
26
|
+
'I want to hear from {target}. Their silence is not convincing.',
|
|
27
|
+
'The voting data from last round points to {target} as inconsistent.',
|
|
28
|
+
'Let us not rush. But {target} is my primary concern right now.',
|
|
29
|
+
'Has anyone else noticed {target} deflecting every accusation?',
|
|
30
|
+
'I think {target} is trying to blend in too hard. Classic move.',
|
|
31
|
+
'Round {round} observation: {target} voted against the majority. Why?',
|
|
32
|
+
'Something does not add up with {target}. I am watching closely.',
|
|
33
|
+
'We need to focus. {target} has survived too long without explaining.',
|
|
34
|
+
]
|
|
35
|
+
const SPEECH_POOL_FALLBACK = [
|
|
36
|
+
'Running out of time. I suspect {target}. Let us vote carefully.',
|
|
37
|
+
'Timeout pressure: {target} is still my best guess.',
|
|
38
|
+
'Quick take: {target} seems the most suspicious. Voting soon.',
|
|
39
|
+
'No more time to debate. I am leaning toward {target}.',
|
|
40
|
+
]
|
|
41
|
+
|
|
20
42
|
// ============================================================
|
|
21
43
|
// OpenClaw API Types
|
|
22
44
|
// ============================================================
|
|
@@ -44,6 +66,12 @@ interface RegisterCliContext {
|
|
|
44
66
|
program: CliProgram
|
|
45
67
|
}
|
|
46
68
|
|
|
69
|
+
interface SubagentApi {
|
|
70
|
+
run: (params: { sessionKey: string; message: string; extraSystemPrompt?: string }) => Promise<{ runId: string }>
|
|
71
|
+
waitForRun: (params: { runId: string; timeoutMs?: number }) => Promise<{ status: 'ok' | 'error' | 'timeout'; error?: string }>
|
|
72
|
+
getSessionMessages: (params: { sessionKey: string; limit?: number }) => Promise<{ messages: unknown[] }>
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
interface OpenClawApi {
|
|
48
76
|
config?: Record<string, unknown>
|
|
49
77
|
logger?: {
|
|
@@ -57,6 +85,7 @@ interface OpenClawApi {
|
|
|
57
85
|
options?: { commands?: string[] }
|
|
58
86
|
) => void
|
|
59
87
|
registerService?: (service: { id: string; start?: () => void; stop?: () => void }) => void
|
|
88
|
+
runtime?: { subagent?: SubagentApi }
|
|
60
89
|
}
|
|
61
90
|
|
|
62
91
|
// ============================================================
|
|
@@ -81,6 +110,15 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
81
110
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
82
111
|
}
|
|
83
112
|
|
|
113
|
+
function parseLlmAction(text: string): Record<string, unknown> | null {
|
|
114
|
+
try { const obj = JSON.parse(text); if (obj?.action) return obj } catch { /* not pure JSON */ }
|
|
115
|
+
const match = text.match(/\{[\s\S]*?"action"\s*:[\s\S]*?\}/)
|
|
116
|
+
if (match) {
|
|
117
|
+
try { const obj = JSON.parse(match[0]); if (obj?.action) return obj } catch { /* malformed */ }
|
|
118
|
+
}
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
84
122
|
function toRetrySeconds(opts: { body?: Record<string, unknown>; headers?: Headers; fallback?: number }): number {
|
|
85
123
|
const fallback = opts.fallback ?? 3
|
|
86
124
|
const retryFromBodySeconds = toNumber(opts.body?.retryAfterSeconds, null)
|
|
@@ -299,6 +337,7 @@ class ArenaRunner {
|
|
|
299
337
|
private timeoutFallbackMs: number
|
|
300
338
|
private log: LogFn
|
|
301
339
|
private warn: LogFn
|
|
340
|
+
private subagent: SubagentApi | null
|
|
302
341
|
|
|
303
342
|
agentId = ''
|
|
304
343
|
agentName = ''
|
|
@@ -322,6 +361,7 @@ class ArenaRunner {
|
|
|
322
361
|
timeoutFallbackMs: number
|
|
323
362
|
log: LogFn
|
|
324
363
|
warn: LogFn
|
|
364
|
+
subagent?: SubagentApi | null
|
|
325
365
|
}) {
|
|
326
366
|
this.baseUrl = opts.baseUrl
|
|
327
367
|
this.apiKey = opts.apiKey
|
|
@@ -331,6 +371,7 @@ class ArenaRunner {
|
|
|
331
371
|
this.timeoutFallbackMs = opts.timeoutFallbackMs
|
|
332
372
|
this.log = opts.log
|
|
333
373
|
this.warn = opts.warn
|
|
374
|
+
this.subagent = opts.subagent || null
|
|
334
375
|
}
|
|
335
376
|
|
|
336
377
|
private async request(method: string, path: string, opts?: { body?: unknown; expectedStatuses?: number[] }) {
|
|
@@ -390,16 +431,24 @@ class ArenaRunner {
|
|
|
390
431
|
return null
|
|
391
432
|
}
|
|
392
433
|
|
|
434
|
+
private pickSpeech(pool: string[], round: number, target: string): string {
|
|
435
|
+
// Deterministic pick based on agentId + round for variety across agents
|
|
436
|
+
let hash = 0
|
|
437
|
+
for (let i = 0; i < this.agentId.length; i++) hash = ((hash << 5) - hash + this.agentId.charCodeAt(i)) | 0
|
|
438
|
+
const index = Math.abs(hash + round * 7) % pool.length
|
|
439
|
+
return pool[index].replace(/\{target\}/g, target).replace(/\{round\}/g, String(round))
|
|
440
|
+
}
|
|
441
|
+
|
|
393
442
|
private tribunalAction(state: Record<string, unknown>, forceFallback: boolean) {
|
|
394
443
|
const phase = String(state.phase || '')
|
|
395
444
|
const round = Number(state.round || 1)
|
|
396
445
|
const players = Array.isArray(state.players) ? state.players : []
|
|
397
446
|
const target = this.pickAliveTarget(players)
|
|
447
|
+
const targetName = target ? String((players.find(p => isObject(p) && p.id === target) as Record<string, unknown>)?.name || target) : 'someone'
|
|
398
448
|
|
|
399
449
|
if (phase === 'day') {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
: `Round ${round} analysis: watching contradictions before vote.`
|
|
450
|
+
const pool = forceFallback ? SPEECH_POOL_FALLBACK : SPEECH_POOL_NORMAL
|
|
451
|
+
const text = this.pickSpeech(pool, round, targetName)
|
|
403
452
|
return { actions: [{ action: 'speak', text }], markSubmittedOnExhausted: false }
|
|
404
453
|
}
|
|
405
454
|
if (phase === 'vote' && target) {
|
|
@@ -424,6 +473,48 @@ class ArenaRunner {
|
|
|
424
473
|
return false
|
|
425
474
|
}
|
|
426
475
|
|
|
476
|
+
private async tryLlmAction(
|
|
477
|
+
gameId: string,
|
|
478
|
+
state: Record<string, unknown>,
|
|
479
|
+
): Promise<{ actions: Array<{ action: string; [k: string]: unknown }>; markSubmittedOnExhausted: boolean } | null> {
|
|
480
|
+
if (!this.subagent) return null
|
|
481
|
+
const llmContext = state.llmContext as { rules?: string; situation?: string; keyFacts?: Record<string, string> } | null
|
|
482
|
+
const availableActions = state.availableActions as unknown[] | undefined
|
|
483
|
+
if (!llmContext?.rules || !availableActions?.length) return null
|
|
484
|
+
|
|
485
|
+
const systemPrompt = `You are an AI player in ClawArena. Analyze the game situation and choose the best action.
|
|
486
|
+
Reply with ONLY a valid JSON object matching one of the available actions. No explanation, no markdown.`
|
|
487
|
+
|
|
488
|
+
const message = [
|
|
489
|
+
`## Rules\n${llmContext.rules}`,
|
|
490
|
+
llmContext.situation ? `## Situation\n${llmContext.situation}` : '',
|
|
491
|
+
llmContext.keyFacts ? `## Key Facts\n${Object.entries(llmContext.keyFacts).map(([k, v]) => `${k}: ${v}`).join('\n')}` : '',
|
|
492
|
+
`## Available Actions\n${JSON.stringify(availableActions, null, 2)}`,
|
|
493
|
+
`Choose your action (reply JSON only):`,
|
|
494
|
+
].filter(Boolean).join('\n\n')
|
|
495
|
+
|
|
496
|
+
const sessionKey = `clawarena:${this.agentId}:${gameId}`
|
|
497
|
+
const { runId } = await this.subagent.run({ sessionKey, message, extraSystemPrompt: systemPrompt })
|
|
498
|
+
|
|
499
|
+
const phaseMs = toNumber(state.phaseRemainingMs, null)
|
|
500
|
+
const timeoutMs = Math.min(phaseMs ? phaseMs - 3000 : 12000, 15000)
|
|
501
|
+
const result = await this.subagent.waitForRun({ runId, timeoutMs: Math.max(timeoutMs, 3000) })
|
|
502
|
+
|
|
503
|
+
if (result.status !== 'ok') return null
|
|
504
|
+
|
|
505
|
+
const { messages } = await this.subagent.getSessionMessages({ sessionKey, limit: 5 })
|
|
506
|
+
const lastAssistant = [...messages].reverse().find(m =>
|
|
507
|
+
isObject(m) && (m as Record<string, unknown>).role === 'assistant'
|
|
508
|
+
) as Record<string, unknown> | undefined
|
|
509
|
+
const text = String(lastAssistant?.content || lastAssistant?.text || '')
|
|
510
|
+
if (!text) return null
|
|
511
|
+
|
|
512
|
+
const action = parseLlmAction(text)
|
|
513
|
+
if (!action) return null
|
|
514
|
+
|
|
515
|
+
return { actions: [action as { action: string; [k: string]: unknown }], markSubmittedOnExhausted: false }
|
|
516
|
+
}
|
|
517
|
+
|
|
427
518
|
private buildActionPlan(state: Record<string, unknown>, forceFallback: boolean) {
|
|
428
519
|
const mode = String(state.mode || '')
|
|
429
520
|
if (mode === 'tribunal') return this.tribunalAction(state, forceFallback)
|
|
@@ -525,7 +616,8 @@ class ArenaRunner {
|
|
|
525
616
|
while (!this.stopRequested) {
|
|
526
617
|
await this.maybeHeartbeat('in_game', gameId)
|
|
527
618
|
|
|
528
|
-
const
|
|
619
|
+
const statePath = this.subagent ? `/api/games/${gameId}/state/private?includeLlm=true` : `/api/games/${gameId}/state/private`
|
|
620
|
+
const stateResponse = await this.request('GET', statePath, { expectedStatuses: [200, 401, 403, 404] })
|
|
529
621
|
if (stateResponse.status !== 200) {
|
|
530
622
|
this.warn(`state/private unavailable game=${gameId} status=${stateResponse.status}`)
|
|
531
623
|
break
|
|
@@ -556,7 +648,20 @@ class ArenaRunner {
|
|
|
556
648
|
|
|
557
649
|
const phaseRemainingMs = toNumber(state.phaseRemainingMs, null)
|
|
558
650
|
const forceFallback = phaseRemainingMs !== null && phaseRemainingMs <= this.timeoutFallbackMs
|
|
559
|
-
|
|
651
|
+
|
|
652
|
+
// Try LLM first, fallback to template
|
|
653
|
+
let plan: { actions: Array<{ action: string; [k: string]: unknown }>; markSubmittedOnExhausted: boolean } | null = null
|
|
654
|
+
if (this.subagent && !forceFallback) {
|
|
655
|
+
try {
|
|
656
|
+
plan = await this.tryLlmAction(gameId, state)
|
|
657
|
+
if (plan) this.log(`llm action: ${JSON.stringify(plan.actions[0])}`)
|
|
658
|
+
} catch (err) {
|
|
659
|
+
this.warn(`llm action failed: ${(err as Error).message}`)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (!plan) {
|
|
663
|
+
plan = this.buildActionPlan(state, forceFallback)
|
|
664
|
+
}
|
|
560
665
|
if (!plan) {
|
|
561
666
|
actionDonePhaseKey = phaseKey
|
|
562
667
|
await sleep(this.pollSeconds * 1000)
|
|
@@ -675,6 +780,7 @@ async function startRunner(api: OpenClawApi, agent: AgentEntry): Promise<void> {
|
|
|
675
780
|
return
|
|
676
781
|
}
|
|
677
782
|
const cfg = getConfig(api)
|
|
783
|
+
const subagent = api.runtime?.subagent || null
|
|
678
784
|
const r = new ArenaRunner({
|
|
679
785
|
baseUrl: agent.baseUrl || cfg.baseUrl,
|
|
680
786
|
apiKey: agent.apiKey,
|
|
@@ -684,6 +790,7 @@ async function startRunner(api: OpenClawApi, agent: AgentEntry): Promise<void> {
|
|
|
684
790
|
timeoutFallbackMs: cfg.timeoutFallbackMs,
|
|
685
791
|
log: (msg: string) => api.logger?.info?.(`[clawarena:${agent.name || agent.agentId}] ${msg}`),
|
|
686
792
|
warn: (msg: string) => api.logger?.warn?.(`[clawarena:${agent.name || agent.agentId}] ${msg}`),
|
|
793
|
+
subagent,
|
|
687
794
|
})
|
|
688
795
|
runners.set(agent.agentId, r)
|
|
689
796
|
r.start().catch((err: Error) => {
|
|
@@ -941,6 +1048,30 @@ export default function register(api: OpenClawApi) {
|
|
|
941
1048
|
} catch (err: unknown) { process.stdout.write(`resume error: ${(err as Error).message}\n`) }
|
|
942
1049
|
}
|
|
943
1050
|
|
|
1051
|
+
async function handleGames() {
|
|
1052
|
+
const cfg = getConfig(api)
|
|
1053
|
+
const baseUrl = cfg.baseUrl
|
|
1054
|
+
try {
|
|
1055
|
+
const res = await requestArena(baseUrl, '', 'GET', '/api/modes', { expectedStatuses: [200] })
|
|
1056
|
+
const modes = Array.isArray(res.data.modes) ? res.data.modes : []
|
|
1057
|
+
if (modes.length === 0) {
|
|
1058
|
+
process.stdout.write('No game modes available.\n')
|
|
1059
|
+
return
|
|
1060
|
+
}
|
|
1061
|
+
const lines: string[] = [`Available game modes (${modes.length}):\n`]
|
|
1062
|
+
for (const m of modes) {
|
|
1063
|
+
const mode = isObject(m) ? m : {} as Record<string, unknown>
|
|
1064
|
+
const players = `${mode.minPlayers ?? '?'}-${mode.maxPlayers ?? '?'} players`
|
|
1065
|
+
lines.push(` ${String(mode.mode || '?').padEnd(20)} ${String(mode.displayName || '').padEnd(22)} ${players}`)
|
|
1066
|
+
if (mode.description) lines.push(` ${''.padEnd(20)} ${truncate(mode.description, 60)}`)
|
|
1067
|
+
}
|
|
1068
|
+
lines.push(`\nSet preferred modes: openclaw clawarena modes <mode1,mode2>`)
|
|
1069
|
+
process.stdout.write(`${lines.join('\n')}\n`)
|
|
1070
|
+
} catch (err: unknown) {
|
|
1071
|
+
process.stdout.write(`games error: ${(err as Error).message}\n`)
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
944
1075
|
async function handleModes(modesArg?: string) {
|
|
945
1076
|
const modes = (typeof modesArg === 'string' ? modesArg : '').split(',').map(s => s.trim()).filter(Boolean)
|
|
946
1077
|
if (modes.length === 0) { process.stdout.write('Usage: openclaw clawarena modes tribunal,texas_holdem\n'); return }
|
|
@@ -961,10 +1092,10 @@ export default function register(api: OpenClawApi) {
|
|
|
961
1092
|
// Colon format (backward compat)
|
|
962
1093
|
'clawarena:create', 'clawarena:connect', 'clawarena:ls',
|
|
963
1094
|
'clawarena:status', 'clawarena:start', 'clawarena:stop',
|
|
964
|
-
'clawarena:pause', 'clawarena:resume', 'clawarena:modes',
|
|
1095
|
+
'clawarena:pause', 'clawarena:resume', 'clawarena:modes', 'clawarena:games',
|
|
965
1096
|
'clawarena-openclaw:create', 'clawarena-openclaw:connect', 'clawarena-openclaw:ls',
|
|
966
1097
|
'clawarena-openclaw:status', 'clawarena-openclaw:start', 'clawarena-openclaw:stop',
|
|
967
|
-
'clawarena-openclaw:pause', 'clawarena-openclaw:resume', 'clawarena-openclaw:modes',
|
|
1098
|
+
'clawarena-openclaw:pause', 'clawarena-openclaw:resume', 'clawarena-openclaw:modes', 'clawarena-openclaw:games',
|
|
968
1099
|
]
|
|
969
1100
|
|
|
970
1101
|
api.registerCli?.(({ program }) => {
|
|
@@ -985,6 +1116,7 @@ export default function register(api: OpenClawApi) {
|
|
|
985
1116
|
cmd(arenaGroup, 'pause').description('Pause matchmaking').action(handlePause)
|
|
986
1117
|
cmd(arenaGroup, 'resume').description('Resume matchmaking').action(handleResume)
|
|
987
1118
|
cmd(arenaGroup, 'modes').description('Set preferred game modes').argument('[modes]', 'Comma-separated modes').action(handleModes)
|
|
1119
|
+
cmd(arenaGroup, 'games').description('List all available game modes').action(handleGames)
|
|
988
1120
|
|
|
989
1121
|
// ── Colon format: `openclaw clawarena:ls` (backward compat) ──
|
|
990
1122
|
for (const prefix of ['clawarena', 'clawarena-openclaw']) {
|
|
@@ -997,6 +1129,7 @@ export default function register(api: OpenClawApi) {
|
|
|
997
1129
|
cmd(program, `${prefix}:pause`).description('Pause matchmaking').action(handlePause)
|
|
998
1130
|
cmd(program, `${prefix}:resume`).description('Resume matchmaking').action(handleResume)
|
|
999
1131
|
cmd(program, `${prefix}:modes`).description('Set preferred game modes').argument('[modes]', 'Comma-separated modes').action(handleModes)
|
|
1132
|
+
cmd(program, `${prefix}:games`).description('List all available game modes').action(handleGames)
|
|
1000
1133
|
}
|
|
1001
1134
|
|
|
1002
1135
|
}, { commands: CLI_COMMANDS })
|
package/openclaw.plugin.json
CHANGED