wafer-cli 0.2.9__py3-none-any.whl → 0.2.10__py3-none-any.whl

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.
wafer/wevin_cli.py CHANGED
@@ -25,6 +25,7 @@ class StreamingChunkFrontend:
25
25
 
26
26
  Designed for programmatic consumption by extensions/UIs.
27
27
  Emits events in the format expected by wevin-extension handleWevinEvent:
28
+ - {type: 'session_start', session_id: '...', model: '...'}
28
29
  - {type: 'text_delta', delta: '...'}
29
30
  - {type: 'tool_call_start', tool_name: '...'}
30
31
  - {type: 'tool_call_end', tool_name: '...', args: {...}}
@@ -33,16 +34,31 @@ class StreamingChunkFrontend:
33
34
  - {type: 'error', error: '...'}
34
35
  """
35
36
 
36
- def __init__(self) -> None:
37
+ def __init__(self, session_id: str | None = None, model: str | None = None) -> None:
37
38
  self._current_tool_call: dict | None = None
39
+ self._session_id = session_id
40
+ self._model = model
38
41
 
39
42
  def _emit(self, obj: dict) -> None:
40
43
  """Emit a single NDJSON line."""
41
44
  print(json.dumps(obj, ensure_ascii=False), flush=True)
42
45
 
43
46
  async def start(self) -> None:
44
- """Initialize frontend."""
45
- pass
47
+ """Initialize frontend and emit session_start if session_id is known."""
48
+ if self._session_id:
49
+ self._emit({
50
+ "type": "session_start",
51
+ "session_id": self._session_id,
52
+ "model": self._model,
53
+ })
54
+
55
+ def emit_session_start(self, session_id: str, model: str | None = None) -> None:
56
+ """Emit session_start event (for new sessions created during run)."""
57
+ self._emit({
58
+ "type": "session_start",
59
+ "session_id": session_id,
60
+ "model": model or self._model,
61
+ })
46
62
 
47
63
  async def stop(self) -> None:
48
64
  """Emit session_end event."""
@@ -335,31 +351,116 @@ def main( # noqa: PLR0913, PLR0915
335
351
  single_turn: bool | None = None, # None = use template default
336
352
  model: str | None = None,
337
353
  resume: str | None = None,
354
+ from_turn: int | None = None,
338
355
  tools: list[str] | None = None,
356
+ allow_spawn: bool = False,
357
+ max_tool_fails: int | None = None,
358
+ max_turns: int | None = None,
339
359
  template: str | None = None,
340
360
  template_args: dict[str, str] | None = None,
341
361
  corpus_path: str | None = None,
342
362
  list_sessions: bool = False,
363
+ get_session: str | None = None,
343
364
  json_output: bool = False,
344
- # Legacy args (ignored)
345
- problem: str | None = None,
346
- reference: str | None = None,
347
- **kwargs: object,
348
365
  ) -> None:
349
366
  """Run wevin agent in-process via rollouts."""
367
+ import trio
368
+ from dataclasses import asdict
369
+
350
370
  from wafer_core.rollouts import FileSessionStore
351
371
 
372
+ session_store = FileSessionStore()
373
+
374
+ # Handle --get-session: load session by ID and print
375
+ if get_session:
376
+ async def _get_session() -> None:
377
+ try:
378
+ session, err = await session_store.get(get_session)
379
+ if err or not session:
380
+ if json_output:
381
+ print(json.dumps({"error": err or f"Session {get_session} not found"}))
382
+ sys.exit(1)
383
+ else:
384
+ print(f"Error: {err or 'Session not found'}", file=sys.stderr)
385
+ sys.exit(1)
386
+
387
+ if json_output:
388
+ # Serialize messages to dicts
389
+ try:
390
+ messages_data = [asdict(msg) for msg in session.messages]
391
+ except Exception as e:
392
+ # If serialization fails, return error
393
+ error_msg = f"Failed to serialize messages: {e}"
394
+ print(json.dumps({"error": error_msg}))
395
+ sys.exit(1)
396
+
397
+ print(json.dumps({
398
+ "session_id": session.session_id,
399
+ "status": session.status.value,
400
+ "model": session.endpoint.model if session.endpoint else None,
401
+ "created_at": session.created_at,
402
+ "updated_at": session.updated_at,
403
+ "messages": messages_data,
404
+ "tags": session.tags,
405
+ }))
406
+ else:
407
+ print(f"Session: {session.session_id}")
408
+ print(f"Status: {session.status.value}")
409
+ print(f"Messages: {len(session.messages)}")
410
+ for i, msg in enumerate(session.messages):
411
+ # Fail fast if message can't be converted to string - corrupted data is a bug
412
+ content_preview = str(msg.content)[:100] if msg.content else ""
413
+ print(f" [{i}] {msg.role}: {content_preview}...")
414
+ except KeyboardInterrupt:
415
+ # User cancelled - exit cleanly
416
+ sys.exit(130) # Standard exit code for SIGINT
417
+ except Exception as e:
418
+ # Any other error - log and exit with error
419
+ error_msg = f"Failed to load session {get_session}: {e}"
420
+ if json_output:
421
+ print(json.dumps({"error": error_msg}))
422
+ else:
423
+ print(f"Error: {error_msg}", file=sys.stderr)
424
+ sys.exit(1)
425
+
426
+ try:
427
+ trio.run(_get_session)
428
+ except KeyboardInterrupt:
429
+ sys.exit(130)
430
+ except Exception as e:
431
+ error_msg = f"Failed to run session loader: {e}"
432
+ if json_output:
433
+ print(json.dumps({"error": error_msg}))
434
+ else:
435
+ print(f"Error: {error_msg}", file=sys.stderr)
436
+ sys.exit(1)
437
+ return
438
+
352
439
  # Handle --list-sessions: show recent sessions and exit
353
440
  if list_sessions:
354
- session_store = FileSessionStore()
355
- sessions = session_store.list_sync(limit=20)
356
- if not sessions:
357
- print("No sessions found.")
358
- else:
359
- print("Recent sessions:")
441
+ sessions = session_store.list_sync(limit=50)
442
+ if json_output:
443
+ # Return metadata only - messages loaded on-demand via --get-session
444
+ sessions_data = []
360
445
  for s in sessions:
361
- preview = _get_session_preview(s)
362
- print(f" {s.session_id} {preview}")
446
+ sessions_data.append({
447
+ "session_id": s.session_id,
448
+ "status": s.status.value,
449
+ "model": s.endpoint.model if s.endpoint else None,
450
+ "created_at": s.created_at if hasattr(s, "created_at") else None,
451
+ "updated_at": s.updated_at if hasattr(s, "updated_at") else None,
452
+ "message_count": len(s.messages),
453
+ "preview": _get_session_preview(s),
454
+ })
455
+ print(json.dumps({"sessions": sessions_data}))
456
+ else:
457
+ if not sessions:
458
+ print("No sessions found.")
459
+ else:
460
+ print("Recent sessions:")
461
+ for s in sessions:
462
+ preview = _get_session_preview(s)
463
+ print(f" {s.session_id} {preview}")
363
464
  return
364
465
 
365
466
  # Emit early event for JSON mode before heavy imports
@@ -367,8 +468,7 @@ def main( # noqa: PLR0913, PLR0915
367
468
  if json_output:
368
469
  print(json.dumps({"type": "initializing"}), flush=True)
369
470
 
370
- import trio
371
- from wafer_core.rollouts import FileSessionStore, Message, Trajectory
471
+ from wafer_core.rollouts import Message, Trajectory
372
472
  from wafer_core.rollouts.frontends import NoneFrontend, RunnerConfig, run_interactive
373
473
 
374
474
  _setup_logging()
@@ -444,7 +544,9 @@ def main( # noqa: PLR0913, PLR0915
444
544
  )
445
545
  else:
446
546
  if json_output:
447
- frontend = StreamingChunkFrontend()
547
+ # Emit session_start if we have a session_id (from --resume)
548
+ model_name = endpoint.model if hasattr(endpoint, 'model') else None
549
+ frontend = StreamingChunkFrontend(session_id=session_id, model=model_name)
448
550
  else:
449
551
  frontend = NoneFrontend(show_tool_calls=True, show_thinking=False)
450
552
  config = RunnerConfig(
@@ -455,6 +557,13 @@ def main( # noqa: PLR0913, PLR0915
455
557
  hide_session_info=True, # We print our own resume command
456
558
  )
457
559
  states = await run_interactive(trajectory, endpoint, frontend, environment, config)
560
+ # Emit session_start for new sessions (if session_id was None and we got one)
561
+ # Check first state to emit as early as possible
562
+ if json_output and isinstance(frontend, StreamingChunkFrontend):
563
+ first_session_id = states[0].session_id if states and states[0].session_id else None
564
+ if first_session_id and not session_id: # New session created
565
+ model_name = endpoint.model if hasattr(endpoint, 'model') else None
566
+ frontend.emit_session_start(first_session_id, model_name)
458
567
  # Print resume command with full wafer agent prefix
459
568
  if states and states[-1].session_id:
460
569
  print(f"\nResume with: wafer agent --resume {states[-1].session_id}")