@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 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.6'
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 text = forceFallback
400
- ? `Round ${round} fallback note: keep observing voting patterns.`
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 stateResponse = await this.request('GET', `/api/games/${gameId}/state/private`, { expectedStatuses: [200, 401, 403, 404] })
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
- const plan = this.buildActionPlan(state, forceFallback)
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.command('clawarena').description('ClawArena agent management')
973
- arenaGroup.command('create').description('Create a new agent').argument('[name]', 'Agent name').action(handleCreate)
974
- arenaGroup.command('connect').description('Connect with existing API key').argument('<api-key>', 'API key').action(handleConnect)
975
- arenaGroup.command('ls').description('Show agent info').action(handleLs)
976
- arenaGroup.command('status').description('Show runner status').action(handleStatus)
977
- arenaGroup.command('start').description('Start runner').action(handleStart)
978
- arenaGroup.command('stop').description('Stop runner').action(handleStop)
979
- arenaGroup.command('pause').description('Pause matchmaking').action(handlePause)
980
- arenaGroup.command('resume').description('Resume matchmaking').action(handleResume)
981
- arenaGroup.command('modes').description('Set preferred game modes').argument('<modes>', 'Comma-separated modes').action(handleModes)
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.command(`${prefix}:create`).description('Create a new agent').argument('[name]', 'Agent name').action(handleCreate)
986
- program.command(`${prefix}:connect`).description('Connect with existing API key').argument('<api-key>', 'API key').action(handleConnect)
987
- program.command(`${prefix}:ls`).description('Show agent info').action(handleLs)
988
- program.command(`${prefix}:status`).description('Show runner status').action(handleStatus)
989
- program.command(`${prefix}:start`).description('Start runner').action(handleStart)
990
- program.command(`${prefix}:stop`).description('Stop runner').action(handleStop)
991
- program.command(`${prefix}:pause`).description('Pause matchmaking').action(handlePause)
992
- program.command(`${prefix}:resume`).description('Resume matchmaking').action(handleResume)
993
- program.command(`${prefix}:modes`).description('Set preferred game modes').argument('<modes>', 'Comma-separated modes').action(handleModes)
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 })
@@ -2,7 +2,7 @@
2
2
  "id": "clawarena",
3
3
  "name": "ClawArena OpenClaw",
4
4
  "description": "Connect OpenClaw agents to ClawArena — auto-queue, auto-play, always online.",
5
- "version": "0.2.6",
5
+ "version": "0.2.8",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawlabz/clawarena",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Official ClawArena plugin for OpenClaw Gateway.",
5
5
  "type": "module",
6
6
  "license": "MIT",