tactus 0.34.1__py3-none-any.whl → 0.35.0__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.
Files changed (81) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/broker_log.py +17 -14
  3. tactus/adapters/channels/__init__.py +17 -15
  4. tactus/adapters/channels/base.py +16 -7
  5. tactus/adapters/channels/broker.py +43 -13
  6. tactus/adapters/channels/cli.py +19 -15
  7. tactus/adapters/channels/host.py +15 -6
  8. tactus/adapters/channels/ipc.py +82 -31
  9. tactus/adapters/channels/sse.py +41 -23
  10. tactus/adapters/cli_hitl.py +19 -19
  11. tactus/adapters/cli_log.py +4 -4
  12. tactus/adapters/control_loop.py +138 -99
  13. tactus/adapters/cost_collector_log.py +9 -9
  14. tactus/adapters/file_storage.py +56 -52
  15. tactus/adapters/http_callback_log.py +23 -13
  16. tactus/adapters/ide_log.py +17 -9
  17. tactus/adapters/lua_tools.py +4 -5
  18. tactus/adapters/mcp.py +16 -19
  19. tactus/adapters/mcp_manager.py +46 -30
  20. tactus/adapters/memory.py +9 -9
  21. tactus/adapters/plugins.py +42 -42
  22. tactus/broker/client.py +75 -78
  23. tactus/broker/protocol.py +57 -57
  24. tactus/broker/server.py +252 -197
  25. tactus/cli/app.py +3 -1
  26. tactus/cli/control.py +2 -2
  27. tactus/core/config_manager.py +181 -135
  28. tactus/core/dependencies/registry.py +66 -48
  29. tactus/core/dsl_stubs.py +222 -163
  30. tactus/core/exceptions.py +10 -1
  31. tactus/core/execution_context.py +152 -112
  32. tactus/core/lua_sandbox.py +72 -64
  33. tactus/core/message_history_manager.py +138 -43
  34. tactus/core/mocking.py +41 -27
  35. tactus/core/output_validator.py +49 -44
  36. tactus/core/registry.py +94 -80
  37. tactus/core/runtime.py +211 -176
  38. tactus/core/template_resolver.py +16 -16
  39. tactus/core/yaml_parser.py +55 -45
  40. tactus/docs/extractor.py +7 -6
  41. tactus/ide/server.py +119 -78
  42. tactus/primitives/control.py +10 -6
  43. tactus/primitives/file.py +48 -46
  44. tactus/primitives/handles.py +47 -35
  45. tactus/primitives/host.py +29 -27
  46. tactus/primitives/human.py +154 -137
  47. tactus/primitives/json.py +22 -23
  48. tactus/primitives/log.py +26 -26
  49. tactus/primitives/message_history.py +285 -31
  50. tactus/primitives/model.py +15 -9
  51. tactus/primitives/procedure.py +86 -64
  52. tactus/primitives/procedure_callable.py +58 -51
  53. tactus/primitives/retry.py +31 -29
  54. tactus/primitives/session.py +42 -29
  55. tactus/primitives/state.py +54 -43
  56. tactus/primitives/step.py +9 -13
  57. tactus/primitives/system.py +34 -21
  58. tactus/primitives/tool.py +44 -31
  59. tactus/primitives/tool_handle.py +76 -54
  60. tactus/primitives/toolset.py +25 -22
  61. tactus/sandbox/config.py +4 -4
  62. tactus/sandbox/container_runner.py +161 -107
  63. tactus/sandbox/docker_manager.py +20 -20
  64. tactus/sandbox/entrypoint.py +16 -14
  65. tactus/sandbox/protocol.py +15 -15
  66. tactus/stdlib/classify/llm.py +1 -3
  67. tactus/stdlib/core/validation.py +0 -3
  68. tactus/testing/pydantic_eval_runner.py +1 -1
  69. tactus/utils/asyncio_helpers.py +27 -0
  70. tactus/utils/cost_calculator.py +7 -7
  71. tactus/utils/model_pricing.py +11 -12
  72. tactus/utils/safe_file_library.py +156 -132
  73. tactus/utils/safe_libraries.py +27 -27
  74. tactus/validation/error_listener.py +18 -5
  75. tactus/validation/semantic_visitor.py +392 -333
  76. tactus/validation/validator.py +89 -49
  77. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
  78. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
  79. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.1.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
