wafer-cli 0.2.8__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."""
@@ -253,6 +269,7 @@ def _build_environment(
253
269
  ) -> Environment:
254
270
  """Build a CodingEnvironment from template config."""
255
271
  from wafer_core.environments.coding import CodingEnvironment
272
+ from wafer_core.rollouts.templates import DANGEROUS_BASH_COMMANDS
256
273
 
257
274
  working_dir = Path(corpus_path) if corpus_path else Path.cwd()
258
275
  resolved_tools = tools_override or tpl.tools
@@ -260,6 +277,7 @@ def _build_environment(
260
277
  working_dir=working_dir,
261
278
  enabled_tools=resolved_tools,
262
279
  bash_allowlist=tpl.bash_allowlist,
280
+ bash_denylist=DANGEROUS_BASH_COMMANDS,
263
281
  ) # type: ignore[assignment]
264
282
  return env
265
283
 
@@ -333,31 +351,116 @@ def main( # noqa: PLR0913, PLR0915
333
351
  single_turn: bool | None = None, # None = use template default
334
352
  model: str | None = None,
335
353
  resume: str | None = None,
354
+ from_turn: int | None = None,
336
355
  tools: list[str] | None = None,
356
+ allow_spawn: bool = False,
357
+ max_tool_fails: int | None = None,
358
+ max_turns: int | None = None,
337
359
  template: str | None = None,
338
360
  template_args: dict[str, str] | None = None,
339
361
  corpus_path: str | None = None,
340
362
  list_sessions: bool = False,
363
+ get_session: str | None = None,
341
364
  json_output: bool = False,
342
- # Legacy args (ignored)
343
- problem: str | None = None,
344
- reference: str | None = None,
345
- **kwargs: object,
346
365
  ) -> None:
347
366
  """Run wevin agent in-process via rollouts."""
367
+ import trio
368
+ from dataclasses import asdict
369
+
348
370
  from wafer_core.rollouts import FileSessionStore
349
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
+
350
439
  # Handle --list-sessions: show recent sessions and exit
351
440
  if list_sessions:
352
- session_store = FileSessionStore()
353
- sessions = session_store.list_sync(limit=20)
354
- if not sessions:
355
- print("No sessions found.")
356
- else:
357
- 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 = []
358
445
  for s in sessions:
359
- preview = _get_session_preview(s)
360
- 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}")
361
464
  return
362
465
 
363
466
  # Emit early event for JSON mode before heavy imports
@@ -365,8 +468,7 @@ def main( # noqa: PLR0913, PLR0915
365
468
  if json_output:
366
469
  print(json.dumps({"type": "initializing"}), flush=True)
367
470
 
368
- import trio
369
- from wafer_core.rollouts import FileSessionStore, Message, Trajectory
471
+ from wafer_core.rollouts import Message, Trajectory
370
472
  from wafer_core.rollouts.frontends import NoneFrontend, RunnerConfig, run_interactive
371
473
 
372
474
  _setup_logging()
@@ -442,7 +544,9 @@ def main( # noqa: PLR0913, PLR0915
442
544
  )
443
545
  else:
444
546
  if json_output:
445
- 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)
446
550
  else:
447
551
  frontend = NoneFrontend(show_tool_calls=True, show_thinking=False)
448
552
  config = RunnerConfig(
@@ -453,6 +557,13 @@ def main( # noqa: PLR0913, PLR0915
453
557
  hide_session_info=True, # We print our own resume command
454
558
  )
455
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)
456
567
  # Print resume command with full wafer agent prefix
457
568
  if states and states[-1].session_id:
458
569
  print(f"\nResume with: wafer agent --resume {states[-1].session_id}")