dojozero-client 0.2.1__tar.gz → 0.3.0__tar.gz

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.
Files changed (19) hide show
  1. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/.gitignore +7 -2
  2. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/PKG-INFO +3 -3
  3. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/README.md +1 -1
  4. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/pyproject.toml +2 -2
  5. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/__init__.py +3 -1
  6. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_client.py +22 -9
  7. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_config.py +2 -2
  8. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_daemon.py +72 -26
  9. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/tests/test_client.py +3 -3
  10. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/tests/test_daemon.py +103 -73
  11. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_cli.py +0 -0
  12. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_credentials.py +0 -0
  13. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_exceptions.py +0 -0
  14. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_rpc.py +0 -0
  15. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_strategy.py +0 -0
  16. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/src/dojozero_client/_transport.py +0 -0
  17. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/tests/test_config.py +0 -0
  18. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/tests/test_credentials.py +0 -0
  19. {dojozero_client-0.2.1 → dojozero_client-0.3.0}/tests/test_rpc.py +0 -0
@@ -158,7 +158,8 @@ uv.lock
158
158
  Thumbs.db
159
159
 
160
160
  # Sample scenario artifacts
161
- dojozero-store
161
+ dojozero-store/
162
+ store/
162
163
 
163
164
  # Data
164
165
  /outputs/
@@ -174,4 +175,8 @@ package-lock.json
174
175
  logs/
175
176
 
176
177
  # node modules
