flatagents 4.1.0__tar.gz → 4.2.0__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.
Files changed (56) hide show
  1. {flatagents-4.1.0 → flatagents-4.2.0}/AGENTS.md +1 -1
  2. {flatagents-4.1.0 → flatagents-4.2.0}/PKG-INFO +2 -2
  3. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/__init__.py +1 -1
  4. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/adapters/claude_code.py +6 -1
  5. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/adapters/codex_cli.py +27 -5
  6. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/adapters/pi_agent_bridge.py +6 -2
  7. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/adapters/smolagents.py +3 -1
  8. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/flatagent.d.ts +1 -1
  9. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/flatagent.slim.d.ts +1 -1
  10. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/flatagents-runtime.d.ts +1 -1
  11. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/flatagents-runtime.schema.json +1 -1
  12. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/flatagents-runtime.slim.d.ts +1 -1
  13. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/flatmachine.d.ts +4 -1
  14. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/flatmachine.schema.json +6 -0
  15. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/flatmachine.slim.d.ts +4 -1
  16. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/profile.d.ts +1 -1
  17. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/profile.slim.d.ts +1 -1
  18. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/prompt.d.ts +1 -1
  19. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/prompt.slim.d.ts +1 -1
  20. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/baseagent.py +1 -0
  21. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/flatagent.py +3 -1
  22. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/monitoring.py +22 -11
  23. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/profiles.py +15 -4
  24. flatagents-4.2.0/flatagents/tests/__init__.py +0 -0
  25. flatagents-4.2.0/flatagents/tests/test_monitoring.py +189 -0
  26. flatagents-4.2.0/flatagents/tests/test_profiles.py +25 -0
  27. {flatagents-4.1.0 → flatagents-4.2.0}/pyproject.toml +2 -2
  28. {flatagents-4.1.0 → flatagents-4.2.0}/.gitignore +0 -0
  29. {flatagents-4.1.0 → flatagents-4.2.0}/README.md +0 -0
  30. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/adapters/__init__.py +0 -0
  31. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/adapters/call_throttle.py +0 -0
  32. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/adapters/claude_code_sessions.py +0 -0
  33. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/adapters/codex_cli_sessions.py +0 -0
  34. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/adapters/compat.py +0 -0
  35. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/adapters/pi_agent_runner.mjs +0 -0
  36. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/README.md +0 -0
  37. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/__init__.py +0 -0
  38. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/flatagent.schema.json +0 -0
  39. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/profile.schema.json +0 -0
  40. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/assets/prompt.schema.json +0 -0
  41. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/__init__.py +0 -0
  42. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/anthropic.py +0 -0
  43. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/cerebras.py +0 -0
  44. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/github_copilot_auth.py +0 -0
  45. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/github_copilot_client.py +0 -0
  46. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/github_copilot_login.py +0 -0
  47. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/github_copilot_types.py +0 -0
  48. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/openai.py +0 -0
  49. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/openai_codex_auth.py +0 -0
  50. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/openai_codex_client.py +0 -0
  51. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/openai_codex_login.py +0 -0
  52. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/providers/openai_codex_types.py +0 -0
  53. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/tool_loop.py +0 -0
  54. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/tools.py +0 -0
  55. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/utils.py +0 -0
  56. {flatagents-4.1.0 → flatagents-4.2.0}/flatagents/validation.py +0 -0