@@ -105,10 +105,10 @@ class ContainerRunner:
105
105
  """
106
106
  self.config = config
107
107
 
108
- # Parse image name and tag from config.image (e.g., "tactus-sandbox:local")
109
- image_parts = config.image.split(":")
110
- image_name = image_parts[0] if len(image_parts) > 0 else "tactus-sandbox"
111
- image_tag = image_parts[1] if len(image_parts) > 1 else "local"
108
+ # Parse image name and tag from config.image (e.g., "tactus-sandbox:local").
109
+ image_name_parts = config.image.split(":")
110
+ image_name = image_name_parts[0] if len(image_name_parts) > 0 else "tactus-sandbox"
111
+ image_tag = image_name_parts[1] if len(image_name_parts) > 1 else "local"
112
112
 
113
113
  self.docker_manager = DockerManager(
114
114
  image_name=image_name,
@@ -138,8 +138,9 @@ class ContainerRunner:
138
138
  return
139
139
 
140
140
  # Check if auto-rebuild is disabled
141
- auto_rebuild = os.environ.get("TACTUS_AUTO_REBUILD_SANDBOX", "true").lower()
142
- if auto_rebuild not in ("true", "1", "yes"):
141
+ auto_rebuild_env_value = os.environ.get("TACTUS_AUTO_REBUILD_SANDBOX", "true").lower()
142
+ auto_rebuild_enabled = auto_rebuild_env_value in ("true", "1", "yes")
143
+ if not auto_rebuild_enabled:
143
144
  logger.debug("Auto-rebuild disabled via TACTUS_AUTO_REBUILD_SANDBOX")
144
145
  return
145
146
 
@@ -242,7 +243,7 @@ class ContainerRunner:
242
243
  else f"tactus-sandbox-{uuid.uuid4().hex[:8]}"
243
244
  )
244
245
 
245
- cmd = [
246
+ docker_command = [
246
247
  "docker",
247
248
  "run",
248
249
  "--rm", # Remove container after exit
@@ -251,27 +252,27 @@ class ContainerRunner:
251
252
  container_name,
252
253
  ]
253
254
 
254
- cmd.extend(["--network", self.config.network])
255
+ docker_command.extend(["--network", self.config.network])
255
256
 
256
257
  # Resource limits
257
258
  if self.config.limits.memory:
258
- cmd.extend(["--memory", self.config.limits.memory])
259
+ docker_command.extend(["--memory", self.config.limits.memory])
259
260
  if self.config.limits.cpus:
260
- cmd.extend(["--cpus", self.config.limits.cpus])
261
+ docker_command.extend(["--cpus", self.config.limits.cpus])
261
262
 
262
263
  # Working directory mount is handled by SandboxConfig.add_default_volumes()
263
264
  # which adds ".:/workspace:rw" to config.volumes (unless mount_current_dir=False)
264
265
 
265
266
  # Mount MCP servers if available
266
267
  if mcp_servers_path and mcp_servers_path.exists():
267
- cmd.extend(["-v", f"{mcp_servers_path}:/mcp-servers:ro"])
268
+ docker_command.extend(["-v", f"{mcp_servers_path}:/mcp-servers:ro"])
268
269
 
269
270
  # Development mode: mount live Tactus source code
270
271
  if self.config.dev_mode:
271
272
  tactus_src_dir = self._find_tactus_source_dir()
272
273
  if tactus_src_dir:
273
- logger.info(f"[DEV MODE] Mounting live Tactus source from: {tactus_src_dir}")
274
- cmd.extend(["-v", f"{tactus_src_dir}/tactus:/app/tactus:ro"])
274
+ logger.info("[DEV MODE] Mounting live Tactus source from: %s", tactus_src_dir)
275
+ docker_command.extend(["-v", f"{tactus_src_dir}/tactus:/app/tactus:ro"])
275
276
  else:
276
277
  logger.warning(
277
278
  "[DEV MODE] Could not locate Tactus source directory, using baked-in version"
@@ -279,31 +280,36 @@ class ContainerRunner:
279
280
 
280
281
  # Additional user-configured volumes
281
282
  for volume in self.config.volumes:
282
- cmd.extend(["-v", self._normalize_volume_spec(volume, base_dir=volume_base_dir)])
283
+ docker_command.extend(
284
+ ["-v", self._normalize_volume_spec(volume, base_dir=volume_base_dir)]
285
+ )
283
286
 
284
287
  # User-configured additional env vars
285
288
  for key, value in self.config.env.items():
286
289
  if key in self._BLOCKED_CONTAINER_ENV_KEYS:
287
- logger.warning(f"[SANDBOX] Refusing to pass secret env var into container: {key}")
290
+ logger.warning(
291
+ "[SANDBOX] Refusing to pass secret env var into container: %s",
292
+ key,
293
+ )
288
294
  continue
289
- cmd.extend(["--env", f"{key}={value}"])
295
+ docker_command.extend(["--env", f"{key}={value}"])
290
296
 
291
297
  # Optional per-run callback URL for HTTP event streaming (IDE).
292
298
  if callback_url:
293
- cmd.extend(["--env", f"TACTUS_CALLBACK_URL={callback_url}"])
299
+ docker_command.extend(["--env", f"TACTUS_CALLBACK_URL={callback_url}"])
294
300
 
295
301
  # Extra env vars for this run
296
302
  if extra_env:
297
303
  for key, value in extra_env.items():
298
- cmd.extend(["--env", f"{key}={value}"])
304
+ docker_command.extend(["--env", f"{key}={value}"])
299
305
 
300
306
  # Working directory inside container
301
- cmd.extend(["-w", "/workspace"])
307
+ docker_command.extend(["-w", "/workspace"])
302
308
 
303
309
  # Image name
304
- cmd.append(self.config.image)
310
+ docker_command.append(self.config.image)
305
311
 
306
- return cmd
312
+ return docker_command
307
313
 
308
314
  def _normalize_volume_spec(self, volume: str, base_dir: Optional[Path]) -> str:
309
315
  """
