fluxloop-cli 0.2.2__tar.gz → 0.2.4__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.

Potentially problematic release.


This version of fluxloop-cli might be problematic. Click here for more details.

Files changed (36) hide show
  1. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/PKG-INFO +13 -1
  2. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/README.md +12 -0
  3. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/__init__.py +1 -1
  4. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/config_loader.py +27 -3
  5. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/runner.py +40 -3
  6. fluxloop_cli-0.2.4/fluxloop_cli/target_loader.py +135 -0
  7. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/validators.py +10 -0
  8. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli.egg-info/PKG-INFO +13 -1
  9. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/pyproject.toml +1 -1
  10. fluxloop_cli-0.2.2/fluxloop_cli/target_loader.py +0 -95
  11. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/arg_binder.py +0 -0
  12. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/commands/__init__.py +0 -0
  13. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/commands/config.py +0 -0
  14. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/commands/generate.py +0 -0
  15. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/commands/init.py +0 -0
  16. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/commands/parse.py +0 -0
  17. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/commands/record.py +0 -0
  18. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/commands/run.py +0 -0
  19. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/commands/status.py +0 -0
  20. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/config_schema.py +0 -0
  21. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/constants.py +0 -0
  22. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/input_generator.py +0 -0
  23. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/llm_generator.py +0 -0
  24. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/main.py +0 -0
  25. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/project_paths.py +0 -0
  26. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli/templates.py +0 -0
  27. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli.egg-info/SOURCES.txt +0 -0
  28. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
  29. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli.egg-info/entry_points.txt +0 -0
  30. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli.egg-info/requires.txt +0 -0
  31. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/fluxloop_cli.egg-info/top_level.txt +0 -0
  32. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/setup.cfg +0 -0
  33. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/tests/test_arg_binder.py +0 -0
  34. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/tests/test_config_command.py +0 -0
  35. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/tests/test_input_generator.py +0 -0
  36. {fluxloop_cli-0.2.2 → fluxloop_cli-0.2.4}/tests/test_target_loader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -71,6 +71,18 @@ The legacy `setting.yaml` is still supported, but new projects created with
71
71
 
72
72
  Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
73
73
 
74
+ ## Runner Integration Patterns
75
+
76
+ Configure how FluxLoop calls your code in `configs/simulation.yaml`:
77
+
78
+ - Module + function: `module_path`/`function_name` or `target: "module:function"`
79
+ - Class.method (zero-arg ctor): `target: "module:Class.method"`
80
+ - Module-scoped instance method: `target: "module:instance.method"`
81
+ - Class.method with factory: add `factory: "module:make_instance"` (+ `factory_kwargs`)
82
+ - Async generators: set `runner.stream_output_path` if your streamed event shape differs (default `message.delta`).
83
+
84
+ See full examples: `packages/website/docs-cli/configuration/runner-targets.md`.
85
+
74
86
  ## Developing
75
87
 
76
88
  Install dependencies and run tests:
@@ -31,6 +31,18 @@ The legacy `setting.yaml` is still supported, but new projects created with
31
31
 
32
32
  Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
33
33
 
34
+ ## Runner Integration Patterns
35
+
36
+ Configure how FluxLoop calls your code in `configs/simulation.yaml`:
37
+
38
+ - Module + function: `module_path`/`function_name` or `target: "module:function"`
39
+ - Class.method (zero-arg ctor): `target: "module:Class.method"`
40
+ - Module-scoped instance method: `target: "module:instance.method"`
41
+ - Class.method with factory: add `factory: "module:make_instance"` (+ `factory_kwargs`)
42
+ - Async generators: set `runner.stream_output_path` if your streamed event shape differs (default `message.delta`).
43
+
44
+ See full examples: `packages/website/docs-cli/configuration/runner-targets.md`.
45
+
34
46
  ## Developing
35
47
 
36
48
  Install dependencies and run tests:
@@ -2,7 +2,7 @@
2
2
  FluxLoop CLI - Command-line interface for running agent simulations.