@@ -22,7 +22,7 @@
22
22
  ```yaml
23
23
  # profiles.yml — agents reference by name
24
24
  spec: flatprofile
25
- spec_version: "4.1.0"
25
+ spec_version: "4.2.0"
26
26
  data:
27
27
  model_profiles:
28
28
  fast: { provider: cerebras, name: zai-glm-4.6, temperature: 0.6 }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flatagents
3
- Version: 4.1.0
3
+ Version: 4.2.0
4
4
  Summary: A lightweight framework for building LLM-powered agents.
5
5
  Project-URL: Homepage, https://github.com/memgrafter/flatagents
6
6
  Project-URL: Repository, https://github.com/memgrafter/flatagents
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
- Requires-Python: >=3.10
20
+ Requires-Python: >=3.11
21
21
  Requires-Dist: aiofiles
22
22
  Requires-Dist: httpx
23
23
  Requires-Dist: jinja2
@@ -1,4 +1,4 @@
1
- __version__ = "4.1.0"
1
+ __version__ = "4.2.0"
2
2
 
3
3
  from .baseagent import (
4
4
  # Base agent (abstract, for multi-step agents)
@@ -838,7 +838,12 @@ class ClaudeCodeExecutor:
838
838
  # MCP server configuration
839
839
  mcp_config = cfg.get("mcp_config")
840
840
  if mcp_config:
841
- args += ["--mcp-config", str(mcp_config)]
841
+ mcp_config_path = os.path.expanduser(str(mcp_config))
842
+ if not os.path.isabs(mcp_config_path):
843
+ mcp_config_path = os.path.join(self._config_dir, mcp_config_path)
844
+ if not os.path.isfile(mcp_config_path):
845
+ raise FileNotFoundError(f"Claude MCP config file not found: {mcp_config_path}")
846
+ args += ["--mcp-config", mcp_config_path]
842
847
 
843
848
  # Budget (0 = disabled)
844
849
  max_budget = cfg.get("max_budget_usd", 0)
@@ -40,6 +40,17 @@ Config keys (agent config or global settings.agent_runners.codex_cli):
40
40
  rate_limit_jitter +/-seconds jitter (default: 0)
41
41
  use_app_server bool -- use app-server transport (required for fork)
42
42
  session_source App-server session source tag (default: "exec")
43
+
44
+ Session semantics:
45
+ - FlatMachine ``state.session_id`` is accepted by execute() for executor
46
+ protocol compatibility only. It is a machine-level cache/session key and
47
+ is NOT a Codex thread id.
48
+ - To resume an existing Codex CLI conversation/thread, pass
49
+ ``input.resume_session``. Exec transport maps it to
50
+ ``codex exec resume <SESSION_ID>``; app-server transport maps it to
51
+ ``thread/resume``.
52
+ - Do not map arbitrary ``state.session_id`` values to Codex resume; Codex
53
+ requires the real thread id returned in ``output.thread_id``.
43
54
  """
44
55
 
45
56
  from __future__ import annotations
@@ -384,8 +395,9 @@ class CodexCliExecutor:
384
395
  """Execute a Codex CLI invocation.
385
396
 
386
397
  `session_id` is accepted for AgentExecutor protocol compatibility.
387
- The Codex CLI adapter uses explicit `input.resume_session` to resume
388
- existing Codex threads; `session_id` is intentionally ignored.
398
+ It is intentionally ignored because FlatMachine state session ids are
399
+ not Codex thread ids. To resume a Codex CLI thread, pass the real
400
+ Codex thread id as `input.resume_session`.
389
401
  """
390
402
  task = input_data.get("task") or input_data.get("prompt", "")
391
403
  if not task:
@@ -641,7 +653,12 @@ class CodexCliExecutor:
641
653
  # Output schema
642
654
  output_schema = cfg.get("output_schema")
643
655
  if output_schema:
644
- args += ["--output-schema", str(output_schema)]
656
+ output_schema_path = os.path.expanduser(str(output_schema))
657
+ if not os.path.isabs(output_schema_path):
658
+ output_schema_path = os.path.join(self._config_dir, output_schema_path)
659
+ if not os.path.isfile(output_schema_path):
660
+ raise FileNotFoundError(f"Codex output schema file not found: {output_schema_path}")
661
+ args += ["--output-schema", output_schema_path]
645
662
 
646
663
  # Additional directories
647
664
  add_dirs = cfg.get("add_dirs")
@@ -862,8 +879,13 @@ class CodexCliExecutor:
862
879
  # Load output schema if configured
863
880
  output_schema = None
864
881
  schema_path = self._merged.get("output_schema")
865
- if schema_path and os.path.isfile(str(schema_path)):
866
- with open(str(schema_path)) as f:
882
+ if schema_path:
883
+ schema_path = os.path.expanduser(str(schema_path))
884
+ if not os.path.isabs(schema_path):
885
+ schema_path = os.path.join(self._config_dir, schema_path)
886
+ if not os.path.isfile(schema_path):
887
+ raise FileNotFoundError(f"Codex output schema file not found: {schema_path}")
888
+ with open(schema_path) as f:
867
889
  output_schema = json.load(f)
868
890
 
869
891
  await transport.turn_start(thread_id, text, output_schema)
@@ -103,8 +103,12 @@ def create_pi_agent_bridge_executor(
103
103
  runner_path = config.get("runner") or settings.get("runner")
104
104
  if not runner_path:
105
105
  runner_path = os.path.join(os.path.dirname(__file__), "pi_agent_runner.mjs")
106
- elif not os.path.isabs(runner_path):
107
- runner_path = os.path.join(config_dir, runner_path)
106
+ else:
107
+ runner_path = os.path.expanduser(runner_path)
108
+ if not os.path.isabs(runner_path):
109
+ runner_path = os.path.join(config_dir, runner_path)
110
+ if not os.path.isfile(runner_path):
111
+ raise FileNotFoundError(f"pi-agent runner file not found: {runner_path}")
108
112
 
109
113
  node_path = config.get("node") or settings.get("node") or "node"
110
114
  timeout = config.get("timeout") or settings.get("timeout")
@@ -105,9 +105,11 @@ def _load_factory(ref: str, config_dir: str):
105
105
  module_ref, factory_name = _parse_ref(ref)
106
106
 
107
107
  if module_ref.endswith(".py") or module_ref.startswith(".") or "/" in module_ref:
108
- module_path = module_ref
108
+ module_path = os.path.expanduser(module_ref)
109
109
  if not os.path.isabs(module_path):
110
110
  module_path = os.path.join(config_dir, module_path)
111
+ if not os.path.isfile(module_path):
112
+ raise FileNotFoundError(f"Smolagents factory file not found: {module_path}")
111
113
  spec = importlib.util.spec_from_file_location("smolagents_factory", module_path)
112
114
  if spec is None or spec.loader is None:
113
115
  raise ImportError(f"Unable to load smolagents factory from {module_path}")
@@ -9,7 +9,7 @@
9
9
  * Each may be provided inline or by reference.
10
10
  */
11
11
 
12
- export const SPEC_VERSION = "4.1.0";
12
+ export const SPEC_VERSION = "4.2.0";
13
13
 
14
14
  import {
15
15
  PromptWrapper,
@@ -1,4 +1,4 @@
1
- export const SPEC_VERSION = "4.1.0";
1
+ export const SPEC_VERSION = "4.2.0";
2
2
  import { PromptWrapper, PromptData, PromptRef, OutputSchema, MCPConfig, ToolDefinition, } from "./prompt";
3
3
  import { ProfileWrapper, ProfileData, ProfileRef, ModelConfig, OAuthConfig, } from "./profile";
4
4
  export interface AgentWrapper {
@@ -800,7 +800,7 @@ export interface BackendConfig {
800
800
  aws_region?: string;
801
801
  }
802
802
 
803
- export const SPEC_VERSION = "4.1.0";
803
+ export const SPEC_VERSION = "4.2.0";
804
804
 
805
805
  export interface SDKRuntimeWrapper {
806
806
  spec: "flatagents-runtime";
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "spec_version": {
13
13
  "type": "string",
14
- "const": "4.1.0"
14
+ "const": "4.2.0"
15
15
  },
16
16
  "execution_lock": {
17
17
  "$ref": "#/definitions/ExecutionLock"
@@ -252,7 +252,7 @@ export interface BackendConfig {
252
252
  dynamodb_table?: string;
253
253
  aws_region?: string;
254
254
  }
255
- export const SPEC_VERSION = "4.1.0";
255
+ export const SPEC_VERSION = "4.2.0";
256
256
  export interface SDKRuntimeWrapper {
257
257
  spec: "flatagents-runtime";
258
258
  spec_version: typeof SPEC_VERSION;
@@ -190,7 +190,7 @@ waiting_channel - Signal channel this machine is blocked on (v1.2.0)
190
190
  config_hash - Content-addressed machine config key for cross-SDK resume (v2.1.0)
191
191
  */
192
192
 
193
- export const SPEC_VERSION = "4.1.0";
193
+ export const SPEC_VERSION = "4.2.0";
194
194
 
195
195
  export interface MachineWrapper {
196
196
  spec: "flatmachine";
@@ -209,6 +209,7 @@ export interface MachineRuntimeMetadata {
209
209
  current_state: string;
210
210
  total_api_calls: number;
211
211
  total_cost: number;
212
+ depth?: number;
212
213
  }
213
214
 
214
215
  export interface MachineData {
@@ -245,6 +246,7 @@ export interface HooksRefConfig {
245
246
  export interface MachineSettings {
246
247
  max_steps?: number;
247
248
  parallel_fallback?: "sequential" | "error";
249
+ max_depth?: number;
248
250
  [key: string]: any;
249
251
  }
250
252
 
@@ -350,6 +352,7 @@ export interface MachineSnapshot {
350
352
  loop_cost: number;
351
353
  };
352
354
  config_hash?: string;
355
+ depth?: number;
353
356
  }
354
357
 
355
358
  export interface PersistenceConfig {
@@ -114,6 +114,9 @@
114
114
  },
115
115
  "total_cost": {
116
116
  "type": "number"
117
+ },
118
+ "depth": {
119
+ "type": "number"
117
120
  }
118
121
  },
119
122
  "required": [
@@ -1127,6 +1130,9 @@
1127
1130
  "sequential",
1128
1131
  "error"
1129
1132
  ]
1133
+ },
1134
+ "max_depth": {
1135
+ "type": "number"
1130
1136
  }
1131
1137
  }
1132
1138
  },
@@ -1,4 +1,4 @@
1
- export const SPEC_VERSION = "4.1.0";
1
+ export const SPEC_VERSION = "4.2.0";
2
2
  export interface MachineWrapper {
3
3
  spec: "flatmachine";
4
4
  spec_version: string;
@@ -14,6 +14,7 @@ export interface MachineRuntimeMetadata {
14
14
  current_state: string;
15
15
  total_api_calls: number;
16
16
  total_cost: number;
17
+ depth?: number;
17
18
  }
18
19
  export interface MachineData {
19
20
  name?: string;
@@ -42,6 +43,7 @@ export interface HooksRefConfig {
42
43
  export interface MachineSettings {
43
44
  max_steps?: number;
44
45
  parallel_fallback?: "sequential" | "error";
46
+ max_depth?: number;
45
47
  [key: string]: any;
46
48
  }
47
49
  export interface StateDefinition {
@@ -125,6 +127,7 @@ export interface MachineSnapshot {
125
127
  loop_cost: number;
126
128
  };
127
129
  config_hash?: string;
130
+ depth?: number;
128
131
  }
129
132
  export interface PersistenceConfig {
130
133
  enabled: boolean;
@@ -14,7 +14,7 @@
14
14
  * It does not define prompt text or output schema.
15
15
  */
16
16
 
17
- export const SPEC_VERSION = "4.1.0";
17
+ export const SPEC_VERSION = "4.2.0";
18
18
 
19
19
  export interface ProfileWrapper {
20
20
  spec: "flatprofile";
@@ -1,4 +1,4 @@
1
- export const SPEC_VERSION = "4.1.0";
1
+ export const SPEC_VERSION = "4.2.0";
2
2
  export interface ProfileWrapper {
3
3
  spec: "flatprofile";
4
4
  spec_version: string;
@@ -12,7 +12,7 @@
12
12
  * It does not define runtime, model, profile, or adapter selection.
13
13
  */
14
14
 
15
- export const SPEC_VERSION = "4.1.0";
15
+ export const SPEC_VERSION = "4.2.0";
16
16
 
17
17
  export interface PromptWrapper {
18
18
  spec: "prompt";
@@ -1,4 +1,4 @@
1
- export const SPEC_VERSION = "4.1.0";
1
+ export const SPEC_VERSION = "4.2.0";
2
2
  export interface PromptWrapper {
3
3
  spec: "prompt";
4
4
  spec_version: string;
@@ -1030,6 +1030,7 @@ class FlatAgent(ABC):
1030
1030
  config = {}
1031
1031
 
1032
1032
  if config_file is not None:
1033
+ config_file = os.path.abspath(os.path.expanduser(config_file))
1033
1034
  if not os.path.exists(config_file):
1034
1035
  raise FileNotFoundError(f"Configuration file not found: {config_file}")
1035
1036
 
@@ -446,6 +446,7 @@ class FlatAgent:
446
446
  config_dir = kwargs.get("config_dir") or os.getcwd()
447
447
 
448
448
  if config_file is not None:
449
+ config_file = os.path.abspath(os.path.expanduser(config_file))
449
450
  if not os.path.exists(config_file):
450
451
  raise FileNotFoundError(f"Configuration file not found: {config_file}")
451
452
  with open(config_file, 'r') as f:
@@ -515,7 +516,8 @@ class FlatAgent:
515
516
  except ImportError:
516
517
  yaml = None
517
518
 
518
- path = ref if os.path.isabs(ref) else os.path.join(self._config_dir, ref)
519
+ expanded = os.path.expanduser(ref)
520
+ path = expanded if os.path.isabs(expanded) else os.path.join(self._config_dir, expanded)
519
521
  if not os.path.exists(path):
520
522
  raise FileNotFoundError(f"Referenced config file not found: {path}")
521
523
 
@@ -120,15 +120,21 @@ def setup_logging(
120
120
  if force:
121
121
  lib_logger.handlers.clear()
122
122
 
123
- # Only add a handler if this logger has none yet (avoid duplicates)
124
- if not lib_logger.handlers:
125
- stdout_handler = logging.StreamHandler(sys.stdout)
126
- stdout_handler.setFormatter(formatter)
127
- lib_logger.addHandler(stdout_handler)
128
-
129
- # Prevent propagation to root to avoid duplicate output when the
130
- # application also configures root-level handlers.
131
- lib_logger.propagate = False
123
+ # Opt-in to a console handler via FLATAGENTS_LOG_HANDLER.
124
+ # This keeps headful apps (CLI, TUI, REPL) from seeing library
125
+ # logs unless the user explicitly requests them.
126
+ add_console = os.getenv('FLATAGENTS_LOG_HANDLER', '').lower() in ('stdout', 'console', 'true')
127
+
128
+ if add_console:
129
+ if not lib_logger.handlers:
130
+ stdout_handler = logging.StreamHandler(sys.stdout)
131
+ stdout_handler.setFormatter(formatter)
132
+ lib_logger.addHandler(stdout_handler)
133
+ lib_logger.propagate = False
134
+ else:
135
+ # No stdout handler by default — logs propagate to the host app's
136
+ # root logger so headful apps control whether they appear on console.
137
+ lib_logger.propagate = True
132
138
 
133
139
  # Add file handler if FLATAGENTS_LOG_DIR is set
134
140
  log_dir = os.getenv('FLATAGENTS_LOG_DIR')
@@ -232,8 +238,13 @@ def _init_metrics() -> None:
232
238
  SERVICE_NAME: service_name
233
239
  })
234
240
 
235
- # Check which exporter to use (default to console so metrics are visible)
236
- exporter_type = os.getenv('OTEL_METRICS_EXPORTER', 'console').lower()
241
+ # Check which exporter to use (default to none headful apps
242
+ # control whether metrics appear via explicit env opt-in).
243
+ exporter_type = os.getenv('OTEL_METRICS_EXPORTER', 'none').lower()
244
+
245
+ if exporter_type == 'none':
246
+ _metrics_enabled = False
247
+ return
237
248
 
238
249
  if exporter_type == 'console':
239
250
  # Use console exporter for testing/debugging
@@ -196,7 +196,12 @@ class ProfileManager:
196
196
  if override_cfg:
197
197
  result.update(override_cfg)
198
198
  else:
199
- logger.warning(f"Override profile '{self._override_profile}' not found")
199
+ available_profiles = list(self._profiles.keys())
200
+ raise ValueError(
201
+ f"Override profile '{self._override_profile}' not found. "
202
+ f"The intended model override was NOT applied. "
203
+ f"Available profiles: {available_profiles}"
204
+ )
200
205
 
201
206
  return result
202
207
 
@@ -224,9 +229,9 @@ def load_profiles_from_file(profiles_file: str) -> Dict[str, Any]:
224
229
  except ImportError:
225
230
  raise ImportError("pyyaml is required for profiles.yml")
226
231
 
232
+ profiles_file = os.path.abspath(os.path.expanduser(profiles_file))
227
233
  if not os.path.exists(profiles_file):
228
- logger.debug(f"No profiles file at {profiles_file}")
229
- return {'profiles': {}, 'default': None, 'override': None}
234
+ raise FileNotFoundError(f"Profiles file not found: {profiles_file}")
230
235
 
231
236
  with open(profiles_file, 'r') as f:
232
237
  config = yaml.safe_load(f) or {}
@@ -320,7 +325,13 @@ def discover_profiles_file(config_dir: str, explicit_path: Optional[str] = None)
320
325
  Path to profiles.yml if found, explicit_path if provided, or None
321
326
  """
322
327
  if explicit_path:
323
- return explicit_path
328
+ path = os.path.expanduser(explicit_path)
329
+ if not os.path.isabs(path):
330
+ path = os.path.join(config_dir, path)
331
+ path = os.path.abspath(path)
332
+ if not os.path.exists(path):
333
+ raise FileNotFoundError(f"Profiles file not found: {path}")
334
+ return path
324
335
  default_path = os.path.join(config_dir, 'profiles.yml')
325
336
  return default_path if os.path.exists(default_path) else None
326
337
 
File without changes
@@ -0,0 +1,189 @@
1
+ """Tests for monitoring.py — headful logging opt-in and metrics defaults."""
2
+
3
+ import logging
4
+ import sys
5
+ import importlib
6
+
7
+ import pytest
8
+
9
+
10
+ # ── Helpers ──────────────────────────────────────────────────────────────────
11
+
12
+ _ENV_KEYS = (
13
+ "FLATAGENTS_LOG_HANDLER",
14
+ "FLATAGENTS_LOG_LEVEL",
15
+ "FLATAGENTS_LOG_FORMAT",
16
+ "FLATAGENTS_LOG_DIR",
17
+ "FLATAGENTS_METRICS_ENABLED",
18
+ "OTEL_METRICS_EXPORTER",
19
+ "OTEL_SERVICE_NAME",
20
+ "OTEL_EXPORTER_OTLP_ENDPOINT",
21
+ )
22
+
23
+
24
+ def _reset_env(monkeypatch):
25
+ """Clear flatagents-related env vars."""
26
+ for key in _ENV_KEYS:
27
+ monkeypatch.delenv(key, raising=False)
28
+
29
+
30
+ def _reset_module(mod):
31
+ """Reset flatagents.monitoring internal state."""
32
+ mod._logging_configured = False
33
+ mod._metrics_init_attempted = False
34
+ mod._metrics_enabled = False
35
+ mod._meter = None
36
+ mod._cached_histograms.clear()
37
+ lib_logger = logging.getLogger("flatagents")
38
+ lib_logger.handlers.clear()
39
+ lib_logger.propagate = True
40
+
41
+
42
+ # ── Fixtures ────────────────────────────────────────────────────────────────
43
+
44
+
45
+ @pytest.fixture(autouse=True)
46
+ def _clean_flatagents(monkeypatch):
47
+ """Auto-reset env and module state for every test."""
48
+ _reset_env(monkeypatch)
49
+ mod = importlib.import_module("flatagents.monitoring")
50
+ _reset_module(mod)
51
+ yield
52
+
53
+
54
+ # ── Issue 1: setup_logging ──────────────────────────────────────────────────
55
+
56
+
57
+ class TestSetupLoggingDefault:
58
+ """setup_logging() adds no stdout handler and sets propagate=True."""
59
+
60
+ def test_no_console_handler_by_default(self):
61
+ from flatagents.monitoring import setup_logging
62
+
63
+ setup_logging()
64
+ lib_logger = logging.getLogger("flatagents")
65
+
66
+ stdout_handlers = [
67
+ h
68
+ for h in lib_logger.handlers
69
+ if isinstance(h, logging.StreamHandler)
70
+ and h.stream is sys.stdout
71
+ ]
72
+ assert len(stdout_handlers) == 0
73
+
74
+ def test_propagate_is_true_by_default(self):
75
+ from flatagents.monitoring import setup_logging
76
+
77
+ setup_logging()
78
+ lib_logger = logging.getLogger("flatagents")
79
+
80
+ assert lib_logger.propagate is True
81
+
82
+ def test_no_duplicate_handlers_on_second_call(self):
83
+ from flatagents.monitoring import setup_logging
84
+
85
+ setup_logging()
86
+ setup_logging()
87
+ lib_logger = logging.getLogger("flatagents")
88
+
89
+ stdout_handlers = [
90
+ h
91
+ for h in lib_logger.handlers
92
+ if isinstance(h, logging.StreamHandler)
93
+ and h.stream is sys.stdout
94
+ ]
95
+ assert len(stdout_handlers) <= 1
96
+
97
+
98
+ class TestSetupLoggingOptIn:
99
+ """FLATAGENTS_LOG_HANDLER=stdout adds handler and sets propagate=False."""
100
+
101
+ @pytest.mark.parametrize(
102
+ "opt_in_value, should_add_handler",
103
+ [
104
+ ("stdout", True),
105
+ ("console", True),
106
+ ("true", True),
107
+ ("STDLONG", False),
108
+ ("1", False),
109
+ ("", False),
110
+ ],
111
+ )
112
+ def test_opt_in_variations(self, opt_in_value, should_add_handler, monkeypatch):
113
+ """Test various env values for FLATAGENTS_LOG_HANDLER."""
114
+ if opt_in_value:
115
+ monkeypatch.setenv("FLATAGENTS_LOG_HANDLER", opt_in_value)
116
+
117
+ from flatagents.monitoring import setup_logging
118
+
119
+ setup_logging()
120
+ lib_logger = logging.getLogger("flatagents")
121
+
122
+ stdout_handlers = [
123
+ h
124
+ for h in lib_logger.handlers
125
+ if isinstance(h, logging.StreamHandler)
126
+ and h.stream is sys.stdout
127
+ ]
128
+ assert len(stdout_handlers) == (1 if should_add_handler else 0)
129
+ # When handler added: propagate=False; when not: propagate=True
130
+ assert lib_logger.propagate == (not should_add_handler)
131
+
132
+
133
+ # ── Issue 2: _init_metrics ─────────────────────────────────────────────────
134
+
135
+ class TestInitMetricsDefault:
136
+ """_init_metrics() with default env does NOT create console exporter."""
137
+
138
+ def test_metrics_disabled_by_default(self):
139
+ """Even when otel is installed, default exporter=none disables metrics."""
140
+ try:
141
+ import opentelemetry # noqa: F401
142
+ except ImportError:
143
+ pytest.skip("opentelemetry not installed")
144
+
145
+ import flatagents.monitoring as mod
146
+ from flatagents.monitoring import _init_metrics
147
+
148
+ _init_metrics()
149
+ assert mod._metrics_enabled is False
150
+
151
+ def test_get_meter_returns_none_when_metrics_disabled(self):
152
+ try:
153
+ import opentelemetry # noqa: F401
154
+ except ImportError:
155
+ pytest.skip("opentelemetry not installed")
156
+
157
+ from flatagents.monitoring import get_meter
158
+
159
+ assert get_meter() is None
160
+
161
+
162
+ class TestInitMetricsConsoleExporter:
163
+ """OTEL_METRICS_EXPORTER=console creates a console exporter."""
164
+
165
+ def test_metrics_enabled_with_console_exporter(self, monkeypatch):
166
+ try:
167
+ import opentelemetry # noqa: F401
168
+ except ImportError:
169
+ pytest.skip("opentelemetry not installed")
170
+
171
+ monkeypatch.setenv("OTEL_METRICS_EXPORTER", "console")
172
+
173
+ import flatagents.monitoring as mod
174
+ from flatagents.monitoring import _init_metrics
175
+
176
+ _init_metrics()
177
+ assert mod._metrics_enabled is True
178
+
179
+ def test_get_meter_returns_meter_with_console_exporter(self, monkeypatch):
180
+ try:
181
+ import opentelemetry # noqa: F401
182
+ except ImportError:
183
+ pytest.skip("opentelemetry not installed")
184
+
185
+ monkeypatch.setenv("OTEL_METRICS_EXPORTER", "console")
186
+
187
+ from flatagents.monitoring import get_meter
188
+
189
+ assert get_meter() is not None
@@ -0,0 +1,25 @@
1
+ """Tests for model profile resolution."""
2
+
3
+ import pytest
4
+
5
+ from flatagents.profiles import ProfileManager
6
+
7
+
8
+ def test_missing_override_profile_fails_fast_with_actionable_message():
9
+ manager = ProfileManager(
10
+ {
11
+ "profiles": {
12
+ "cheap": {"provider": "cerebras", "name": "zai-glm-4.6"},
13
+ },
14
+ "default": "cheap",
15
+ "override": "fast",
16
+ }
17
+ )
18
+
19
+ with pytest.raises(ValueError) as exc_info:
20
+ manager.resolve_model_config(None)
21
+
22
+ message = str(exc_info.value)
23
+ assert "Override profile 'fast' not found" in message
24
+ assert "intended model override was NOT applied" in message
25
+ assert "cheap" in message
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flatagents"
7
- version = "4.1.0"
7
+ version = "4.2.0"
8
8
  description = "A lightweight framework for building LLM-powered agents."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
11
- requires-python = ">=3.10"
11
+ requires-python = ">=3.11"
12
12
  authors = [
13
13
  { email = "memgrafter@gmail.com" }
14
14
  ]
File without changes
File without changes