@@ -319,27 +325,31 @@ class ContainerRunner:
319
325
  - volume_name:/container[:mode] (left unchanged)
320
326
  """
321
327
  # Basic split: host:container[:mode]
322
- parts = volume.split(":")
323
- if len(parts) < 2:
328
+ volume_parts = volume.split(":")
329
+ if len(volume_parts) < 2:
324
330
  return volume
325
331
 
326
- host = parts[0]
327
- container = parts[1]
328
- mode = parts[2] if len(parts) > 2 else None
332
+ host_path_raw = volume_parts[0]
333
+ container_path = volume_parts[1]
334
+ mount_mode = volume_parts[2] if len(volume_parts) > 2 else None
329
335
 
330
- host_is_path = host.startswith(("/", "./", "../", "~")) or host == "." or host == ".."
336
+ host_is_path = (
337
+ host_path_raw.startswith(("/", "./", "../", "~"))
338
+ or host_path_raw == "."
339
+ or host_path_raw == ".."
340
+ )
331
341
  if not host_is_path:
332
342
  # Named volume (or other special form) - leave unchanged
333
343
  return volume
334
344
 
335
- host_path = Path(host).expanduser()
345
+ host_path = Path(host_path_raw).expanduser()
336
346
  if not host_path.is_absolute():
337
347
  host_path = (base_dir or Path.cwd()) / host_path
338
348
  host_path = host_path.resolve()
339
349
 
340
- if mode:
341
- return f"{host_path}:{container}:{mode}"
342
- return f"{host_path}:{container}"
350
+ if mount_mode:
351
+ return f"{host_path}:{container_path}:{mount_mode}"
352
+ return f"{host_path}:{container_path}"
343
353
 
344
354
  async def run(
345
355
  self,
@@ -374,24 +384,24 @@ class ContainerRunner:
374
384
  """
375
385
  # Ensure sandbox is up to date (auto-rebuild if code changed)
376
386
  # Skip for IDE to avoid blocking UI - IDE has its own rebuild mechanism
377
- skip_rebuild_for_ide = (event_handler is not None) or (callback_url is not None)
378
- self._ensure_sandbox_up_to_date(skip_for_ide=skip_rebuild_for_ide)
387
+ skip_rebuild_for_ide_execution = (event_handler is not None) or (callback_url is not None)
388
+ self._ensure_sandbox_up_to_date(skip_for_ide=skip_rebuild_for_ide_execution)
379
389
 
380
- execution_id = str(uuid.uuid4())[:8]
381
- start_time = time.time()
390
+ execution_identifier = str(uuid.uuid4())[:8]
391
+ start_timestamp = time.time()
382
392
  broker_server = None