177
- node_modules/
178
+ node_modules/
179
+
180
+
181
+ agents/llms/*.yaml
182
+ !agents/llms/default.yaml
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dojozero-client
3
- Version: 0.2.1
4
- Summary: Python SDK for external agents participating in DojoZero trials. Works with OpenClaw and CoPaw.
3
+ Version: 0.3.0
4
+ Summary: Python SDK for external agents participating in DojoZero trials. Works with OpenClaw and QwenPaw.
5
5
  Project-URL: Homepage, https://github.com/agentscope-ai/DojoZero
6
6
  Project-URL: Repository, https://github.com/agentscope-ai/DojoZero
7
7
  Project-URL: Issues, https://github.com/agentscope-ai/DojoZero/issues
@@ -149,7 +149,7 @@ asyncio.run(main())
149
149
 
150
150
  ## Documentation
151
151
 
152
- For the full API reference, daemon mode, multiple agent profiles, and Agent Skill integration (OpenClaw / CoPaw / AgentScope), see the [full documentation](https://github.com/agentscope-ai/DojoZero/tree/main/docs).
152
+ For the full API reference, daemon mode, multiple agent profiles, and Agent Skill integration (OpenClaw / QwenPaw / AgentScope), see the [full documentation](https://github.com/agentscope-ai/DojoZero/tree/main/docs).
153
153
 
154
154
  ## License
155
155
 
@@ -127,7 +127,7 @@ asyncio.run(main())
127
127
 
128
128
  ## Documentation
129
129
 
130
- For the full API reference, daemon mode, multiple agent profiles, and Agent Skill integration (OpenClaw / CoPaw / AgentScope), see the [full documentation](https://github.com/agentscope-ai/DojoZero/tree/main/docs).
130
+ For the full API reference, daemon mode, multiple agent profiles, and Agent Skill integration (OpenClaw / QwenPaw / AgentScope), see the [full documentation](https://github.com/agentscope-ai/DojoZero/tree/main/docs).
131
131
 
132
132
  ## License
133
133
 
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dojozero-client"
7
- version = "0.2.1"
8
- description = "Python SDK for external agents participating in DojoZero trials. Works with OpenClaw and CoPaw."
7
+ version = "0.3.0"
8
+ description = "Python SDK for external agents participating in DojoZero trials. Works with OpenClaw and QwenPaw."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
11
11
  license = {text = "MIT"}
@@ -27,6 +27,7 @@ from dojozero_client._client import (
27
27
  GatewayInfo,
28
28
  Holding,
29
29
  Odds,
30
+ PollResult,
30
31
  TrialConnection,
31
32
  TrialEndedEvent,
32
33
  TrialMetadata,
@@ -60,7 +61,7 @@ from dojozero_client._exceptions import (
60
61
  )
61
62
  from dojozero_client._transport import GatewayTransport, SSEEvent
62
63
 
63
- __version__ = "0.1.0"
64
+ __version__ = "0.3.0"
64
65
 
65
66
  __all__ = [
66
67
  # Main client
@@ -74,6 +75,7 @@ __all__ = [
74
75
  "GatewayInfo",
75
76
  "Holding",
76
77
  "Odds",
78
+ "PollResult",
77
79
  "TrialEndedEvent",
78
80
  "TrialMetadata",
79
81
  "TrialResults",
@@ -262,6 +262,19 @@ class TrialResults:
262
262
  )
263
263
 
264
264
 
265
+ @dataclass
266
+ class PollResult:
267
+ """Result of a poll_events call."""
268
+
269
+ events: list[EventEnvelope]
270
+ trial_ended: dict[str, Any] | None = None
271
+
272
+ @property
273
+ def is_trial_ended(self) -> bool:
274
+ """Check if trial has ended."""
275
+ return self.trial_ended is not None
276
+
277
+
265
278
  class TrialConnection:
266
279
  """Connection to a specific trial.
267
280
 
@@ -450,24 +463,20 @@ class TrialConnection:
450
463
  async def poll_events(
451
464
  self,
452
465
  since: int | None = None,
453
- limit: int = 50,
454
- event_types: list[str] | None = None,
455
- ) -> list[EventEnvelope]:
466
+ limit: int = 100,
467
+ ) -> "PollResult":
456
468
  """Poll for recent events.
457
469
 
458
470
  Args:
459
471
  since: Get events after this sequence number
460
- limit: Maximum events to return
461
- event_types: Optional event type filter
472
+ limit: Maximum events to return (max 100)
462
473
 
463
474
  Returns:
464
- List of EventEnvelope objects
475
+ PollResult with events and trial_ended info
465
476
  """
466
477
  params: dict[str, Any] = {"limit": limit}
467
478
  if since is not None:
468
479
  params["since"] = since
469
- if event_types:
470
- params["event_types"] = ",".join(event_types)
471
480
 
472
481
  response = await self._transport.request(
473
482
  "GET",
@@ -482,7 +491,10 @@ class TrialConnection:
482
491
  if current_seq > self._last_sequence:
483
492
  self._last_sequence = current_seq
484
493
 
485
- return events
494
+ # Check for trial ended
495
+ trial_ended = response.get("trialEnded")
496
+
497
+ return PollResult(events=events, trial_ended=trial_ended)
486
498
 
487
499
  async def get_current_odds(self) -> Odds:
488
500
  """Get current betting odds.
@@ -880,6 +892,7 @@ __all__ = [
880
892
  "EventEnvelope",
881
893
  "GatewayInfo",
882
894
  "Odds",
895
+ "PollResult",
883
896
  "TrialConnection",
884
897
  "TrialEndedEvent",
885
898
  "TrialMetadata",
@@ -42,7 +42,7 @@ TRIALS_DIR = CONFIG_DIR / "trials"
42
42
  # Default config template with comments
43
43
  DEFAULT_CONFIG_TEMPLATE = """\
44
44
  # DojoZero Client Configuration
45
- # See: https://github.com/anthropics/dojozero
45
+ # See: https://github.com/agentscope-ai/dojozero
46
46
 
47
47
  # Dashboard server URL
48
48
  # For local development: http://localhost:8000
@@ -130,7 +130,7 @@ def save_config(
130
130
  # Write with comments
131
131
  content = f"""\
132
132
  # DojoZero Client Configuration
133
- # See: https://github.com/anthropics/dojozero
133
+ # See: https://github.com/agentscope-ai/dojozero
134
134
 
135
135
  # Dashboard server URL
136
136
  dashboard_url: {dashboard_url}
@@ -18,7 +18,7 @@ from datetime import datetime, timezone
18
18
  from pathlib import Path
19
19
  from typing import TYPE_CHECKING, Any
20
20
 
21
- from dojozero_client._client import AgentResult, DojoClient, EventEnvelope
21
+ from dojozero_client._client import AgentResult, DojoClient, EventEnvelope, PollResult
22
22
  from dojozero_client._config import (
23
23
  CONFIG_DIR,
24
24
  PID_FILE,
@@ -550,37 +550,64 @@ class TrialHandler:
550
550
  return events
551
551
 
552
552
  async def _event_loop(self) -> None:
553
- """Main event processing loop."""
553
+ """Main event processing loop using polling."""
554
554
  if not self._trial:
555
555
  return
556
556
 
557
- try:
558
- async for event in self._trial.events(
559
- event_types=self.filters,
560
- raise_on_trial_end=False,
561
- ):
562
- if not self._running:
563
- break
564
- await self._handle_event(event)
557
+ poll_interval = float(os.environ.get("DOJOZERO_POLL_INTERVAL", "5.0"))
565
558
 
566
- # Check if trial ended naturally
567
- if self._trial.trial_ended is not None:
568
- ended = self._trial.trial_ended
569
- logger.info(
570
- "Trial %s ended (reason=%s, agents=%d)",
571
- self.trial_id,
572
- ended.reason,
573
- len(ended.final_results),
574
- )
575
- self._state.status = ended.reason
576
- if ended.final_results:
577
- _write_results(
578
- self.state_dir / "results.json",
559
+ try:
560
+ while self._running:
561
+ try:
562
+ result: PollResult = await self._trial.poll_events(
563
+ since=self._state.last_event_sequence,
564
+ )
565
+ except Exception as e:
566
+ # 404 or connection error means gateway is gone
567
+ logger.info(
568
+ "Trial %s: Poll failed (%s), treating as trial ended",
579
569
  self.trial_id,
580
- ended.reason,
581
- ended.final_results,
570
+ e,
582
571
  )
583
- self._save_state()
572
+ await self._handle_trial_ended("completed", [])
573
+ return
574
+
575
+ for event in result.events:
576
+ if not self._running:
577
+ return
578
+ await self._handle_event(event)
579
+
580
+ if result.is_trial_ended:
581
+ reason = (
582
+ result.trial_ended.get("reason", "completed")
583
+ if result.trial_ended
584
+ else "completed"
585
+ )
586
+ logger.info("Trial %s ended (reason=%s)", self.trial_id, reason)
587
+ # Fetch final results from server
588
+ try:
589
+ results_resp = await self._trial.get_results()
590
+ final_results = [
591
+ AgentResult(
592
+ agent_id=r.agent_id,
593
+ final_balance=r.final_balance,
594
+ net_profit=r.net_profit,
595
+ total_bets=r.total_bets,
596
+ win_rate=r.win_rate,
597
+ roi=r.roi,
598
+ )
599
+ for r in results_resp.results
600
+ ]
601
+ except Exception:
602
+ final_results = []
603
+ await self._handle_trial_ended(reason, final_results)
604
+ return
605
+
606
+ # If we got a full page, there may be more — poll again
607
+ # immediately to catch up. Otherwise wait before next poll.
608
+ if len(result.events) < 100:
609
+ await asyncio.sleep(poll_interval)
610
+
584
611
  except asyncio.CancelledError:
585
612
  pass
586
613
  except Exception as e:
@@ -588,6 +615,25 @@ class TrialHandler:
588
615
  self._state.status = "error"
589
616
  self._save_state()
590
617
 
618
+ async def _handle_trial_ended(
619
+ self, reason: str, final_results: list[AgentResult]
620
+ ) -> None:
621
+ """Handle trial ended — write results and update state."""
622
+ self._state.status = reason
623
+ if final_results:
624
+ _write_results(
625
+ self.state_dir / "results.json",
626
+ self.trial_id,
627
+ reason,
628
+ final_results,
629
+ )
630
+ # Update balance from our own settled result
631
+ for r in final_results:
632
+ if r.agent_id == self.agent_id:
633
+ self._state.balance = r.final_balance
634
+ break
635
+ self._save_state()
636
+
591
637
  async def _handle_event(self, event: EventEnvelope) -> None:
592
638
  """Process an incoming event."""
593
639
  # Skip events already seen (replayed on reconnect)
@@ -210,11 +210,11 @@ class TestReconnection:
210
210
  """Test extracting agent_id from error message."""
211
211
  import re
212
212
 
213
- # Test format: "Agent copaw-agent already connected"
214
- error_msg = "Agent copaw-agent already connected"
213
+ # Test format: "Agent qwenpaw-agent already connected"
214
+ error_msg = "Agent qwenpaw-agent already connected"
215
215
  match = re.search(r"Agent (\S+) already", error_msg)
216
216
  assert match is not None
217
- assert match.group(1) == "copaw-agent"
217
+ assert match.group(1) == "qwenpaw-agent"
218
218
 
219
219
  def test_extract_agent_id_from_json_error(self):
220
220
  """Test extracting agent_id from JSON error."""
@@ -9,7 +9,12 @@ from unittest.mock import AsyncMock, MagicMock, patch
9
9
 
10
10
  import pytest
11
11
 
12
- from dojozero_client._client import AgentResult, EventEnvelope, TrialEndedEvent
12
+ from dojozero_client._client import (
13
+ AgentResult,
14
+ EventEnvelope,
15
+ PollResult,
16
+ TrialResults,
17
+ )
13
18
  from dojozero_client._daemon import (
14
19
  DaemonState,
15
20
  TrialHandler,
@@ -273,24 +278,8 @@ def _make_agent_result(**overrides: object) -> AgentResult:
273
278
  return AgentResult(**defaults) # type: ignore[arg-type]
274
279
 
275
280
 
276
- def _make_trial_ended(
277
- reason: str = "completed",
278
- results: list[AgentResult] | None = None,
279
- ) -> TrialEndedEvent:
280
- """Create a TrialEndedEvent with sensible defaults."""
281
- if results is None:
282
- results = [_make_agent_result()]
283
- return TrialEndedEvent(
284
- trial_id="test-trial",
285
- reason=reason,
286
- timestamp=datetime.now(timezone.utc),
287
- final_results=results,
288
- message=f"Trial has {reason}",
289
- )
290
-
291
-
292
281
  class TestTrialHandlerTrialEnd:
293
- """Tests for TrialHandler handling trial_ended events."""
282
+ """Tests for TrialHandler handling trial_ended via polling."""
294
283
 
295
284
  @pytest.mark.asyncio
296
285
  async def test_event_loop_handles_completed_trial(self):
@@ -306,21 +295,40 @@ class TestTrialHandlerTrialEnd:
306
295
  handler._state.status = "connected"
307
296
  handler.state_dir.mkdir(parents=True, exist_ok=True)
308
297
 
309
- # Mock trial that yields one event then ends
310
298
  mock_trial = AsyncMock()
311
- ended = _make_trial_ended("completed")
299
+ call_count = 0
300
+
301
+ async def mock_poll(**kwargs):
302
+ nonlocal call_count
303
+ call_count += 1
304
+ if call_count == 1:
305
+ # First poll: return an event
306
+ return PollResult(
307
+ events=[
308
+ EventEnvelope(
309
+ trial_id="test-trial",
310
+ sequence=1,
311
+ timestamp=datetime.now(timezone.utc),
312
+ payload={"event_type": "event.odds_update"},
313
+ )
314
+ ],
315
+ )
316
+ # Second poll: trial ended
317
+ return PollResult(
318
+ events=[],
319
+ trial_ended={"reason": "completed", "message": ""},
320
+ )
312
321
 
313
- async def mock_events(**kwargs):
314
- event = EventEnvelope(
322
+ agent_result = _make_agent_result()
323
+ mock_trial.poll_events = mock_poll
324
+ mock_trial.get_results = AsyncMock(
325
+ return_value=TrialResults(
315
326
  trial_id="test-trial",
316
- sequence=1,
317
- timestamp=datetime.now(timezone.utc),
318
- payload={"event_type": "event.odds_update"},
327
+ status="completed",
328
+ results=[agent_result],
329
+ ended_at=datetime.now(timezone.utc),
319
330
  )
320
- yield event
321
-
322
- mock_trial.events = mock_events
323
- mock_trial.trial_ended = ended
331
+ )
324
332
  handler._trial = mock_trial
325
333
  handler._running = True
326
334
 
@@ -353,14 +361,20 @@ class TestTrialHandlerTrialEnd:
353
361
  handler.state_dir.mkdir(parents=True, exist_ok=True)
354
362
 
355
363
  mock_trial = AsyncMock()
356
- ended = _make_trial_ended("cancelled")
357
-
358
- async def mock_events(**kwargs):
359
- return
360
- yield # make it an async generator
361
-
362
- mock_trial.events = mock_events
363
- mock_trial.trial_ended = ended
364
+ mock_trial.poll_events = AsyncMock(
365
+ return_value=PollResult(
366
+ events=[],
367
+ trial_ended={"reason": "cancelled", "message": ""},
368
+ )
369
+ )
370
+ mock_trial.get_results = AsyncMock(
371
+ return_value=TrialResults(
372
+ trial_id="test-trial",
373
+ status="cancelled",
374
+ results=[],
375
+ ended_at=None,
376
+ )
377
+ )
364
378
  handler._trial = mock_trial
365
379
  handler._running = True
366
380
 
@@ -383,14 +397,20 @@ class TestTrialHandlerTrialEnd:
383
397
  handler.state_dir.mkdir(parents=True, exist_ok=True)
384
398
 
385
399
  mock_trial = AsyncMock()
386
- ended = _make_trial_ended("failed", results=[])
387
-
388
- async def mock_events(**kwargs):
389
- return
390
- yield
391
-
392
- mock_trial.events = mock_events
393
- mock_trial.trial_ended = ended
400
+ mock_trial.poll_events = AsyncMock(
401
+ return_value=PollResult(
402
+ events=[],
403
+ trial_ended={"reason": "failed", "message": ""},
404
+ )
405
+ )
406
+ mock_trial.get_results = AsyncMock(
407
+ return_value=TrialResults(
408
+ trial_id="test-trial",
409
+ status="failed",
410
+ results=[],
411
+ ended_at=None,
412
+ )
413
+ )
394
414
  handler._trial = mock_trial
395
415
  handler._running = True
396
416
 
@@ -401,8 +421,8 @@ class TestTrialHandlerTrialEnd:
401
421
  assert not (handler.state_dir / "results.json").exists()
402
422
 
403
423
  @pytest.mark.asyncio
404
- async def test_event_loop_no_trial_ended(self):
405
- """Test event loop does not change status when stream ends without trial_ended."""
424
+ async def test_event_loop_gateway_404_treats_as_ended(self):
425
+ """Test poll failure (e.g. gateway 404) is treated as trial ended."""
406
426
  with tempfile.TemporaryDirectory() as tmpdir:
407
427
  with patch("dojozero_client._daemon.TRIALS_DIR", Path(tmpdir)):
408
428
  client = MagicMock()
@@ -415,25 +435,19 @@ class TestTrialHandlerTrialEnd:
415
435
  handler.state_dir.mkdir(parents=True, exist_ok=True)
416
436
 
417
437
  mock_trial = AsyncMock()
418
-
419
- async def mock_events(**kwargs):
420
- return
421
- yield
422
-
423
- mock_trial.events = mock_events
424
- mock_trial.trial_ended = None # No trial_ended event
438
+ mock_trial.poll_events = AsyncMock(
439
+ side_effect=Exception("404 Not Found")
440
+ )
425
441
  handler._trial = mock_trial
426
442
  handler._running = True
427
443
 
428
444
  await handler._event_loop()
429
445
 
430
- # Status should not be changed by event loop
431
- assert handler._state.status == "connected"
432
- assert not (handler.state_dir / "results.json").exists()
446
+ assert handler._state.status == "completed"
433
447
 
434
448
  @pytest.mark.asyncio
435
449
  async def test_event_loop_real_error_sets_error_status(self):
436
- """Test real exceptions still set status=error."""
450
+ """Test unhandled exceptions in event processing set status=error."""
437
451
  with tempfile.TemporaryDirectory() as tmpdir:
438
452
  with patch("dojozero_client._daemon.TRIALS_DIR", Path(tmpdir)):
439
453
  client = MagicMock()
@@ -446,16 +460,26 @@ class TestTrialHandlerTrialEnd:
446
460
  handler.state_dir.mkdir(parents=True, exist_ok=True)
447
461
 
448
462
  mock_trial = AsyncMock()
449
-
450
- async def mock_events(**kwargs):
451
- raise ConnectionError("Network failed")
452
- yield # make it an async generator
453
-
454
- mock_trial.events = mock_events
455
- mock_trial.trial_ended = None
463
+ mock_trial.poll_events = AsyncMock(
464
+ return_value=PollResult(
465
+ events=[
466
+ EventEnvelope(
467
+ trial_id="test-trial",
468
+ sequence=1,
469
+ timestamp=datetime.now(timezone.utc),
470
+ payload={"event_type": "event.test"},
471
+ )
472
+ ],
473
+ )
474
+ )
456
475
  handler._trial = mock_trial
457
476
  handler._running = True
458
477
 
478
+ # Make _handle_event raise to trigger error path
479
+ handler._handle_event = AsyncMock(
480
+ side_effect=RuntimeError("Unexpected crash")
481
+ )
482
+
459
483
  await handler._event_loop()
460
484
 
461
485
  assert handler._state.status == "error"
@@ -483,16 +507,22 @@ class TestTrialHandlerTrialEnd:
483
507
  roi=-0.2,
484
508
  ),
485
509
  ]
486
- ended = _make_trial_ended("completed", results=results)
487
510
 
488
511
  mock_trial = AsyncMock()
489
-
490
- async def mock_events(**kwargs):
491
- return
492
- yield
493
-
494
- mock_trial.events = mock_events
495
- mock_trial.trial_ended = ended
512
+ mock_trial.poll_events = AsyncMock(
513
+ return_value=PollResult(
514
+ events=[],
515
+ trial_ended={"reason": "completed", "message": ""},
516
+ )
517
+ )
518
+ mock_trial.get_results = AsyncMock(
519
+ return_value=TrialResults(
520
+ trial_id="test-trial",
521
+ status="completed",
522
+ results=results,
523
+ ended_at=datetime.now(timezone.utc),
524
+ )
525
+ )
496
526
  handler._trial = mock_trial
497
527
  handler._running = True
498
528