agentx-dev 3.0.2__tar.gz → 3.0.5__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.
- {agentx_dev-3.0.2/agentx_dev.egg-info → agentx_dev-3.0.5}/PKG-INFO +1 -1
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Agents/Agent.py +139 -14
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Runner/AgentRun.py +160 -40
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Runner/AsyncAgentRun.py +84 -2
- {agentx_dev-3.0.2 → agentx_dev-3.0.5/agentx_dev.egg-info}/PKG-INFO +1 -1
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev.egg-info/top_level.txt +0 -1
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/pyproject.toml +1 -1
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/AGENTX.md +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/LICENSE +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/MANIFEST.in +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/README.md +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Agents/__init__.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/AsyncTools.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/AutoSetup.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Cache.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/ChatModel.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Config.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/DefaultTools.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Loader.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/MCP.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Memory.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Observability.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Planner.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Runner/__init__.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Runner/promptTemplate.yaml +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Session.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Streaming.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Supervisor.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/Tools.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/WebTools.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/__init__.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/resources/__init__.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev/resources/promptTemplate.yaml +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev.egg-info/SOURCES.txt +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev.egg-info/dependency_links.txt +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/agentx_dev.egg-info/requires.txt +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/README.md +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/async_example.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/async_quickstart.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/auto_features_example.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/caching_example.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/chatbot_example.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/complete_example.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/concurrent_example.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/concurrent_tool_example.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/file_agent_demo.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/function_calling_demo.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/mcp_demo.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/observability_example.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/orchestration_demo.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/planner_example.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/supervisor_codebase_analysis_demo.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/supervisor_example.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/examples/sync_quickstart.py +0 -0
- {agentx_dev-3.0.2 → agentx_dev-3.0.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentx-dev
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.5
|
|
4
4
|
Summary: A lightweight LLM agent framework with multi-agent orchestration (Supervisor + dynamic specialist spawning), permission-gated default tools, MCP integration, and prompt-template presets.
|
|
5
5
|
Author-email: Bruce-Arhin Shadrach <brucearhin098@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -99,8 +99,28 @@ class StandardParser(_ParserMixin, BaseModel):
|
|
|
99
99
|
- from_json: Creates an instance from a JSON string.
|
|
100
100
|
- from_function_call: Creates an instance from a tool-call args dict.
|
|
101
101
|
"""
|
|
102
|
-
action: str = Field(
|
|
103
|
-
|
|
102
|
+
action: str = Field(
|
|
103
|
+
default="",
|
|
104
|
+
description=(
|
|
105
|
+
"REQUIRED. Must be one of the registered tool names shown "
|
|
106
|
+
"in the system prompt, OR the exact literal string "
|
|
107
|
+
"'Final_Answer' (with underscore) when you are ready to "
|
|
108
|
+
"return the completed answer. Do NOT put natural-language "
|
|
109
|
+
"response text here — that goes in action_input."
|
|
110
|
+
),
|
|
111
|
+
alias='action',
|
|
112
|
+
)
|
|
113
|
+
action_input: str | Dict | List = Field(
|
|
114
|
+
default="",
|
|
115
|
+
description=(
|
|
116
|
+
"REQUIRED. When action is a tool name, this is the JSON "
|
|
117
|
+
"object matching that tool's argument schema (or the "
|
|
118
|
+
"single string the tool expects). When action is "
|
|
119
|
+
"'Final_Answer', this is the actual answer TEXT you want "
|
|
120
|
+
"the user to see."
|
|
121
|
+
),
|
|
122
|
+
alias='action_input',
|
|
123
|
+
)
|
|
104
124
|
|
|
105
125
|
model_config = ConfigDict(populate_by_name=True)
|
|
106
126
|
|
|
@@ -150,8 +170,27 @@ class Instruction_Tuned_(_ParserMixin, BaseModel):
|
|
|
150
170
|
- action: str, the action to take or final answer.
|
|
151
171
|
- action_input: str, Dict, or List, the input to the action.
|
|
152
172
|
"""
|
|
153
|
-
action: str = Field(
|
|
154
|
-
|
|
173
|
+
action: str = Field(
|
|
174
|
+
default="",
|
|
175
|
+
description=(
|
|
176
|
+
"REQUIRED. Must be one of the registered tool names shown "
|
|
177
|
+
"in the system prompt, OR the exact literal string "
|
|
178
|
+
"'Final_Answer' (with underscore) when you are ready to "
|
|
179
|
+
"return the completed answer. Never put natural-language "
|
|
180
|
+
"response text here — that goes in action_input."
|
|
181
|
+
),
|
|
182
|
+
alias='action',
|
|
183
|
+
)
|
|
184
|
+
action_input: str | Dict | List = Field(
|
|
185
|
+
default="",
|
|
186
|
+
description=(
|
|
187
|
+
"REQUIRED. When action is a tool name, this is the JSON "
|
|
188
|
+
"object matching that tool's schema (or the single string "
|
|
189
|
+
"it expects). When action is 'Final_Answer', this is the "
|
|
190
|
+
"actual answer TEXT you want the user to see."
|
|
191
|
+
),
|
|
192
|
+
alias='action_input',
|
|
193
|
+
)
|
|
155
194
|
|
|
156
195
|
model_config = ConfigDict(populate_by_name=True)
|
|
157
196
|
|
|
@@ -172,9 +211,36 @@ class React_(_ParserMixin, BaseModel):
|
|
|
172
211
|
- action: str, the action to take or final answer.
|
|
173
212
|
- action_input: str, Dict, or List, the input to the action.
|
|
174
213
|
"""
|
|
175
|
-
Thought: str = Field(
|
|
176
|
-
|
|
177
|
-
|
|
214
|
+
Thought: str = Field(
|
|
215
|
+
default="",
|
|
216
|
+
description=(
|
|
217
|
+
"REQUIRED. Brief reasoning for THIS turn: what the last "
|
|
218
|
+
"tool returned + what you'll do differently now. Keep it "
|
|
219
|
+
"concrete, not generic."
|
|
220
|
+
),
|
|
221
|
+
alias='Thought',
|
|
222
|
+
)
|
|
223
|
+
action: str = Field(
|
|
224
|
+
default="",
|
|
225
|
+
description=(
|
|
226
|
+
"REQUIRED. Must be one of the registered tool names shown "
|
|
227
|
+
"in the system prompt, OR the exact literal string "
|
|
228
|
+
"'Final_Answer' (with underscore) when you are ready to "
|
|
229
|
+
"return the completed answer. Never put natural-language "
|
|
230
|
+
"response text here — that goes in action_input."
|
|
231
|
+
),
|
|
232
|
+
alias='action',
|
|
233
|
+
)
|
|
234
|
+
action_input: str | Dict | List = Field(
|
|
235
|
+
default="",
|
|
236
|
+
description=(
|
|
237
|
+
"REQUIRED. When action is a tool name, this is the JSON "
|
|
238
|
+
"object matching that tool's schema (or the single string "
|
|
239
|
+
"it expects). When action is 'Final_Answer', this is the "
|
|
240
|
+
"actual answer TEXT you want the user to see."
|
|
241
|
+
),
|
|
242
|
+
alias='action_input',
|
|
243
|
+
)
|
|
178
244
|
|
|
179
245
|
model_config = ConfigDict(populate_by_name=True)
|
|
180
246
|
|
|
@@ -198,14 +264,35 @@ class ChainOfThought(_ParserMixin, BaseModel):
|
|
|
198
264
|
``instance.action`` / ``instance.action_input`` uniformly across every
|
|
199
265
|
AgentType — no more parser-specific field lookups.
|
|
200
266
|
"""
|
|
201
|
-
Thought: str = Field(
|
|
267
|
+
Thought: str = Field(
|
|
268
|
+
default="",
|
|
269
|
+
description=(
|
|
270
|
+
"REQUIRED. Step-by-step reasoning covering: (a) what the "
|
|
271
|
+
"user asked; (b) what info is needed; (c) which tool fits; "
|
|
272
|
+
"(d) the exact argument shape; (e) whether one call finishes."
|
|
273
|
+
),
|
|
274
|
+
alias='Thought',
|
|
275
|
+
)
|
|
202
276
|
action: str = Field(
|
|
203
|
-
"
|
|
277
|
+
default="",
|
|
278
|
+
description=(
|
|
279
|
+
"REQUIRED. Must be one of the registered tool names shown "
|
|
280
|
+
"in the system prompt, OR the exact literal string "
|
|
281
|
+
"'Final_Answer' (with underscore) when you are ready to "
|
|
282
|
+
"return the completed answer. Never put natural-language "
|
|
283
|
+
"response text here — that goes in action_input."
|
|
284
|
+
),
|
|
204
285
|
validation_alias=AliasChoices("action", "Action"),
|
|
205
286
|
serialization_alias="action",
|
|
206
287
|
)
|
|
207
288
|
action_input: str | Dict | List = Field(
|
|
208
|
-
"
|
|
289
|
+
default="",
|
|
290
|
+
description=(
|
|
291
|
+
"REQUIRED. When action is a tool name, this is the JSON "
|
|
292
|
+
"object matching that tool's schema (or the single string "
|
|
293
|
+
"it expects). When action is 'Final_Answer', this is the "
|
|
294
|
+
"actual answer TEXT you want the user to see."
|
|
295
|
+
),
|
|
209
296
|
validation_alias=AliasChoices("action_input", "Action_Input"),
|
|
210
297
|
serialization_alias="action_input",
|
|
211
298
|
)
|
|
@@ -224,8 +311,27 @@ class ZeroShot(_ParserMixin, BaseModel):
|
|
|
224
311
|
"""
|
|
225
312
|
A Pydantic model representing the Zero-Shot prompt template.
|
|
226
313
|
"""
|
|
227
|
-
action: str = Field(
|
|
228
|
-
|
|
314
|
+
action: str = Field(
|
|
315
|
+
default="",
|
|
316
|
+
description=(
|
|
317
|
+
"REQUIRED. Must be one of the registered tool names shown "
|
|
318
|
+
"in the system prompt, OR the exact literal string "
|
|
319
|
+
"'Final_Answer' (with underscore) when you are ready to "
|
|
320
|
+
"return the completed answer. Never put natural-language "
|
|
321
|
+
"response text here — that goes in action_input."
|
|
322
|
+
),
|
|
323
|
+
alias='action',
|
|
324
|
+
)
|
|
325
|
+
action_input: str | Dict | List = Field(
|
|
326
|
+
default="",
|
|
327
|
+
description=(
|
|
328
|
+
"REQUIRED. When action is a tool name, this is the JSON "
|
|
329
|
+
"object matching that tool's schema (or the single string "
|
|
330
|
+
"it expects). When action is 'Final_Answer', this is the "
|
|
331
|
+
"actual answer TEXT you want the user to see."
|
|
332
|
+
),
|
|
333
|
+
alias='action_input',
|
|
334
|
+
)
|
|
229
335
|
|
|
230
336
|
model_config = ConfigDict(populate_by_name=True)
|
|
231
337
|
|
|
@@ -241,8 +347,27 @@ class FewShot(_ParserMixin, BaseModel):
|
|
|
241
347
|
"""
|
|
242
348
|
A Pydantic model representing the Few-Shot prompt template.
|
|
243
349
|
"""
|
|
244
|
-
action: str = Field(
|
|
245
|
-
|
|
350
|
+
action: str = Field(
|
|
351
|
+
default="",
|
|
352
|
+
description=(
|
|
353
|
+
"REQUIRED. Must be one of the registered tool names shown "
|
|
354
|
+
"in the system prompt, OR the exact literal string "
|
|
355
|
+
"'Final_Answer' (with underscore) when you are ready to "
|
|
356
|
+
"return the completed answer. Never put natural-language "
|
|
357
|
+
"response text here — that goes in action_input."
|
|
358
|
+
),
|
|
359
|
+
alias='action',
|
|
360
|
+
)
|
|
361
|
+
action_input: str | Dict | List = Field(
|
|
362
|
+
default="",
|
|
363
|
+
description=(
|
|
364
|
+
"REQUIRED. When action is a tool name, this is the JSON "
|
|
365
|
+
"object matching that tool's schema (or the single string "
|
|
366
|
+
"it expects). When action is 'Final_Answer', this is the "
|
|
367
|
+
"actual answer TEXT you want the user to see."
|
|
368
|
+
),
|
|
369
|
+
alias='action_input',
|
|
370
|
+
)
|
|
246
371
|
|
|
247
372
|
model_config = ConfigDict(populate_by_name=True)
|
|
248
373
|
|
|
@@ -114,6 +114,34 @@ class CircuitBreaker:
|
|
|
114
114
|
_NO_ARG = object()
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
# Set of case-insensitive strings that should be treated as the model's
|
|
118
|
+
# "I'm done, return this as the final answer" signal. Exact string
|
|
119
|
+
# "Final_Answer" is the canonical form the prompt template teaches;
|
|
120
|
+
# the tolerant variants exist because under use_function_calling=True
|
|
121
|
+
# the schema doesn't constrain `action` to any specific value, so
|
|
122
|
+
# LLMs (especially gpt-4o-mini) emit "Final Answer" (with space) or
|
|
123
|
+
# "final_answer" (lowercase) or "finalanswer" (no separator). Any of
|
|
124
|
+
# those should end the loop, not get routed to Tool_Runner.
|
|
125
|
+
#
|
|
126
|
+
# Deliberately excludes bare "answer" or "final" — those are too
|
|
127
|
+
# likely to collide with a user-defined tool of the same name.
|
|
128
|
+
_TERMINAL_ACTION_VARIANTS = frozenset({
|
|
129
|
+
"final_answer", "finalanswer",
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _is_terminal_action(action: str) -> bool:
|
|
134
|
+
"""True if ``action`` should end the loop with action_input as the
|
|
135
|
+
final answer. Accepts Final_Answer, Final Answer, final_answer,
|
|
136
|
+
FINAL_ANSWER, finalanswer — any capitalization + underscore/space
|
|
137
|
+
variant. Empty string is not terminal (caller handles that as
|
|
138
|
+
'unrecognized action' via the implicit-final guardrail)."""
|
|
139
|
+
if not action or not isinstance(action, str):
|
|
140
|
+
return False
|
|
141
|
+
normalized = action.strip().lower().replace(" ", "_").replace("-", "_")
|
|
142
|
+
return normalized in _TERMINAL_ACTION_VARIANTS
|
|
143
|
+
|
|
144
|
+
|
|
117
145
|
def _build_sandbox_hint(perms: Any) -> Optional[str]:
|
|
118
146
|
"""Generate a short block describing where the runner is allowed to
|
|
119
147
|
operate on disk. Injected into the system prompt when Permissions
|
|
@@ -1019,6 +1047,7 @@ class AgentRunner:
|
|
|
1019
1047
|
permissions: Any = None,
|
|
1020
1048
|
include_denied_tools: bool = False,
|
|
1021
1049
|
system_addendum: Optional[str] = None,
|
|
1050
|
+
strict_tool_dispatch: bool = False,
|
|
1022
1051
|
):
|
|
1023
1052
|
# PEP 8 alias: prefer lowercase `agent=`. Either argument works; passing
|
|
1024
1053
|
# both raises so misuse is loud rather than silently ambiguous.
|
|
@@ -1104,6 +1133,16 @@ class AgentRunner:
|
|
|
1104
1133
|
else:
|
|
1105
1134
|
self.system_addendum = system_addendum
|
|
1106
1135
|
|
|
1136
|
+
# When True: on unknown-tool dispatch, feed the model an error
|
|
1137
|
+
# observation ("that tool doesn't exist; use one of [X,Y] or
|
|
1138
|
+
# Final_Answer") and let the loop iterate rather than
|
|
1139
|
+
# immediately falling to implicit-final. Only applies when the
|
|
1140
|
+
# unknown action looks like a tool identifier (no spaces) —
|
|
1141
|
+
# natural-language actions still go straight to implicit-final
|
|
1142
|
+
# since retrying makes no sense there. Default False keeps
|
|
1143
|
+
# the 3.0.4 behavior.
|
|
1144
|
+
self.strict_tool_dispatch = strict_tool_dispatch
|
|
1145
|
+
|
|
1107
1146
|
self.auto_cache = auto_cache and config.caching_enabled
|
|
1108
1147
|
self.auto_memory = auto_memory and config.memory_enabled
|
|
1109
1148
|
_setup = _ensure_auto_setup() if (self.auto_cache or self.auto_memory) else None
|
|
@@ -1323,53 +1362,38 @@ class AgentRunner:
|
|
|
1323
1362
|
step_description = f"Step {count}: {action} with {action_input}"
|
|
1324
1363
|
steps.append(step_description)
|
|
1325
1364
|
|
|
1326
|
-
|
|
1365
|
+
# Terminal action check — tolerant matching so the loop ends
|
|
1366
|
+
# for the canonical "Final_Answer" AND for the LLM's common
|
|
1367
|
+
# variants ("Final Answer", "final_answer", "finalanswer").
|
|
1368
|
+
# Under use_function_calling=True the schema doesn't
|
|
1369
|
+
# constrain `action` to any spelling, so we accept them all.
|
|
1370
|
+
if _is_terminal_action(action):
|
|
1327
1371
|
final_answer = action_input
|
|
1328
1372
|
yield {"type": "final", "content": final_answer}
|
|
1329
1373
|
break
|
|
1330
1374
|
|
|
1331
|
-
#
|
|
1332
|
-
#
|
|
1333
|
-
#
|
|
1334
|
-
#
|
|
1335
|
-
#
|
|
1336
|
-
#
|
|
1337
|
-
#
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
# registry. Treat the whole turn as an implicit Final_Answer
|
|
1341
|
-
# built from whatever text the model provided (thought +
|
|
1342
|
-
# action + action_input, whichever are present).
|
|
1343
|
-
known_tools = set(self.func) | set(self.args)
|
|
1344
|
-
if action and action != "Final_Answer" and action not in known_tools:
|
|
1345
|
-
parts: List[str] = []
|
|
1346
|
-
if thought:
|
|
1347
|
-
parts.append(str(thought))
|
|
1348
|
-
# action itself may BE the response text
|
|
1349
|
-
if action:
|
|
1350
|
-
parts.append(str(action))
|
|
1351
|
-
# action_input often carries the actual body
|
|
1352
|
-
if action_input and str(action_input).strip():
|
|
1353
|
-
parts.append(str(action_input))
|
|
1354
|
-
final_answer = "\n\n".join(parts) or (
|
|
1355
|
-
"(agent emitted an unrecognized action and no text)"
|
|
1356
|
-
)
|
|
1375
|
+
# Normalize BEFORE the known-tools check — OpenAI's FC path
|
|
1376
|
+
# sometimes returns tool names with a "functions." prefix
|
|
1377
|
+
# ("functions.get_my_scores" instead of "get_my_scores").
|
|
1378
|
+
# Without this, a valid tool call falls through to the
|
|
1379
|
+
# implicit-final guardrail, which then leaks the model's
|
|
1380
|
+
# internal Thought into user-visible text. Rewrite `action`
|
|
1381
|
+
# here so the rest of the loop uses the canonical name.
|
|
1382
|
+
normalized_action = self.registry._normalize_tool_name(action)
|
|
1383
|
+
if normalized_action != action:
|
|
1357
1384
|
if self.verbose:
|
|
1358
1385
|
print(
|
|
1359
|
-
f"\x1B[
|
|
1360
|
-
f"'{action
|
|
1361
|
-
f"treating turn as Final_Answer\x1B[0m"
|
|
1386
|
+
f"\x1B[3;33m[loop] normalized action "
|
|
1387
|
+
f"'{action}' -> '{normalized_action}'\x1B[0m"
|
|
1362
1388
|
)
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
#
|
|
1367
|
-
#
|
|
1368
|
-
#
|
|
1369
|
-
#
|
|
1370
|
-
#
|
|
1371
|
-
# traces). Cheaper than dispatch — we abort before the tool
|
|
1372
|
-
# even runs on the 3rd repeat.
|
|
1389
|
+
action = normalized_action
|
|
1390
|
+
|
|
1391
|
+
# Loop-level spiral check — 3 identical (action, args) in a
|
|
1392
|
+
# row and we bail. Placed BEFORE the known-tools branch so
|
|
1393
|
+
# it catches BOTH kinds of spiral: a valid tool getting
|
|
1394
|
+
# re-dispatched (dup-guard also catches this at the tool
|
|
1395
|
+
# layer), and an unknown tool being retried via strict-mode
|
|
1396
|
+
# (dup-guard would never see those since dispatch skips them).
|
|
1373
1397
|
try:
|
|
1374
1398
|
action_sig = f"{action}::{json.dumps(action_input, sort_keys=True, default=repr)}"
|
|
1375
1399
|
except Exception:
|
|
@@ -1404,6 +1428,102 @@ class AgentRunner:
|
|
|
1404
1428
|
yield {"type": "final", "content": final_answer}
|
|
1405
1429
|
break
|
|
1406
1430
|
|
|
1431
|
+
# Guardrail: the model emitted an `action` value that is
|
|
1432
|
+
# neither a terminal-answer variant NOR a registered tool
|
|
1433
|
+
# (even after normalization). Two scenarios trigger this:
|
|
1434
|
+
# 1. use_function_calling=True + missing schema guidance:
|
|
1435
|
+
# the model stuffs its natural-language response text
|
|
1436
|
+
# into `action` (e.g. "Provide a response to the
|
|
1437
|
+
# athlete."). Route to final so the loop terminates.
|
|
1438
|
+
# 2. action="" (missing field, Pydantic default).
|
|
1439
|
+
# NEVER include `Thought` in the user-facing final — the
|
|
1440
|
+
# Thought is internal reasoning ("The user needs their
|
|
1441
|
+
# scores; I'll fetch them") and leaking it to the user
|
|
1442
|
+
# exposes the agent's introspection. Prefer action_input
|
|
1443
|
+
# (that's what the model wanted the user to see). Only fall
|
|
1444
|
+
# back to `action` when action_input is empty AND action
|
|
1445
|
+
# looks like natural-language text (contains a space, not
|
|
1446
|
+
# just an identifier).
|
|
1447
|
+
known_tools = set(self.func) | set(self.args)
|
|
1448
|
+
if not action or action not in known_tools:
|
|
1449
|
+
# Strict mode: when the unknown `action` looks like a
|
|
1450
|
+
# tool identifier (no spaces, short), assume the model
|
|
1451
|
+
# was TRYING to call a tool and misnamed it. Feed an
|
|
1452
|
+
# error observation back so the model can retry with
|
|
1453
|
+
# a valid name, instead of collapsing to implicit-final
|
|
1454
|
+
# after the very first mistake. The existing dup-guard
|
|
1455
|
+
# + loop-level circuit breaker still bound retries.
|
|
1456
|
+
# Only skipped when: strict mode is off, we've hit
|
|
1457
|
+
# max_iterations, or the action is clearly the model's
|
|
1458
|
+
# response text (contains a space → natural language).
|
|
1459
|
+
looks_like_tool_id = (
|
|
1460
|
+
bool(action) and " " not in action and len(action) < 80
|
|
1461
|
+
)
|
|
1462
|
+
if (
|
|
1463
|
+
self.strict_tool_dispatch
|
|
1464
|
+
and looks_like_tool_id
|
|
1465
|
+
and count < self.max_iterations
|
|
1466
|
+
):
|
|
1467
|
+
available = sorted(known_tools)
|
|
1468
|
+
error_content = (
|
|
1469
|
+
f"[framework] error: '{action}' is not a "
|
|
1470
|
+
f"registered tool. Available tools: {available}. "
|
|
1471
|
+
f"Options: (1) call one of them with the correct "
|
|
1472
|
+
f"argument shape; (2) emit action=\"Final_Answer\" "
|
|
1473
|
+
f"with your final response text in action_input "
|
|
1474
|
+
f"if you're done."
|
|
1475
|
+
)
|
|
1476
|
+
tc_id = getattr(self, "_last_function_call_id", None) if self.use_function_calling else None
|
|
1477
|
+
err_msg = {
|
|
1478
|
+
"role": "tool" if tc_id else "function",
|
|
1479
|
+
"name": "tool_call_error",
|
|
1480
|
+
"content": error_content,
|
|
1481
|
+
}
|
|
1482
|
+
if tc_id:
|
|
1483
|
+
err_msg["tool_call_id"] = tc_id
|
|
1484
|
+
working_history.append(err_msg)
|
|
1485
|
+
yield {
|
|
1486
|
+
"type": "tool_result",
|
|
1487
|
+
"name": action,
|
|
1488
|
+
"result": error_content,
|
|
1489
|
+
"is_error": True,
|
|
1490
|
+
}
|
|
1491
|
+
if self.verbose:
|
|
1492
|
+
print(
|
|
1493
|
+
f"\x1B[1;33m[loop] strict-retry: action "
|
|
1494
|
+
f"'{action[:60]}' unknown; feeding error "
|
|
1495
|
+
f"back so the model can retry\x1B[0m"
|
|
1496
|
+
)
|
|
1497
|
+
self._last_function_call_id = None
|
|
1498
|
+
count += 1
|
|
1499
|
+
continue
|
|
1500
|
+
|
|
1501
|
+
# Non-strict OR strict-can't-help path: implicit-final.
|
|
1502
|
+
# Prefer action_input; fall back to action-as-text ONLY
|
|
1503
|
+
# when it's natural language (has spaces). NEVER leak
|
|
1504
|
+
# Thought — it's internal reasoning that shouldn't reach
|
|
1505
|
+
# a user, especially in third-person-persona apps.
|
|
1506
|
+
if action_input and str(action_input).strip():
|
|
1507
|
+
final_answer = str(action_input)
|
|
1508
|
+
elif action and action.strip() and " " in action:
|
|
1509
|
+
final_answer = str(action)
|
|
1510
|
+
else:
|
|
1511
|
+
final_answer = (
|
|
1512
|
+
"(agent emitted an unrecognized action and no "
|
|
1513
|
+
"answer text — try rephrasing or re-run)"
|
|
1514
|
+
)
|
|
1515
|
+
if self.verbose:
|
|
1516
|
+
preview = (action or "<empty>")[:60]
|
|
1517
|
+
print(
|
|
1518
|
+
f"\x1B[1;33m[loop] implicit-final: action "
|
|
1519
|
+
f"'{preview}' is not a registered tool and not "
|
|
1520
|
+
f"a Final_Answer variant; returning "
|
|
1521
|
+
f"action_input as final (Thought NOT leaked)"
|
|
1522
|
+
f"\x1B[0m"
|
|
1523
|
+
)
|
|
1524
|
+
yield {"type": "final", "content": final_answer}
|
|
1525
|
+
break
|
|
1526
|
+
|
|
1407
1527
|
yield {"type": "tool_call", "name": action, "args": action_input}
|
|
1408
1528
|
if self.verbose:
|
|
1409
1529
|
print(f"\x1B[3;33m[tool] Invoking '{action}' with args: {action_input}\x1B[0m")
|
|
@@ -13,7 +13,7 @@ from agentx_dev.ChatModel import BaseChatModel
|
|
|
13
13
|
from agentx_dev.Agents.Agent import StandardParser, ToolCall, ToolError
|
|
14
14
|
from agentx_dev.Tools import StandardTool, StructuredTool, logger
|
|
15
15
|
from agentx_dev.AsyncTools import AsyncStandardTool, AsyncStructuredTool
|
|
16
|
-
from agentx_dev.Runner.AgentRun import ToolRegistry
|
|
16
|
+
from agentx_dev.Runner.AgentRun import ToolRegistry, _is_terminal_action
|
|
17
17
|
from typing import Dict, Callable, List, Type, Optional, Any, AsyncIterator
|
|
18
18
|
from pydantic import BaseModel, Field
|
|
19
19
|
import asyncio
|
|
@@ -86,6 +86,7 @@ class AsyncAgentRunner:
|
|
|
86
86
|
agent: AgentFormattor | AgentPrompt | str | None = None,
|
|
87
87
|
permissions: Any = None,
|
|
88
88
|
include_denied_tools: bool = False,
|
|
89
|
+
strict_tool_dispatch: bool = False,
|
|
89
90
|
):
|
|
90
91
|
if Agent is not None and agent is not None:
|
|
91
92
|
raise TypeError("AsyncAgentRunner received both 'Agent' and 'agent'; pass only one.")
|
|
@@ -133,6 +134,8 @@ class AsyncAgentRunner:
|
|
|
133
134
|
"are mutually exclusive. Function-calling routes through the "
|
|
134
135
|
"AgentType parser; native binding skips it. Pick one."
|
|
135
136
|
)
|
|
137
|
+
# See AgentRunner.__init__ for the strict_tool_dispatch contract.
|
|
138
|
+
self.strict_tool_dispatch = strict_tool_dispatch
|
|
136
139
|
|
|
137
140
|
self.auto_cache = auto_cache and config.caching_enabled
|
|
138
141
|
self.auto_memory = auto_memory and config.memory_enabled
|
|
@@ -493,7 +496,10 @@ class AsyncAgentRunner:
|
|
|
493
496
|
step_description = f"Step {count}: {action} with {action_input}"
|
|
494
497
|
steps.append(step_description)
|
|
495
498
|
|
|
496
|
-
|
|
499
|
+
# Terminal-action check — tolerant of format variants that
|
|
500
|
+
# LLMs emit under use_function_calling=True where the schema
|
|
501
|
+
# doesn't constrain `action` to a specific spelling.
|
|
502
|
+
if _is_terminal_action(action):
|
|
497
503
|
final_answer = action_input
|
|
498
504
|
break
|
|
499
505
|
|
|
@@ -503,6 +509,82 @@ class AsyncAgentRunner:
|
|
|
503
509
|
elif thought:
|
|
504
510
|
logger.info(f"thought: {thought}")
|
|
505
511
|
|
|
512
|
+
# Strip OpenAI's "functions." prefix before the known-tools
|
|
513
|
+
# check — otherwise a valid tool call falls through to the
|
|
514
|
+
# implicit-final guardrail and leaks Thought as user text.
|
|
515
|
+
normalized_action = self.registry._normalize_tool_name(action)
|
|
516
|
+
if normalized_action != action:
|
|
517
|
+
if self.verbose:
|
|
518
|
+
print(
|
|
519
|
+
f"\x1B[3;33m[loop] normalized action "
|
|
520
|
+
f"'{action}' -> '{normalized_action}'\x1B[0m"
|
|
521
|
+
)
|
|
522
|
+
action = normalized_action
|
|
523
|
+
|
|
524
|
+
# Implicit-final guardrail — see the sync AgentRunner for
|
|
525
|
+
# the full rationale. If strict_tool_dispatch=True AND the
|
|
526
|
+
# unknown action looks like a tool identifier (no spaces)
|
|
527
|
+
# AND we still have iteration budget, feed an error
|
|
528
|
+
# observation back to the model and let it retry.
|
|
529
|
+
known_tools = set(self.registry.sync_std) | set(self.registry.sync_struct) \
|
|
530
|
+
| set(self.registry.async_std) | set(self.registry.async_struct)
|
|
531
|
+
if not action or action not in known_tools:
|
|
532
|
+
looks_like_tool_id = (
|
|
533
|
+
bool(action) and " " not in action and len(action) < 80
|
|
534
|
+
)
|
|
535
|
+
if (
|
|
536
|
+
self.strict_tool_dispatch
|
|
537
|
+
and looks_like_tool_id
|
|
538
|
+
and count < self.max_iterations
|
|
539
|
+
):
|
|
540
|
+
available = sorted(known_tools)
|
|
541
|
+
error_content = (
|
|
542
|
+
f"[framework] error: '{action}' is not a "
|
|
543
|
+
f"registered tool. Available tools: {available}. "
|
|
544
|
+
f"Options: (1) call one of them with the correct "
|
|
545
|
+
f"argument shape; (2) emit action=\"Final_Answer\" "
|
|
546
|
+
f"with your final response text in action_input "
|
|
547
|
+
f"if you're done."
|
|
548
|
+
)
|
|
549
|
+
tc_id = getattr(self, "_last_function_call_id", None) if self.use_function_calling else None
|
|
550
|
+
err_msg = {
|
|
551
|
+
"role": "tool" if tc_id else "function",
|
|
552
|
+
"name": "tool_call_error",
|
|
553
|
+
"content": error_content,
|
|
554
|
+
}
|
|
555
|
+
if tc_id:
|
|
556
|
+
err_msg["tool_call_id"] = tc_id
|
|
557
|
+
working_history.append(err_msg)
|
|
558
|
+
if self.verbose:
|
|
559
|
+
print(
|
|
560
|
+
f"\x1B[1;33m[loop] strict-retry: action "
|
|
561
|
+
f"'{action[:60]}' unknown; feeding error "
|
|
562
|
+
f"back so the model can retry\x1B[0m"
|
|
563
|
+
)
|
|
564
|
+
self._last_function_call_id = None
|
|
565
|
+
count += 1
|
|
566
|
+
continue
|
|
567
|
+
|
|
568
|
+
# Non-strict OR strict-can't-help: implicit-final.
|
|
569
|
+
if action_input and str(action_input).strip():
|
|
570
|
+
final_answer = str(action_input)
|
|
571
|
+
elif action and action.strip() and " " in action:
|
|
572
|
+
final_answer = str(action)
|
|
573
|
+
else:
|
|
574
|
+
final_answer = (
|
|
575
|
+
"(agent emitted an unrecognized action and no "
|
|
576
|
+
"answer text — try rephrasing or re-run)"
|
|
577
|
+
)
|
|
578
|
+
if self.verbose:
|
|
579
|
+
preview = (action or "<empty>")[:60]
|
|
580
|
+
print(
|
|
581
|
+
f"\x1B[1;33m[loop] implicit-final: action "
|
|
582
|
+
f"'{preview}' is not a registered tool and not "
|
|
583
|
+
f"a Final_Answer variant; returning action_input "
|
|
584
|
+
f"as final (Thought NOT leaked)\x1B[0m"
|
|
585
|
+
)
|
|
586
|
+
break
|
|
587
|
+
|
|
506
588
|
if self.verbose:
|
|
507
589
|
print(f"\x1B[3;33m[tool] Invoking '{action}' with args: {action_input}\x1B[0m")
|
|
508
590
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentx-dev
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.5
|
|
4
4
|
Summary: A lightweight LLM agent framework with multi-agent orchestration (Supervisor + dynamic specialist spawning), permission-gated default tools, MCP integration, and prompt-template presets.
|
|
5
5
|
Author-email: Bruce-Arhin Shadrach <brucearhin098@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
|
|
3
3
|
name = "agentx-dev"
|
|
4
|
-
version = "3.0.
|
|
4
|
+
version = "3.0.5"
|
|
5
5
|
description = "A lightweight LLM agent framework with multi-agent orchestration (Supervisor + dynamic specialist spawning), permission-gated default tools, MCP integration, and prompt-template presets."
|
|
6
6
|
authors = [{ name = "Bruce-Arhin Shadrach", email = "brucearhin098@gmail.com" }]
|
|
7
7
|
# Best practice: link to the license file
|
|
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
|
|
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
|