383
393
 
384
394
  # Create temporary workspace if not provided
385
- temp_dir = None
395
+ temp_workspace_path = None
386
396
  if working_dir is None:
387
- temp_dir = tempfile.mkdtemp(prefix="tactus-sandbox-")
388
- working_dir = Path(temp_dir)
397
+ temp_workspace_path = tempfile.mkdtemp(prefix="tactus-sandbox-")
398
+ working_dir = Path(temp_workspace_path)
389
399
 
390
400
  # If we have a source file, copy its directory contents
391
401
  if source_file_path:
392
- src_dir = Path(source_file_path).parent
393
- if src_dir.exists():
394
- for item in src_dir.iterdir():
402
+ source_parent_dir = Path(source_file_path).parent
403
+ if source_parent_dir.exists():
404
+ for item in source_parent_dir.iterdir():
395
405
  if item.is_file():
396
406
  shutil.copy2(item, working_dir / item.name)
397
407
  elif item.is_dir() and not item.name.startswith("."):
@@ -459,7 +469,9 @@ class ContainerRunner:
459
469
 
460
470
  scheme = "tls" if broker_transport == "tls" else "tcp"
461
471
  broker_env = {
462
- "TACTUS_BROKER_SOCKET": f"{scheme}://{self.config.broker_host}:{broker_server.bound_port}"
472
+ "TACTUS_BROKER_SOCKET": (
473
+ f"{scheme}://{self.config.broker_host}:" f"{broker_server.bound_port}"
474
+ )
463
475
  }
464
476
  else:
465
477
  raise SandboxError(
@@ -469,19 +481,19 @@ class ContainerRunner:
469
481
  working_dir=working_dir,
470
482
  mcp_servers_path=mcp_path if mcp_path.exists() else None,
471
483
  extra_env=broker_env,
472
- execution_id=execution_id,
484
+ execution_id=execution_identifier,
473
485
  callback_url=callback_url,
474
486
  volume_base_dir=volume_base_dir,
475
487
  )
476
488
 
477
- logger.debug(f"Docker command: {' '.join(docker_cmd)}")
489
+ logger.debug("Docker command: %s", " ".join(docker_cmd))
478
490
 
479
491
  # Create execution request
480
492
  request = ExecutionRequest(
481
493
  source=source,
482
494
  working_dir="/workspace",
483
495
  params=params or {},
484
- execution_id=execution_id,
496
+ execution_id=execution_identifier,
485
497
  run_id=run_id,
486
498
  source_file_path=source_file_path,
487
499
  format=format,
@@ -527,19 +539,19 @@ class ContainerRunner:
527
539
  llm_backend_config=llm_backend_config,
528
540
  )
529
541
 
530
- result.duration_seconds = time.time() - start_time
542
+ result.duration_seconds = time.time() - start_timestamp
531
543
  return result
532
544
 
533
545
  except asyncio.TimeoutError:
534
546
  return ExecutionResult.timeout(
535
- duration_seconds=time.time() - start_time,
547
+ duration_seconds=time.time() - start_timestamp,
536
548
  )
537
549
  except Exception as e:
538
- logger.exception(f"Sandbox execution failed: {e}")
550
+ logger.exception("Sandbox execution failed: %s", e)
539
551
  return ExecutionResult.failure(
540
552
  error=str(e),
541
553
  error_type=type(e).__name__,
542
- duration_seconds=time.time() - start_time,
554
+ duration_seconds=time.time() - start_timestamp,
543
555
  )
544
556
  finally:
545
557
  if broker_server is not None:
@@ -549,11 +561,11 @@ class ContainerRunner:
549
561
  logger.debug("[BROKER] Failed to close broker server", exc_info=True)
550
562
 
551
563
  # Cleanup temp directory
552
- if temp_dir:
564
+ if temp_workspace_path:
553
565
  try:
554
- shutil.rmtree(temp_dir)
555
- except Exception as e:
556
- logger.warning(f"Failed to cleanup temp dir: {e}")
566
+ shutil.rmtree(temp_workspace_path)
567
+ except Exception as e: # pragma: no cover
568
+ logger.warning("Failed to cleanup temp dir: %s", e) # pragma: no cover
557
569
 
