flatagents 4.0.1__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.
- {flatagents-4.0.1 → flatagents-4.2.0}/AGENTS.md +1 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/PKG-INFO +2 -2
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/__init__.py +1 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/adapters/claude_code.py +6 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/adapters/codex_cli.py +27 -5
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/adapters/pi_agent_bridge.py +6 -2
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/adapters/smolagents.py +3 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/flatagent.d.ts +1 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/flatagent.schema.json +3 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/flatagent.slim.d.ts +1 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/flatagents-runtime.d.ts +1 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/flatagents-runtime.schema.json +1 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/flatagents-runtime.slim.d.ts +1 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/flatmachine.d.ts +9 -6
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/flatmachine.schema.json +9 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/flatmachine.slim.d.ts +4 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/profile.d.ts +1 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/profile.slim.d.ts +1 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/prompt.d.ts +3 -2
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/prompt.schema.json +3 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/prompt.slim.d.ts +2 -1
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/baseagent.py +1 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/flatagent.py +110 -2
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/monitoring.py +22 -11
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/profiles.py +15 -4
- flatagents-4.2.0/flatagents/tests/__init__.py +0 -0
- flatagents-4.2.0/flatagents/tests/test_monitoring.py +189 -0
- flatagents-4.2.0/flatagents/tests/test_profiles.py +25 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/tool_loop.py +21 -15
- {flatagents-4.0.1 → flatagents-4.2.0}/pyproject.toml +2 -2
- {flatagents-4.0.1 → flatagents-4.2.0}/.gitignore +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/README.md +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/adapters/__init__.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/adapters/call_throttle.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/adapters/claude_code_sessions.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/adapters/codex_cli_sessions.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/adapters/compat.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/adapters/pi_agent_runner.mjs +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/README.md +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/__init__.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/assets/profile.schema.json +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/__init__.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/anthropic.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/cerebras.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/github_copilot_auth.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/github_copilot_client.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/github_copilot_login.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/github_copilot_types.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/openai.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/openai_codex_auth.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/openai_codex_client.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/openai_codex_login.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/providers/openai_codex_types.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/tools.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/utils.py +0 -0
- {flatagents-4.0.1 → flatagents-4.2.0}/flatagents/validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flatagents
|
|
3
|
-
Version: 4.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.
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
21
|
Requires-Dist: aiofiles
|
|
22
22
|
Requires-Dist: httpx
|
|
23
23
|
Requires-Dist: jinja2
|
|
@@ -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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
|
866
|
-
|
|
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
|
-
|
|
107
|
-
runner_path = os.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}")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SPEC_VERSION = "4.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 {
|
|
@@ -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.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.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
|
|
|
@@ -288,13 +290,13 @@ export interface StateDefinition {
|
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
export interface ToolLoopStateConfig {
|
|
291
|
-
max_tool_calls?: number; // default:
|
|
292
|
-
max_turns?: number; // default:
|
|
293
|
+
max_tool_calls?: number; // default: 0 (unlimited)
|
|
294
|
+
max_turns?: number; // default: 0 (unlimited); counts LLM calls, not tool calls
|
|
293
295
|
allowed_tools?: string[]; // allowlist
|
|
294
296
|
denied_tools?: string[]; // denylist (wins)
|
|
295
|
-
tool_timeout?: number; // seconds, default:
|
|
296
|
-
total_timeout?: number; // seconds, default:
|
|
297
|
-
max_cost?: number; // dollars
|
|
297
|
+
tool_timeout?: number; // seconds, default: 0 (unlimited)
|
|
298
|
+
total_timeout?: number; // seconds, default: 0 (unlimited)
|
|
299
|
+
max_cost?: number; // dollars, default: 0 (unlimited)
|
|
298
300
|
}
|
|
299
301
|
|
|
300
302
|
export interface MachineInput {
|
|
@@ -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": [
|
|
@@ -210,6 +213,9 @@
|
|
|
210
213
|
"instruction_suffix": {
|
|
211
214
|
"type": "string"
|
|
212
215
|
},
|
|
216
|
+
"post_history_instructions": {
|
|
217
|
+
"type": "string"
|
|
218
|
+
},
|
|
213
219
|
"output": {
|
|
214
220
|
"$ref": "#/definitions/OutputSchema"
|
|
215
221
|
},
|
|
@@ -1124,6 +1130,9 @@
|
|
|
1124
1130
|
"sequential",
|
|
1125
1131
|
"error"
|
|
1126
1132
|
]
|
|
1133
|
+
},
|
|
1134
|
+
"max_depth": {
|
|
1135
|
+
"type": "number"
|
|
1127
1136
|
}
|
|
1128
1137
|
}
|
|
1129
1138
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SPEC_VERSION = "4.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;
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* Prompt is the pure prompt/output contract.
|
|
6
6
|
*
|
|
7
7
|
* It defines:
|
|
8
|
-
* - authored prompt text (`system`, `user`, `instruction_suffix`)
|
|
8
|
+
* - authored prompt text (`system`, `user`, `instruction_suffix`, `post_history_instructions`)
|
|
9
9
|
* - expected structured output (`output`)
|
|
10
10
|
* - optional model-facing tool declarations (`tools`, `mcp`)
|
|
11
11
|
*
|
|
12
12
|
* It does not define runtime, model, profile, or adapter selection.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
export const SPEC_VERSION = "4.0
|
|
15
|
+
export const SPEC_VERSION = "4.2.0";
|
|
16
16
|
|
|
17
17
|
export interface PromptWrapper {
|
|
18
18
|
spec: "prompt";
|
|
@@ -26,6 +26,7 @@ export interface PromptData {
|
|
|
26
26
|
system?: string;
|
|
27
27
|
user: string;
|
|
28
28
|
instruction_suffix?: string;
|
|
29
|
+
post_history_instructions?: string;
|
|
29
30
|
output?: OutputSchema;
|
|
30
31
|
mcp?: MCPConfig;
|
|
31
32
|
tools?: ToolDefinition[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SPEC_VERSION = "4.0
|
|
1
|
+
export const SPEC_VERSION = "4.2.0";
|
|
2
2
|
export interface PromptWrapper {
|
|
3
3
|
spec: "prompt";
|
|
4
4
|
spec_version: string;
|
|
@@ -10,6 +10,7 @@ export interface PromptData {
|
|
|
10
10
|
system?: string;
|
|
11
11
|
user: string;
|
|
12
12
|
instruction_suffix?: string;
|
|
13
|
+
post_history_instructions?: string;
|
|
13
14
|
output?: OutputSchema;
|
|
14
15
|
mcp?: MCPConfig;
|
|
15
16
|
tools?: ToolDefinition[];
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -604,6 +606,7 @@ class FlatAgent:
|
|
|
604
606
|
self._system_prompt_template = prompt_data.get('system', self.DEFAULT_SYSTEM_PROMPT)
|
|
605
607
|
self._user_prompt_template = prompt_data.get('user', '')
|
|
606
608
|
self._instruction_suffix = prompt_data.get('instruction_suffix', '')
|
|
609
|
+
self._post_history_instructions = prompt_data.get('post_history_instructions', '')
|
|
607
610
|
self.output_schema = prompt_data.get('output', {})
|
|
608
611
|
self.mcp_config = prompt_data.get('mcp')
|
|
609
612
|
self.tools = prompt_data.get('tools', [])
|
|
@@ -615,6 +618,11 @@ class FlatAgent:
|
|
|
615
618
|
if self._instruction_suffix
|
|
616
619
|
else None
|
|
617
620
|
)
|
|
621
|
+
self._compiled_post_history_instructions = (
|
|
622
|
+
self._jinja_env.from_string(self._post_history_instructions)
|
|
623
|
+
if self._post_history_instructions
|
|
624
|
+
else None
|
|
625
|
+
)
|
|
618
626
|
|
|
619
627
|
if self._runtime_type == 'llm':
|
|
620
628
|
model_config = profile_data.get('model')
|
|
@@ -644,6 +652,7 @@ class FlatAgent:
|
|
|
644
652
|
self._system_prompt_template = data.get('system', self.DEFAULT_SYSTEM_PROMPT)
|
|
645
653
|
self._user_prompt_template = data.get('user', '')
|
|
646
654
|
self._instruction_suffix = data.get('instruction_suffix', '')
|
|
655
|
+
self._post_history_instructions = data.get('post_history_instructions', '')
|
|
647
656
|
self._compiled_system = self._jinja_env.from_string(self._system_prompt_template)
|
|
648
657
|
self._compiled_user = self._jinja_env.from_string(self._user_prompt_template)
|
|
649
658
|
self._compiled_instruction_suffix = (
|
|
@@ -651,6 +660,11 @@ class FlatAgent:
|
|
|
651
660
|
if self._instruction_suffix
|
|
652
661
|
else None
|
|
653
662
|
)
|
|
663
|
+
self._compiled_post_history_instructions = (
|
|
664
|
+
self._jinja_env.from_string(self._post_history_instructions)
|
|
665
|
+
if self._post_history_instructions
|
|
666
|
+
else None
|
|
667
|
+
)
|
|
654
668
|
self.output_schema = data.get('output', {})
|
|
655
669
|
self.mcp_config = data.get('mcp')
|
|
656
670
|
self.tools = data.get('tools', [])
|
|
@@ -904,6 +918,87 @@ class FlatAgent:
|
|
|
904
918
|
prompt = f"{prompt}\n\n{suffix}"
|
|
905
919
|
return prompt
|
|
906
920
|
|
|
921
|
+
def _render_post_history_instructions(
|
|
922
|
+
self,
|
|
923
|
+
input_data: Dict[str, Any],
|
|
924
|
+
tools_prompt: str = "",
|
|
925
|
+
tools: Optional[List[Dict]] = None,
|
|
926
|
+
) -> str:
|
|
927
|
+
"""
|
|
928
|
+
Render post-history instructions with the same template context as prompts.
|
|
929
|
+
|
|
930
|
+
These instructions are not part of the stored logical conversation. They
|
|
931
|
+
are appended ephemerally to the final submitted user/tool message after
|
|
932
|
+
the full message history has been constructed.
|
|
933
|
+
"""
|
|
934
|
+
if self._compiled_post_history_instructions is None:
|
|
935
|
+
return ""
|
|
936
|
+
|
|
937
|
+
model_config = {
|
|
938
|
+
**self._model_config_raw,
|
|
939
|
+
"name": self.model,
|
|
940
|
+
"temperature": self.temperature,
|
|
941
|
+
"max_tokens": self.max_tokens,
|
|
942
|
+
"top_p": self.top_p,
|
|
943
|
+
"top_k": self.top_k,
|
|
944
|
+
"frequency_penalty": self.frequency_penalty,
|
|
945
|
+
"presence_penalty": self.presence_penalty,
|
|
946
|
+
"seed": self.seed,
|
|
947
|
+
"base_url": self.base_url,
|
|
948
|
+
}
|
|
949
|
+
return self._compiled_post_history_instructions.render(
|
|
950
|
+
input=input_data,
|
|
951
|
+
tools_prompt=tools_prompt,
|
|
952
|
+
tools=tools or [],
|
|
953
|
+
model=model_config,
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
def _messages_with_post_history_instructions(
|
|
957
|
+
self,
|
|
958
|
+
messages: List[Dict[str, Any]],
|
|
959
|
+
post_history_instructions: str,
|
|
960
|
+
) -> List[Dict[str, Any]]:
|
|
961
|
+
"""
|
|
962
|
+
Return an ephemeral copy of messages with post-history instructions.
|
|
963
|
+
|
|
964
|
+
Rules:
|
|
965
|
+
- if the final submitted message is user/tool, append there;
|
|
966
|
+
- otherwise append a synthetic user message;
|
|
967
|
+
- never mutate the original list or message dicts.
|
|
968
|
+
"""
|
|
969
|
+
instructions = (post_history_instructions or "").strip()
|
|
970
|
+
if not instructions:
|
|
971
|
+
return messages
|
|
972
|
+
|
|
973
|
+
copied = [dict(message) for message in messages]
|
|
974
|
+
body = f"System:\n\n{instructions}"
|
|
975
|
+
|
|
976
|
+
if copied and copied[-1].get("role") in ("user", "tool"):
|
|
977
|
+
self._append_post_history_to_message(copied[-1], body)
|
|
978
|
+
else:
|
|
979
|
+
copied.append({"role": "user", "content": body})
|
|
980
|
+
return copied
|
|
981
|
+
|
|
982
|
+
def _append_post_history_to_message(self, message: Dict[str, Any], body: str) -> None:
|
|
983
|
+
"""Append rendered post-history body to one copied message in-place."""
|
|
984
|
+
content = message.get("content")
|
|
985
|
+
append_text = f"\n\n{body}"
|
|
986
|
+
|
|
987
|
+
if isinstance(content, str):
|
|
988
|
+
message["content"] = f"{content}{append_text}" if content else body
|
|
989
|
+
return
|
|
990
|
+
|
|
991
|
+
if isinstance(content, list):
|
|
992
|
+
text = append_text if content else body
|
|
993
|
+
message["content"] = list(content) + [{"type": "text", "text": text}]
|
|
994
|
+
return
|
|
995
|
+
|
|
996
|
+
if content is None:
|
|
997
|
+
message["content"] = body
|
|
998
|
+
return
|
|
999
|
+
|
|
1000
|
+
message["content"] = f"{content}{append_text}"
|
|
1001
|
+
|
|
907
1002
|
def _runtime_task_from_prompt(self, system_prompt: str, user_prompt: str) -> str:
|
|
908
1003
|
"""Flatten prompt fields into a runtime task when needed."""
|
|
909
1004
|
if self._runtime_type == "claude-code":
|
|
@@ -1150,10 +1245,23 @@ class FlatAgent:
|
|
|
1150
1245
|
{"role": "user", "content": user_prompt}
|
|
1151
1246
|
]
|
|
1152
1247
|
|
|
1248
|
+
# Append post-history instructions only to an ephemeral submit copy.
|
|
1249
|
+
# The logical history, caller-provided messages, and rendered_user_prompt
|
|
1250
|
+
# must remain unchanged so tool-loop chains/checkpoints never persist it.
|
|
1251
|
+
post_history_instructions = self._render_post_history_instructions(
|
|
1252
|
+
input_data,
|
|
1253
|
+
tools_prompt=tools_prompt,
|
|
1254
|
+
tools=_mcp_tools,
|
|
1255
|
+
)
|
|
1256
|
+
submit_messages = self._messages_with_post_history_instructions(
|
|
1257
|
+
all_messages,
|
|
1258
|
+
post_history_instructions,
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1153
1261
|
# Build LLM call parameters
|
|
1154
1262
|
params = {
|
|
1155
1263
|
"model": self.model,
|
|
1156
|
-
"messages":
|
|
1264
|
+
"messages": submit_messages,
|
|
1157
1265
|
}
|
|
1158
1266
|
# Only include temperature if explicitly provided
|
|
1159
1267
|
if self.temperature is not None:
|
|
@@ -120,15 +120,21 @@ def setup_logging(
|
|
|
120
120
|
if force:
|
|
121
121
|
lib_logger.handlers.clear()
|
|
122
122
|
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -55,14 +55,18 @@ class Tool:
|
|
|
55
55
|
|
|
56
56
|
@dataclass
|
|
57
57
|
class Guardrails:
|
|
58
|
-
"""Configurable limits for the tool loop.
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
"""Configurable limits for the tool loop.
|
|
59
|
+
|
|
60
|
+
Numeric guardrails use 0 to mean unlimited/disabled.
|
|
61
|
+
max_turns counts LLM calls, not tool calls.
|
|
62
|
+
"""
|
|
63
|
+
max_tool_calls: int = 0
|
|
64
|
+
max_turns: int = 0
|
|
61
65
|
allowed_tools: Optional[List[str]] = None
|
|
62
66
|
denied_tools: Optional[List[str]] = None
|
|
63
|
-
tool_timeout: float =
|
|
64
|
-
total_timeout: float =
|
|
65
|
-
max_cost: Optional[float] =
|
|
67
|
+
tool_timeout: float = 0.0
|
|
68
|
+
total_timeout: float = 0.0
|
|
69
|
+
max_cost: Optional[float] = 0.0
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
@dataclass
|
|
@@ -228,7 +232,7 @@ class ToolLoopAgent:
|
|
|
228
232
|
while True:
|
|
229
233
|
# Check total timeout
|
|
230
234
|
elapsed = time.monotonic() - start_time
|
|
231
|
-
if elapsed >= guardrails.total_timeout:
|
|
235
|
+
if guardrails.total_timeout > 0 and elapsed >= guardrails.total_timeout:
|
|
232
236
|
return ToolLoopResult(
|
|
233
237
|
content=last_content, messages=chain,
|
|
234
238
|
tool_calls_count=tool_calls_count, turns=turns,
|
|
@@ -236,7 +240,7 @@ class ToolLoopAgent:
|
|
|
236
240
|
)
|
|
237
241
|
|
|
238
242
|
# Check max turns
|
|
239
|
-
if turns >= guardrails.max_turns:
|
|
243
|
+
if guardrails.max_turns > 0 and turns >= guardrails.max_turns:
|
|
240
244
|
return ToolLoopResult(
|
|
241
245
|
content=last_content, messages=chain,
|
|
242
246
|
tool_calls_count=tool_calls_count, turns=turns,
|
|
@@ -244,7 +248,7 @@ class ToolLoopAgent:
|
|
|
244
248
|
)
|
|
245
249
|
|
|
246
250
|
# Check cost limit
|
|
247
|
-
if guardrails.max_cost is not None and usage.total_cost >= guardrails.max_cost:
|
|
251
|
+
if guardrails.max_cost is not None and guardrails.max_cost > 0 and usage.total_cost >= guardrails.max_cost:
|
|
248
252
|
return ToolLoopResult(
|
|
249
253
|
content=last_content, messages=chain,
|
|
250
254
|
tool_calls_count=tool_calls_count, turns=turns,
|
|
@@ -292,6 +296,7 @@ class ToolLoopAgent:
|
|
|
292
296
|
if (
|
|
293
297
|
response.finish_reason == FinishReason.TOOL_USE
|
|
294
298
|
and guardrails.max_cost is not None
|
|
299
|
+
and guardrails.max_cost > 0
|
|
295
300
|
and usage.total_cost >= guardrails.max_cost
|
|
296
301
|
):
|
|
297
302
|
return ToolLoopResult(
|
|
@@ -312,7 +317,7 @@ class ToolLoopAgent:
|
|
|
312
317
|
|
|
313
318
|
# We have tool calls — check guardrails before executing
|
|
314
319
|
pending_calls = response.tool_calls or []
|
|
315
|
-
if tool_calls_count + len(pending_calls) > guardrails.max_tool_calls:
|
|
320
|
+
if guardrails.max_tool_calls > 0 and tool_calls_count + len(pending_calls) > guardrails.max_tool_calls:
|
|
316
321
|
return ToolLoopResult(
|
|
317
322
|
content=last_content, messages=chain,
|
|
318
323
|
tool_calls_count=tool_calls_count, turns=turns,
|
|
@@ -358,12 +363,13 @@ class ToolLoopAgent:
|
|
|
358
363
|
if guardrails.allowed_tools and name not in guardrails.allowed_tools:
|
|
359
364
|
return ToolResult(content=f"Tool '{name}' is not allowed", is_error=True)
|
|
360
365
|
|
|
361
|
-
# Execute via provider with timeout
|
|
366
|
+
# Execute via provider with timeout. 0 disables the per-tool timeout.
|
|
362
367
|
try:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
timeout=guardrails.tool_timeout
|
|
366
|
-
|
|
368
|
+
coroutine = self._provider.execute_tool(name, tool_call_id, arguments)
|
|
369
|
+
if guardrails.tool_timeout > 0:
|
|
370
|
+
result = await asyncio.wait_for(coroutine, timeout=guardrails.tool_timeout)
|
|
371
|
+
else:
|
|
372
|
+
result = await coroutine
|
|
367
373
|
return result
|
|
368
374
|
except asyncio.TimeoutError:
|
|
369
375
|
return ToolResult(
|
|
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flatagents"
|
|
7
|
-
version = "4.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.
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
12
|
authors = [
|
|
13
13
|
{ email = "memgrafter@gmail.com" }
|
|
14
14
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|