3
3
  """
4
4
 
5
- __version__ = "0.2.2"
5
+ __version__ = "0.2.4"
6
6
 
7
7
  from .main import app
8
8
 
@@ -253,11 +253,35 @@ def _normalize_variation_strategies(payload: Dict[str, Any]) -> None:
253
253
  if not candidate:
254
254
  continue
255
255
 
256
- normalized.append(
257
- candidate.strip().lower().replace(" ", "_").replace("-", "_")
256
+ canonical = (
257
+ candidate.strip()
258
+ .lower()
259
+ .replace(" ", "_")
260
+ .replace("-", "_")
258
261
  )
259
262
 
260
- payload["variation_strategies"] = normalized
263
+ alias_map = {
264
+ "error_prone": (
265
+ VariationStrategy.ERROR_PRONE.value
266
+ if hasattr(VariationStrategy, "ERROR_PRONE")
267
+ else VariationStrategy.TYPO.value
268
+ ),
269
+ }
270
+
271
+ canonical = alias_map.get(canonical, canonical)
272
+
273
+ normalized.append(canonical)
274
+
275
+ # Remove duplicates while preserving order
276
+ seen = set()
277
+ deduped = []
278
+ for item in normalized:
279
+ if item in seen:
280
+ continue
281
+ seen.add(item)
282
+ deduped.append(item)
283
+
284
+ payload["variation_strategies"] = deduped
261
285
 
262
286
 
263
287
  def _detect_config_context(resolved_path: Path) -> tuple[str, Path, Path]:
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  import asyncio
8
8
  import json
9
+ import inspect
9
10
  import time
10
11
  from datetime import datetime
11
12
  from pathlib import Path
@@ -397,11 +398,20 @@ class ExperimentRunner:
397
398
  if callable(error_cb) and hasattr(error_cb, "errors"):
398
399
  callback_store["error"] = error_cb.errors
399
400
 
401
+ if inspect.isasyncgenfunction(agent_func):
402
+ return await self._consume_async_gen(agent_func, kwargs)
403
+
400
404
  if asyncio.iscoroutinefunction(agent_func):
401
- return await agent_func(**kwargs)
405
+ result = await agent_func(**kwargs)
406
+ else:
407
+ loop = asyncio.get_event_loop()
408
+ result = await loop.run_in_executor(None, lambda: agent_func(**kwargs))
402
409
 
403
- loop = asyncio.get_event_loop()
404
- return await loop.run_in_executor(None, lambda: agent_func(**kwargs))
410
+ # If an async generator/iterable is returned, consume it into a string
411
+ if inspect.isasyncgen(result) or hasattr(result, "__aiter__"):
412
+ return await self._consume_async_iterable(result)
413
+
414
+ return result
405
415
 
406
416
  async def _wait_for_callbacks(
407
417
  self,
@@ -464,6 +474,33 @@ class ExperimentRunner:
464
474
 
465
475
  return None
466
476
 
477
+ async def _consume_async_gen(self, func: Callable, kwargs: Dict[str, Any]) -> Any:
478
+ """Consume an async generator function by joining text chunks resolved from events."""
479
+ gen = func(**kwargs)
480
+ return await self._consume_async_iterable(gen)
481
+
482
+ async def _consume_async_iterable(self, agen: Any) -> Any:
483
+ """Consume async iterable items, extracting text via runner.stream_output_path."""
484
+ path = (getattr(self.config.runner, "stream_output_path", None) or "message.delta").split(".")
485
+ chunks: List[str] = []
486
+ async for item in agen:
487
+ val = self._get_by_path(item, path)
488
+ if isinstance(val, str) and val:
489
+ chunks.append(val)
490
+ return "".join(chunks) if chunks else None
491
+
492
+ @staticmethod
493
+ def _get_by_path(obj: Any, parts: List[str]) -> Any:
494
+ cur: Any = obj
495
+ for key in parts:
496
+ if cur is None:
497
+ return None
498
+ if isinstance(cur, dict):
499
+ cur = cur.get(key)
500
+ else:
501
+ cur = getattr(cur, key, None)
502
+ return cur
503
+
467
504
  @staticmethod
468
505
  def _extract_payload(args: Sequence[Any], kwargs: Dict[str, Any]) -> Any:
469
506
  if kwargs:
@@ -0,0 +1,135 @@
1
+ """Utilities for dynamically loading experiment targets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Optional
9
+
10
+ from fluxloop.schemas import RunnerConfig
11
+
12
+
13
+ class TargetLoader:
14
+ """Load callables defined by an experiment runner configuration."""
15
+
16
+ def __init__(self, config: RunnerConfig, source_dir: Optional[Path] = None) -> None:
17
+ self.config = config
18
+ self.source_dir = source_dir
19
+
20
+ def load(self) -> Callable:
21
+ """Return a callable based on the configured target."""
22
+
23
+ work_dir = self._resolve_working_directory()
24
+ remove_path = False
25
+
26
+ if work_dir and work_dir not in sys.path:
27
+ sys.path.insert(0, work_dir)
28
+ remove_path = True
29
+
30
+ try:
31
+ if self.config.target:
32
+ return self._load_from_target(self.config.target)
33
+
34
+ module = importlib.import_module(self.config.module_path)
35
+ return getattr(module, self.config.function_name)
36
+ finally:
37
+ if remove_path:
38
+ sys.path.remove(work_dir)
39
+
40
+ def _resolve_working_directory(self) -> str | None:
41
+ if not self.config.working_directory:
42
+ return None
43
+
44
+ raw_path = Path(self.config.working_directory)
45
+ if not raw_path.is_absolute() and self.source_dir:
46
+ raw_path = (self.source_dir / raw_path).resolve()
47
+ else:
48
+ raw_path = raw_path.expanduser().resolve()
49
+
50
+ path = raw_path
51
+ return str(path)
52
+
53
+ def _load_from_target(self, target: str) -> Callable:
54
+ """Resolve a callable from target string.
55
+
56
+ Supports:
57
+ - module:function
58
+ - module:variable
59
+ - module:Class.method
60
+ - module:variable.method
61
+ If the first attribute is a class, attempts to construct an instance via:
62
+ - runner.factory if provided (module:callable)
63
+ - zero-argument constructor fallback
64
+ """
65
+
66
+ if ":" not in target:
67
+ raise ValueError(
68
+ "Invalid runner.target format. Expected 'module:symbol[.attr]'."
69
+ )
70
+
71
+ module_name, attribute_chain = target.split(":", 1)
72
+
73
+ try:
74
+ module = importlib.import_module(module_name)
75
+ except ImportError as exc:
76
+ raise ValueError(
77
+ f"Failed to import module '{module_name}' for target '{target}': {exc}"
78
+ )
79
+
80
+ parts = attribute_chain.split(".") if attribute_chain else []
81
+ if not parts:
82
+ raise ValueError(f"Invalid target '{target}': missing attribute path")
83
+
84
+ # Resolve first symbol from module
85
+ try:
86
+ obj: Any = getattr(module, parts[0])
87
+ except AttributeError as exc:
88
+ raise ValueError(
89
+ f"Symbol '{parts[0]}' not found in module '{module_name}' for target '{target}'."
90
+ ) from exc
91
+
92
+ # If it's a class, construct instance
93
+ if isinstance(obj, type):
94
+ factory = getattr(self.config, "factory", None)
95
+ factory_kwargs = getattr(self.config, "factory_kwargs", {}) or {}
96
+ if factory:
97
+ if ":" not in factory:
98
+ raise ValueError("runner.factory must be in 'module:callable' format")
99
+ fmod, fname = factory.split(":", 1)
100
+ try:
101
+ fac_mod = importlib.import_module(fmod)
102
+ fac = getattr(fac_mod, fname)
103
+ except Exception as exc:
104
+ raise ValueError(
105
+ f"Failed to import factory '{factory}' for target '{target}': {exc}"
106
+ ) from exc
107
+ try:
108
+ obj = fac(**factory_kwargs)
109
+ except Exception as exc:
110
+ raise ValueError(
111
+ f"Factory '{factory}' failed to construct instance: {exc}"
112
+ ) from exc
113
+ else:
114
+ try:
115
+ obj = obj()
116
+ except TypeError as exc:
117
+ raise ValueError(
118
+ "Cannot construct class without zero-argument constructor. "
119
+ "Provide runner.factory to construct the instance."
120
+ ) from exc
121
+
122
+ # Traverse remaining attributes
123
+ for attr in parts[1:]:
124
+ try:
125
+ obj = getattr(obj, attr)
126
+ except AttributeError as exc:
127
+ raise ValueError(
128
+ f"Attribute '{attr}' not found while resolving target '{target}'."
129
+ ) from exc
130
+
131
+ if not callable(obj):
132
+ raise ValueError(f"Resolved target '{target}' is not callable.")
133
+
134
+ return obj
135
+
@@ -13,11 +13,21 @@ def parse_variation_strategies(values: Iterable[str]) -> List[VariationStrategy]
13
13
  """Parse CLI-provided variation strategy names."""
