fluxloop-cli 0.2.3__py3-none-any.whl → 0.2.4__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.

Potentially problematic release.


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

fluxloop_cli/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
  FluxLoop CLI - Command-line interface for running agent simulations.
3
3
  """
4
4
 
5
- __version__ = "0.2.3"
5
+ __version__ = "0.2.4"
6
6
 
7
7
  from .main import app
8
8
 
fluxloop_cli/runner.py CHANGED
@@ -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:
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import importlib
6
6
  import sys
7
7
  from pathlib import Path
8
- from typing import Callable, Optional
8
+ from typing import Any, Callable, Optional
9
9
 
10
10
  from fluxloop.schemas import RunnerConfig
11
11
 
@@ -51,45 +51,85 @@ class TargetLoader:
51
51
  return str(path)
52
52
 
53
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
+
54
66
  if ":" not in target:
55
67
  raise ValueError(
56
- "Invalid runner.target format. Expected 'module:function' or 'module:Class.method'."
68
+ "Invalid runner.target format. Expected 'module:symbol[.attr]'."
57
69
  )
58
70
 
59
- module_name, attribute_part = target.split(":", 1)
71
+ module_name, attribute_chain = target.split(":", 1)
60
72
 
61
73
  try:
62
74
  module = importlib.import_module(module_name)
63
75
  except ImportError as exc:
64
- raise ValueError(f"Failed to import module '{module_name}' for target '{target}': {exc}")
76
+ raise ValueError(
77
+ f"Failed to import module '{module_name}' for target '{target}': {exc}"
78
+ )
65
79
 
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
80
+ parts = attribute_chain.split(".") if attribute_chain else []
81
+ if not parts:
82
+ raise ValueError(f"Invalid target '{target}': missing attribute path")
74
83
 
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
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
81
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:]:
82
124
  try:
83
- return getattr(instance, method_name)
125
+ obj = getattr(obj, attr)
84
126
  except AttributeError as exc:
85
127
  raise ValueError(
86
- f"Method '{method_name}' not found on class '{class_name}' for target '{target}'."
128
+ f"Attribute '{attr}' not found while resolving target '{target}'."
87
129
  ) from exc
88
130
 
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
131
+ if not callable(obj):
132
+ raise ValueError(f"Resolved target '{target}' is not callable.")
133
+
134
+ return obj
95
135
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.3
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:
@@ -1,4 +1,4 @@
1
- fluxloop_cli/__init__.py,sha256=4xwGwbCKXAH0urdnCLWhCwTBEuntha_81SWQG_voNys,142
1
+ fluxloop_cli/__init__.py,sha256=FHpr2ZbHLVuqou57OtaIPfJQW-8mSfUeIRDZepsNWmk,142
2
2
  fluxloop_cli/arg_binder.py,sha256=oluHrwe1nNVq7alxBhBEoZrLrYop-cRgXgSu59LJcw4,7827
3
3
  fluxloop_cli/config_loader.py,sha256=IoOY39KxDWNjSWyN5a8n89ym7jiUDTmy7Id-o4E0Usk,9450
4
4
  fluxloop_cli/config_schema.py,sha256=JZJRcMFun5hp3vKLAyek7W3NvISyzRzZt0BZAeSU38I,2415
@@ -7,8 +7,8 @@ fluxloop_cli/input_generator.py,sha256=ldlVdPSDfGsP9zO2RALk7QmZjkIvUzTaxDgwOjuPB
7
7
  fluxloop_cli/llm_generator.py,sha256=SosP5DeZuhBLEM6bj7BDp-7mckvVhtNJMEk2ZgV143M,12894
8
8
  fluxloop_cli/main.py,sha256=xJgrKMv6cN6Q1SNz0rbL4owHsN5CSiLkAaAd747WYds,2584
9
9
  fluxloop_cli/project_paths.py,sha256=FoHp-g3aY1nytxGys85Oy3wJ6gmiKU6FVOwkgTtlHNA,4128
10
- fluxloop_cli/runner.py,sha256=Aqd5Gkqn3XqzteAOU4C7-5k3wjoiBVMNBln1fGGNI1o,22545
11
- fluxloop_cli/target_loader.py,sha256=F8fTxf-Jfu8rxK7-3Y_uPPxa6GBkCbQpXPaorQNqF6g,3220
10
+ fluxloop_cli/runner.py,sha256=dzKv0OZiqBoFfO9LMP5rro9lBL3vkvNmlXqh0U-z9vU,24046
11
+ fluxloop_cli/target_loader.py,sha256=TdMBnuD7qkv71C48y5gCaK5sMFyPCoNyOt-JprL0jOI,4734
12
12
  fluxloop_cli/templates.py,sha256=_QJxAq3JnylGryRjFwLVzaPmYMLsIl5eyVBNfkgGOeA,11207
13
13
  fluxloop_cli/validators.py,sha256=_bLXmxUSzVrDtLjqyTba0bDqamRIaOUHhV4xZ7K36Xw,1155
14
14
  fluxloop_cli/commands/__init__.py,sha256=sxJX1mJoOSJnH_iIuCqYT8tjh7_yxlJB702j_B_GPUw,164
@@ -19,8 +19,8 @@ fluxloop_cli/commands/parse.py,sha256=AVPYi59ejFWx4TYyM7JuI69koxDVkIBxy4LBRDMMbF
19
19
  fluxloop_cli/commands/record.py,sha256=56ASu3Np6TX8_F8caMBJArv10ag5M96kJ-sII8df-5Q,4803
20
20
  fluxloop_cli/commands/run.py,sha256=NLkBN2puqDLdHhKhilLriXsZnm7pMDMcoWeBSEXPM-o,9660
21
21
  fluxloop_cli/commands/status.py,sha256=ERZrWoSP3V7dz5A_TEE5b8E0nGwsPggP4nXw4tLOzxE,7841
22
- fluxloop_cli-0.2.3.dist-info/METADATA,sha256=n8OBXteqpnxl72uzEXJTn1gK7YlslqyL1L8yY4wkbSA,3062
23
- fluxloop_cli-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- fluxloop_cli-0.2.3.dist-info/entry_points.txt,sha256=NxOEMku4yLMY5kp_Qcd3JcevfXP6A98FsSf9xHcwkyE,51
25
- fluxloop_cli-0.2.3.dist-info/top_level.txt,sha256=ahLkaxzwhmVU4z-YhkmQVzAbW3-wez9cKnwPiDK7uKM,13
26
- fluxloop_cli-0.2.3.dist-info/RECORD,,
22
+ fluxloop_cli-0.2.4.dist-info/METADATA,sha256=TYbYk8XqV7NQCAfFAgm4HEWFfAKRCYHX5qca8BLi9Kw,3664
23
+ fluxloop_cli-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ fluxloop_cli-0.2.4.dist-info/entry_points.txt,sha256=NxOEMku4yLMY5kp_Qcd3JcevfXP6A98FsSf9xHcwkyE,51
25
+ fluxloop_cli-0.2.4.dist-info/top_level.txt,sha256=ahLkaxzwhmVU4z-YhkmQVzAbW3-wez9cKnwPiDK7uKM,13
26
+ fluxloop_cli-0.2.4.dist-info/RECORD,,