@clawlabz/clawarena 0.2.6 → 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 +166 -27
- 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
|
// ============================================================
|
|
@@ -33,6 +55,7 @@ interface CliCommandChain {
|
|
|
33
55
|
option: (flags: string, desc: string) => CliCommandChain
|
|
34
56
|
action: (handler: (...args: unknown[]) => void) => CliCommandChain
|
|
35
57
|
command: (name: string) => CliCommandChain
|
|
58
|
+
allowExcessArguments: (allow: boolean) => CliCommandChain
|
|
36
59
|
}
|
|
37
60
|
|
|
38
61
|
interface CliProgram {
|
|
@@ -43,6 +66,12 @@ interface RegisterCliContext {
|
|
|
43
66
|
program: CliProgram
|
|
44
67
|
}
|
|
45
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
|
+
|
|
46
75
|
interface OpenClawApi {
|
|
47
76
|
config?: Record<string, unknown>
|
|
48
77
|
logger?: {
|
|
@@ -56,6 +85,7 @@ interface OpenClawApi {
|
|
|
56
85
|
options?: { commands?: string[] }
|
|
57
86
|
) => void
|
|
58
87
|
registerService?: (service: { id: string; start?: () => void; stop?: () => void }) => void
|
|
88
|
+
runtime?: { subagent?: SubagentApi }
|
|
59
89
|
}
|
|
60
90
|
|
|
61
91
|
// ============================================================
|
|
@@ -80,6 +110,15 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
80
110
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
81
111
|
}
|
|
82
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
|
+
|
|
83
122
|
function toRetrySeconds(opts: { body?: Record<string, unknown>; headers?: Headers; fallback?: number }): number {
|
|
84
123
|
const fallback = opts.fallback ?? 3
|
|
85
124
|
const retryFromBodySeconds = toNumber(opts.body?.retryAfterSeconds, null)
|
|
@@ -298,6 +337,7 @@ class ArenaRunner {
|
|
|
298
337
|
private timeoutFallbackMs: number
|
|
299
338
|
private log: LogFn
|
|
300
339
|
private warn: LogFn
|
|
340
|
+
private subagent: SubagentApi | null
|
|
301
341
|
|
|
302
342
|
agentId = ''
|
|
303
343
|
agentName = ''
|
|
@@ -321,6 +361,7 @@ class ArenaRunner {
|
|
|
321
361
|
timeoutFallbackMs: number
|
|
322
362
|
log: LogFn
|
|
323
363
|
warn: LogFn
|
|
364
|
+
subagent?: SubagentApi | null
|
|
324
365
|
}) {
|
|
325
366
|
this.baseUrl = opts.baseUrl
|
|
326
367
|
this.apiKey = opts.apiKey
|
|
@@ -330,6 +371,7 @@ class ArenaRunner {
|
|
|
330
371
|
this.timeoutFallbackMs = opts.timeoutFallbackMs
|
|
331
372
|
this.log = opts.log
|
|
332
373
|
this.warn = opts.warn
|
|
374
|
+
this.subagent = opts.subagent || null
|
|
333
375
|
}
|
|
334
376
|
|
|
335
377
|
private async request(method: string, path: string, opts?: { body?: unknown; expectedStatuses?: number[] }) {
|
|
@@ -389,16 +431,24 @@ class ArenaRunner {
|
|
|
389
431
|
return null
|
|
390
432
|
}
|
|
391
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
|
+
|
|
392
442
|
private tribunalAction(state: Record<string, unknown>, forceFallback: boolean) {
|
|
393
443
|
const phase = String(state.phase || '')
|
|
394
444
|
const round = Number(state.round || 1)
|
|
395
445
|
const players = Array.isArray(state.players) ? state.players : []
|
|
396
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'
|
|
397
448
|
|
|
398
449
|
if (phase === 'day') {
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
: `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)
|
|
402
452
|
return { actions: [{ action: 'speak', text }], markSubmittedOnExhausted: false }
|
|
403
453
|
}
|
|
404
454
|
if (phase === 'vote' && target) {
|
|
@@ -423,6 +473,48 @@ class ArenaRunner {
|
|
|
423
473
|
return false
|
|
424
474
|
}
|
|
425
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
|
+
|
|
426
518
|
private buildActionPlan(state: Record<string, unknown>, forceFallback: boolean) {
|
|
427
519
|
const mode = String(state.mode || '')
|
|
428
520
|
if (mode === 'tribunal') return this.tribunalAction(state, forceFallback)
|
|
@@ -524,7 +616,8 @@ class ArenaRunner {
|
|
|
524
616
|
while (!this.stopRequested) {
|
|
525
617
|
await this.maybeHeartbeat('in_game', gameId)
|
|
526
618
|
|
|
527
|
-
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] })
|
|
528
621
|
if (stateResponse.status !== 200) {
|
|
529
622
|
this.warn(`state/private unavailable game=${gameId} status=${stateResponse.status}`)
|
|
530
623
|
break
|
|
@@ -555,7 +648,20 @@ class ArenaRunner {
|
|
|
555
648
|
|
|
556
649
|
const phaseRemainingMs = toNumber(state.phaseRemainingMs, null)
|
|
557
650
|
const forceFallback = phaseRemainingMs !== null && phaseRemainingMs <= this.timeoutFallbackMs
|
|
558
|
-
|
|
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
|
+
}
|
|
559
665
|
if (!plan) {
|
|
560
666
|
actionDonePhaseKey = phaseKey
|
|
561
667
|
await sleep(this.pollSeconds * 1000)
|
|
@@ -674,6 +780,7 @@ async function startRunner(api: OpenClawApi, agent: AgentEntry): Promise<void> {
|
|
|
674
780
|
return
|
|
675
781
|
}
|
|
676
782
|
const cfg = getConfig(api)
|
|
783
|
+
const subagent = api.runtime?.subagent || null
|
|
677
784
|
const r = new ArenaRunner({
|
|
678
785
|
baseUrl: agent.baseUrl || cfg.baseUrl,
|
|
679
786
|
apiKey: agent.apiKey,
|
|
@@ -683,6 +790,7 @@ async function startRunner(api: OpenClawApi, agent: AgentEntry): Promise<void> {
|
|
|
683
790
|
timeoutFallbackMs: cfg.timeoutFallbackMs,
|
|
684
791
|
log: (msg: string) => api.logger?.info?.(`[clawarena:${agent.name || agent.agentId}] ${msg}`),
|
|
685
792
|
warn: (msg: string) => api.logger?.warn?.(`[clawarena:${agent.name || agent.agentId}] ${msg}`),
|
|
793
|
+
subagent,
|
|
686
794
|
})
|
|
687
795
|
runners.set(agent.agentId, r)
|
|
688
796
|
r.start().catch((err: Error) => {
|
|
@@ -940,6 +1048,30 @@ export default function register(api: OpenClawApi) {
|
|
|
940
1048
|
} catch (err: unknown) { process.stdout.write(`resume error: ${(err as Error).message}\n`) }
|
|
941
1049
|
}
|
|
942
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
|
+
|
|
943
1075
|
async function handleModes(modesArg?: string) {
|
|
944
1076
|
const modes = (typeof modesArg === 'string' ? modesArg : '').split(',').map(s => s.trim()).filter(Boolean)
|
|
945
1077
|
if (modes.length === 0) { process.stdout.write('Usage: openclaw clawarena modes tribunal,texas_holdem\n'); return }
|
|
@@ -960,37 +1092,44 @@ export default function register(api: OpenClawApi) {
|
|
|
960
1092
|
// Colon format (backward compat)
|
|
961
1093
|
'clawarena:create', 'clawarena:connect', 'clawarena:ls',
|
|
962
1094
|
'clawarena:status', 'clawarena:start', 'clawarena:stop',
|
|
963
|
-
'clawarena:pause', 'clawarena:resume', 'clawarena:modes',
|
|
1095
|
+
'clawarena:pause', 'clawarena:resume', 'clawarena:modes', 'clawarena:games',
|
|
964
1096
|
'clawarena-openclaw:create', 'clawarena-openclaw:connect', 'clawarena-openclaw:ls',
|
|
965
1097
|
'clawarena-openclaw:status', 'clawarena-openclaw:start', 'clawarena-openclaw:stop',
|
|
966
|
-
'clawarena-openclaw:pause', 'clawarena-openclaw:resume', 'clawarena-openclaw:modes',
|
|
1098
|
+
'clawarena-openclaw:pause', 'clawarena-openclaw:resume', 'clawarena-openclaw:modes', 'clawarena-openclaw:games',
|
|
967
1099
|
]
|
|
968
1100
|
|
|
969
1101
|
api.registerCli?.(({ program }) => {
|
|
970
1102
|
|
|
1103
|
+
// Helper: configure a command with allowExcessArguments for robustness
|
|
1104
|
+
function cmd(parent: CliCommandChain | CliProgram, name: string) {
|
|
1105
|
+
return parent.command(name).allowExcessArguments(true)
|
|
1106
|
+
}
|
|
1107
|
+
|
|
971
1108
|
// ── Space format: `openclaw clawarena <sub>` ──
|
|
972
|
-
const arenaGroup = program
|
|
973
|
-
arenaGroup
|
|
974
|
-
arenaGroup
|
|
975
|
-
arenaGroup
|
|
976
|
-
arenaGroup
|
|
977
|
-
arenaGroup
|
|
978
|
-
arenaGroup
|
|
979
|
-
arenaGroup
|
|
980
|
-
arenaGroup
|
|
981
|
-
arenaGroup
|
|
1109
|
+
const arenaGroup = cmd(program, 'clawarena').description('ClawArena agent management')
|
|
1110
|
+
cmd(arenaGroup, 'create').description('Create a new agent').argument('[name]', 'Agent name').action(handleCreate)
|
|
1111
|
+
cmd(arenaGroup, 'connect').description('Connect with existing API key').argument('[api-key]', 'API key').action(handleConnect)
|
|
1112
|
+
cmd(arenaGroup, 'ls').description('Show agent info').action(handleLs)
|
|
1113
|
+
cmd(arenaGroup, 'status').description('Show runner status').action(handleStatus)
|
|
1114
|
+
cmd(arenaGroup, 'start').description('Start runner').action(handleStart)
|
|
1115
|
+
cmd(arenaGroup, 'stop').description('Stop runner').action(handleStop)
|
|
1116
|
+
cmd(arenaGroup, 'pause').description('Pause matchmaking').action(handlePause)
|
|
1117
|
+
cmd(arenaGroup, 'resume').description('Resume matchmaking').action(handleResume)
|
|
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)
|
|
982
1120
|
|
|
983
1121
|
// ── Colon format: `openclaw clawarena:ls` (backward compat) ──
|
|
984
1122
|
for (const prefix of ['clawarena', 'clawarena-openclaw']) {
|
|
985
|
-
program
|
|
986
|
-
program
|
|
987
|
-
program
|
|
988
|
-
program
|
|
989
|
-
program
|
|
990
|
-
program
|
|
991
|
-
program
|
|
992
|
-
program
|
|
993
|
-
program
|
|
1123
|
+
cmd(program, `${prefix}:create`).description('Create a new agent').argument('[name]', 'Agent name').action(handleCreate)
|
|
1124
|
+
cmd(program, `${prefix}:connect`).description('Connect with existing API key').argument('[api-key]', 'API key').action(handleConnect)
|
|
1125
|
+
cmd(program, `${prefix}:ls`).description('Show agent info').action(handleLs)
|
|
1126
|
+
cmd(program, `${prefix}:status`).description('Show runner status').action(handleStatus)
|
|
1127
|
+
cmd(program, `${prefix}:start`).description('Start runner').action(handleStart)
|
|
1128
|
+
cmd(program, `${prefix}:stop`).description('Stop runner').action(handleStop)
|
|
1129
|
+
cmd(program, `${prefix}:pause`).description('Pause matchmaking').action(handlePause)
|
|
1130
|
+
cmd(program, `${prefix}:resume`).description('Resume matchmaking').action(handleResume)
|
|
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)
|
|
994
1133
|
}
|
|
995
1134
|
|
|
996
1135
|
}, { commands: CLI_COMMANDS })
|
package/openclaw.plugin.json
CHANGED