14
14
 
15
15
  strategies: List[VariationStrategy] = []
16
+ alias_map = {
17
+ "error_prone": (
18
+ VariationStrategy.ERROR_PRONE.value
19
+ if hasattr(VariationStrategy, "ERROR_PRONE")
20
+ else VariationStrategy.TYPO.value
21
+ )
22
+ }
23
+
16
24
  for value in values:
17
25
  normalized = value.strip().lower().replace("-", "_")
18
26
  if not normalized:
19
27
  continue
20
28
 
29
+ normalized = alias_map.get(normalized, normalized)
30
+
21
31
  try:
22
32
  strategies.append(VariationStrategy(normalized))
23
33
  except ValueError as exc: # pragma: no cover - exercised via Typer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -71,6 +71,18 @@ The legacy `setting.yaml` is still supported, but new projects created with
71
71
 
72
72
  Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
73
73
 
74
+ ## Runner Integration Patterns
75
+
76
+ Configure how FluxLoop calls your code in `configs/simulation.yaml`:
77
+
78
+ - Module + function: `module_path`/`function_name` or `target: "module:function"`
79
+ - Class.method (zero-arg ctor): `target: "module:Class.method"`
80
+ - Module-scoped instance method: `target: "module:instance.method"`
81
+ - Class.method with factory: add `factory: "module:make_instance"` (+ `factory_kwargs`)
82
+ - Async generators: set `runner.stream_output_path` if your streamed event shape differs (default `message.delta`).
83
+
84
+ See full examples: `packages/website/docs-cli/configuration/runner-targets.md`.
85
+
74
86
  ## Developing
75
87
 
76
88
  Install dependencies and run tests:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fluxloop-cli"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "FluxLoop CLI for running agent simulations"
9
9
  authors = [
10
10
  {name = "FluxLoop Team", email = "team@fluxloop.dev"}
@@ -1,95 +0,0 @@
1
- """Utilities for dynamically loading experiment targets."""
2
-
3
- from __future__ import annotations
4
-
5
- import importlib
6
- import sys
7
- from pathlib import Path
8
- from typing import Callable, Optional
9
-
10
- from fluxloop.schemas import RunnerConfig
11
-
12
-
13
- class TargetLoader:
14
- """Load callables defined by an experiment runner configuration."""
15
-
16
- def __init__(self, config: RunnerConfig, source_dir: Optional[Path] = None) -> None:
17
- self.config = config
18
- self.source_dir = source_dir
19
-
20
- def load(self) -> Callable:
21
- """Return a callable based on the configured target."""
22
-
23
- work_dir = self._resolve_working_directory()
24
- remove_path = False
25
-
26
- if work_dir and work_dir not in sys.path:
27
- sys.path.insert(0, work_dir)
28
- remove_path = True
29
-
30
- try:
31
- if self.config.target:
32
- return self._load_from_target(self.config.target)
33
-
34
- module = importlib.import_module(self.config.module_path)
35
- return getattr(module, self.config.function_name)
36
- finally:
37
- if remove_path:
38
- sys.path.remove(work_dir)
39
-
40
- def _resolve_working_directory(self) -> str | None:
41
- if not self.config.working_directory:
42
- return None
43
-
44
- raw_path = Path(self.config.working_directory)
45
- if not raw_path.is_absolute() and self.source_dir:
46
- raw_path = (self.source_dir / raw_path).resolve()
47
- else:
48
- raw_path = raw_path.expanduser().resolve()
49
-
50
- path = raw_path
51
- return str(path)
52
-
53
- def _load_from_target(self, target: str) -> Callable:
54
- if ":" not in target:
55
- raise ValueError(
56
- "Invalid runner.target format. Expected 'module:function' or 'module:Class.method'."
57
- )
58
-
59
- module_name, attribute_part = target.split(":", 1)
60
-
61
- try:
62
- module = importlib.import_module(module_name)
63
- except ImportError as exc:
64
- raise ValueError(f"Failed to import module '{module_name}' for target '{target}': {exc}")
65
-
66
- if "." in attribute_part:
67
- class_name, method_name = attribute_part.split(".", 1)
68
- try:
69
- cls = getattr(module, class_name)
70
- except AttributeError as exc:
71
- raise ValueError(
72
- f"Class '{class_name}' not found in module '{module_name}' for target '{target}'."
73
- ) from exc
74
-
75
- try:
76
- instance = cls()
77
- except TypeError as exc:
78
- raise ValueError(
79
- "MVP limitation: only classes with zero-argument constructors are supported."
80
- ) from exc
81
-
82
- try:
83
- return getattr(instance, method_name)
84
- except AttributeError as exc:
85
- raise ValueError(
86
- f"Method '{method_name}' not found on class '{class_name}' for target '{target}'."
87
- ) from exc
88
-
89
- try:
90
- return getattr(module, attribute_part)
91
- except AttributeError as exc:
92
- raise ValueError(
93
- f"Function or attribute '{attribute_part}' not found in module '{module_name}' for target '{target}'."
94
- ) from exc
95
-
File without changes