558
570
  async def _run_container(
559
571
  self,
@@ -607,80 +619,104 @@ class ContainerRunner:
607
619
  return
608
620
 
609
621
  async def handle_broker_request(
610
- writer: asyncio.StreamWriter, req: dict[str, Any]
622
+ writer: asyncio.StreamWriter, request_payload: dict[str, Any]
611
623
  ) -> None:
612
- req_id = req.get("id") or ""
613
- method = req.get("method")
614
- params = req.get("params") or {}
624
+ request_id = request_payload.get("id") or ""
625
+ request_method = request_payload.get("method")
626
+ request_params = request_payload.get("params") or {}
615
627
 
616
- if not isinstance(req_id, str) or not isinstance(method, str):
628
+ if not isinstance(request_id, str) or not isinstance(request_method, str):
617
629
  await send_event(
618
630
  writer,
619
631
  {
620
- "id": str(req_id) if req_id else "",
632
+ "id": str(request_id) if request_id else "",
621
633
  "event": "error",
622
634
  "error": {"type": "BadRequest", "message": "Missing id/method"},
623
635
  },
624
636
  )
625
637
  return
626
638
 
627
- if method == "events.emit":
628
- event = params.get("event") if isinstance(params, dict) else None
639
+ if request_method == "events.emit":
640
+ event = (
641
+ request_params.get("event") if isinstance(request_params, dict) else None
642
+ )
629
643
  if isinstance(event, dict) and event_handler is not None:
630
644
  try:
631
645
  event_handler(event)
632
646
  except Exception:
633
647
  logger.debug("[BROKER] event_handler raised", exc_info=True)
634
- await send_event(writer, {"id": req_id, "event": "done", "data": {"ok": True}})
648
+ await send_event(
649
+ writer,
650
+ {"id": request_id, "event": "done", "data": {"ok": True}},
651
+ )
635
652
  return
636
653
 
637
- if method == "control.request":
638
- request_data = params.get("request") if isinstance(params, dict) else None
654
+ if request_method == "control.request":
655
+ request_data = (
656
+ request_params.get("request") if isinstance(request_params, dict) else None
657
+ )
639
658
  if control_handler is not None:
640
659
  try:
641
660
  # Send delivered event
642
- await send_event(writer, {"id": req_id, "event": "delivered"})
661
+ await send_event(writer, {"id": request_id, "event": "delivered"})
643
662
 
644
663
  # Call control handler and await response
645
664
  response_data = await control_handler(request_data)
646
665
 
647
666
  # Send response event
648
667
  await send_event(
649
- writer, {"id": req_id, "event": "response", "data": response_data}
668
+ writer,
669
+ {
670
+ "id": request_id,
671
+ "event": "response",
672
+ "data": response_data,
673
+ },
650
674
  )
651
675
  except asyncio.TimeoutError:
652
676
  await send_event(
653
677
  writer,
654
- {"id": req_id, "event": "timeout", "data": {"timed_out": True}},
678
+ {
679
+ "id": request_id,
680
+ "event": "timeout",
681
+ "data": {"timed_out": True},
682
+ },
655
683
  )
656
684
  except Exception as e:
657
685
  logger.debug("[BROKER] control.request handler raised", exc_info=True)
658
686
  await send_event(
659
687
  writer,
660
- {"id": req_id, "event": "error", "error": {"message": str(e)}},
688
+ {
689
+ "id": request_id,
690
+ "event": "error",
691
+ "error": {"message": str(e)},
692
+ },
661
693
  )
662
694
  else:
663
695
  await send_event(
664
696
  writer,
665
697
  {
666
- "id": req_id,
698
+ "id": request_id,
667
699
  "event": "error",
668
700
  "error": {"message": "No control handler configured"},
669
701
  },
670
702
  )
671
703
  return
672
704
 
673
- if method == "tool.call":
674
- name = params.get("name") if isinstance(params, dict) else None
675
- args = params.get("args") if isinstance(params, dict) else None
676
- if args is None:
677
- args = {}
705
+ if request_method == "tool.call":
706
+ tool_name = (
707
+ request_params.get("name") if isinstance(request_params, dict) else None
708
+ )
709
+ tool_args = (
710
+ request_params.get("args") if isinstance(request_params, dict) else None
711
+ )
712
+ if tool_args is None:
713
+ tool_args = {}
678
714
 
679
- if not isinstance(name, str) or not name:
715
+ if not isinstance(tool_name, str) or not tool_name:
680
716
  await send_event(
681
717
  writer,
682
718
  {
683
- "id": req_id,
719
+ "id": request_id,
684
720
  "event": "error",
685
721
  "error": {
686
722
  "type": "BadRequest",
@@ -689,11 +725,11 @@ class ContainerRunner:
689
725
  },
690
726
  )
691
727
  return
692
- if not isinstance(args, dict):
728
+ if not isinstance(tool_args, dict):
693
729
  await send_event(
694
730
  writer,
695
731
  {
696
- "id": req_id,
732
+ "id": request_id,
697
733
  "event": "error",
698
734
  "error": {
699
735
  "type": "BadRequest",
@@ -704,16 +740,16 @@ class ContainerRunner:
704
740
  return
705
741
 
706
742
  try:
707
- result = tool_registry.call(name, args)
743
+ result = tool_registry.call(tool_name, tool_args)
708
744
  except KeyError:
709
745
  await send_event(
710
746
  writer,
711
747
  {
712
- "id": req_id,
748
+ "id": request_id,
713
749
  "event": "error",
714
750
  "error": {
715
751
  "type": "ToolNotAllowed",
716
- "message": f"Tool not allowlisted: {name}",
752
+ "message": f"Tool not allowlisted: {tool_name}",
717
753
  },
718
754
  },
719
755
  )
@@ -723,7 +759,7 @@ class ContainerRunner:
723
759
  await send_event(
724
760
  writer,
725
761
  {
726
- "id": req_id,
762
+ "id": request_id,
727
763
  "event": "error",
728
764
  "error": {"type": type(e).__name__, "message": str(e)},
729
765
  },
@@ -731,32 +767,33 @@ class ContainerRunner:
731
767
  return
732
768
 
733
769
  await send_event(
734
- writer, {"id": req_id, "event": "done", "data": {"result": result}}
770
+ writer,
771
+ {"id": request_id, "event": "done", "data": {"result": result}},
735
772
  )
736
773
  return
737
774
 
738
- if method != "llm.chat":
775
+ if request_method != "llm.chat":
739
776
  await send_event(
740
777
  writer,
741
778
  {
742
- "id": req_id,
779
+ "id": request_id,
743
780
  "event": "error",
744
781
  "error": {
745
782
  "type": "MethodNotFound",
746
- "message": f"Unknown method: {method}",
783
+ "message": f"Unknown method: {request_method}",
747
784
  },
748
785
  },
749
786
  )
750
787
  return
751
788
 
752
789
  provider = (
753
- params.get("provider") if isinstance(params, dict) else None
790
+ request_params.get("provider") if isinstance(request_params, dict) else None
754
791
  ) or "openai"
755
792
  if provider != "openai":
756
793
  await send_event(
757
794
  writer,
758
795
  {
759
- "id": req_id,
796
+ "id": request_id,
760
797
  "event": "error",
761
798
  "error": {
762
799
  "type": "UnsupportedProvider",
@@ -766,17 +803,27 @@ class ContainerRunner:
766
803
  )
767
804
  return
768
805
 
769
- model = params.get("model") if isinstance(params, dict) else None
770
- messages = params.get("messages") if isinstance(params, dict) else None
771
- stream = bool(params.get("stream", False)) if isinstance(params, dict) else False
772
- temperature = params.get("temperature") if isinstance(params, dict) else None
773
- max_tokens = params.get("max_tokens") if isinstance(params, dict) else None
806
+ model = request_params.get("model") if isinstance(request_params, dict) else None
807
+ messages = (
808
+ request_params.get("messages") if isinstance(request_params, dict) else None
809
+ )
810
+ stream = (
811
+ bool(request_params.get("stream", False))
812
+ if isinstance(request_params, dict)
813
+ else False
814
+ )
815
+ temperature = (
816
+ request_params.get("temperature") if isinstance(request_params, dict) else None
817
+ )
818
+ max_tokens = (
819
+ request_params.get("max_tokens") if isinstance(request_params, dict) else None
820
+ )
774
821
 
775
822
  if not isinstance(model, str) or not model:
776
823
  await send_event(
777
824
  writer,
778
825
  {
779
- "id": req_id,
826
+ "id": request_id,
780
827
  "event": "error",
781
828
  "error": {
782
829
  "type": "BadRequest",
@@ -789,7 +836,7 @@ class ContainerRunner:
789
836
  await send_event(
790
837
  writer,
791
838
  {
792
- "id": req_id,
839
+ "id": request_id,
793
840
  "event": "error",
794
841
  "error": {
795
842
  "type": "BadRequest",
@@ -808,7 +855,7 @@ class ContainerRunner:
808
855
  max_tokens=max_tokens,
809
856
  stream=True,
810
857
  )
811
- full_text = ""
858
+ accumulated_text = ""
812
859
  async for chunk in stream_iter:
813
860
  try:
814
861
  delta = chunk.choices[0].delta
@@ -819,18 +866,23 @@ class ContainerRunner:
819
866
  if not text:
820
867
  continue
821
868
 
822
- full_text += text
869
+ accumulated_text += text
823
870
  await send_event(
824
- writer, {"id": req_id, "event": "delta", "data": {"text": text}}
871
+ writer,
872
+ {
873
+ "id": request_id,
874
+ "event": "delta",
875
+ "data": {"text": text},
876
+ },
825
877
  )
826
878
 
827
879
  await send_event(
828
880
  writer,
829
881
  {
830
- "id": req_id,
882
+ "id": request_id,
831
883
  "event": "done",
832
884
  "data": {
833
- "text": full_text,
885
+ "text": accumulated_text,
834
886
  "usage": {
835
887
  "prompt_tokens": 0,
836
888
  "completion_tokens": 0,
@@ -857,7 +909,7 @@ class ContainerRunner:
857
909
  await send_event(
858
910
  writer,
859
911
  {
860
- "id": req_id,
912
+ "id": request_id,
861
913
  "event": "done",
862
914
  "data": {
863
915
  "text": text,
@@ -874,7 +926,7 @@ class ContainerRunner:
874
926
  await send_event(
875
927
  writer,
876
928
  {
877
- "id": req_id,
929
+ "id": request_id,
878
930
  "event": "error",
879
931
  "error": {"type": type(e).__name__, "message": str(e)},
880
932
  },
@@ -885,7 +937,9 @@ class ContainerRunner:
885
937
  async def handle_broker_request(
886
938
  writer: asyncio.StreamWriter, req: dict[str, Any]
887
939
  ) -> None:
888
- raise RuntimeError("Broker requests are not expected in non-stdio transports")
940
+ raise RuntimeError(
941
+ "Broker requests are not expected in non-stdio transports"
942
+ ) # pragma: no cover
889
943
 
890
944
  # Start container process
891
945
  process = await asyncio.create_subprocess_exec(
@@ -894,7 +948,7 @@ class ContainerRunner:
894
948
  stdout=asyncio.subprocess.PIPE,
895
949
  stderr=asyncio.subprocess.PIPE,
896
950
  )
897
- logger.debug(f"[SANDBOX] Spawned container process pid={process.pid}")
951
+ logger.debug("[SANDBOX] Spawned container process pid=%s", process.pid)
898
952
 
899
953
  stdout_task: asyncio.Task[None] | None = None
900
954
  stderr_task: asyncio.Task[None] | None = None
@@ -909,7 +963,7 @@ class ContainerRunner:
909
963
  request_line = (request.to_json() + "\n").encode("utf-8")
910
964
  process.stdin.write(request_line)
911
965
  await process.stdin.drain()
912
- logger.debug(f"[SANDBOX] Sent ExecutionRequest bytes={len(request_line)}")
966
+ logger.debug("[SANDBOX] Sent ExecutionRequest bytes=%s", len(request_line))
913
967
 
914
968
  stdout_bytes = bytearray()
915
969
  result_future: asyncio.Future[ExecutionResult] = (
@@ -968,7 +1022,7 @@ class ContainerRunner:
968
1022
  continue
969
1023
 
970
1024
  if line:
971
- logger.info(f"[container] {line}")
1025
+ logger.info("[container] %s", line)
972
1026
 
973
1027
  stdout_task = asyncio.create_task(stdout_loop())
974
1028
  stderr_task = asyncio.create_task(stderr_loop())
@@ -1075,7 +1129,7 @@ class ContainerRunner:
1075
1129
  except Exception:
1076
1130
  pass
1077
1131
  for task in (stdout_task, stderr_task, wait_task):
1078
- if task is None:
1132
+ if task is None: # pragma: no cover
1079
1133
  continue
1080
1134
  task.cancel()
1081
1135
  try: