agentx-dev 3.0.3__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.
Files changed (55) hide show
  1. {agentx_dev-3.0.3/agentx_dev.egg-info → agentx_dev-3.0.5}/PKG-INFO +1 -1
  2. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Runner/AgentRun.py +126 -44
  3. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Runner/AsyncAgentRun.py +67 -14
  4. {agentx_dev-3.0.3 → agentx_dev-3.0.5/agentx_dev.egg-info}/PKG-INFO +1 -1
  5. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/pyproject.toml +1 -1
  6. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/AGENTX.md +0 -0
  7. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/LICENSE +0 -0
  8. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/MANIFEST.in +0 -0
  9. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/README.md +0 -0
  10. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Agents/Agent.py +0 -0
  11. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Agents/__init__.py +0 -0
  12. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/AsyncTools.py +0 -0
  13. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/AutoSetup.py +0 -0
  14. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Cache.py +0 -0
  15. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/ChatModel.py +0 -0
  16. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Config.py +0 -0
  17. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/DefaultTools.py +0 -0
  18. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Loader.py +0 -0
  19. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/MCP.py +0 -0
  20. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Memory.py +0 -0
  21. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Observability.py +0 -0
  22. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Planner.py +0 -0
  23. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Runner/__init__.py +0 -0
  24. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Runner/promptTemplate.yaml +0 -0
  25. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Session.py +0 -0
  26. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Streaming.py +0 -0
  27. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Supervisor.py +0 -0
  28. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/Tools.py +0 -0
  29. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/WebTools.py +0 -0
  30. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/__init__.py +0 -0
  31. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/resources/__init__.py +0 -0
  32. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev/resources/promptTemplate.yaml +0 -0
  33. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev.egg-info/SOURCES.txt +0 -0
  34. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev.egg-info/dependency_links.txt +0 -0
  35. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev.egg-info/requires.txt +0 -0
  36. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/agentx_dev.egg-info/top_level.txt +0 -0
  37. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/README.md +0 -0
  38. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/async_example.py +0 -0
  39. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/async_quickstart.py +0 -0
  40. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/auto_features_example.py +0 -0
  41. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/caching_example.py +0 -0
  42. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/chatbot_example.py +0 -0
  43. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/complete_example.py +0 -0
  44. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/concurrent_example.py +0 -0
  45. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/concurrent_tool_example.py +0 -0
  46. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/file_agent_demo.py +0 -0
  47. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/function_calling_demo.py +0 -0
  48. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/mcp_demo.py +0 -0
  49. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/observability_example.py +0 -0
  50. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/orchestration_demo.py +0 -0
  51. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/planner_example.py +0 -0
  52. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/supervisor_codebase_analysis_demo.py +0 -0
  53. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/supervisor_example.py +0 -0
  54. {agentx_dev-3.0.3 → agentx_dev-3.0.5}/examples/sync_quickstart.py +0 -0
  55. {agentx_dev-3.0.3 → 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
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
@@ -1047,6 +1047,7 @@ class AgentRunner:
1047
1047
  permissions: Any = None,
1048
1048
  include_denied_tools: bool = False,
1049
1049
  system_addendum: Optional[str] = None,
1050
+ strict_tool_dispatch: bool = False,
1050
1051
  ):
1051
1052
  # PEP 8 alias: prefer lowercase `agent=`. Either argument works; passing
1052
1053
  # both raises so misuse is loud rather than silently ambiguous.
@@ -1132,6 +1133,16 @@ class AgentRunner:
1132
1133
  else:
1133
1134
  self.system_addendum = system_addendum
1134
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
+
1135
1146
  self.auto_cache = auto_cache and config.caching_enabled
1136
1147
  self.auto_memory = auto_memory and config.memory_enabled
1137
1148
  _setup = _ensure_auto_setup() if (self.auto_cache or self.auto_memory) else None
@@ -1361,53 +1372,28 @@ class AgentRunner:
1361
1372
  yield {"type": "final", "content": final_answer}
1362
1373
  break
1363
1374
 
1364
- # Guardrail: the model emitted an `action` value that is
1365
- # neither a terminal-answer variant NOR a registered tool.
1366
- # Two scenarios trigger this:
1367
- # 1. use_function_calling=True: the parser tool schema
1368
- # accepts any string, so the model stuffs its natural-
1369
- # language closing text into `action` (e.g. "Provide a
1370
- # response to the athlete."). Without this guard, the
1371
- # runner would dispatch it as a tool, get "not found",
1372
- # and the turn dies.
1373
- # 2. action="" (empty). Under FC, the model sometimes
1374
- # omits `action` entirely; Pydantic falls back to the
1375
- # field default. Same failure — empty tool name.
1376
- # Route both to Final_Answer, built from whatever text the
1377
- # model actually provided (thought + action + action_input).
1378
- known_tools = set(self.func) | set(self.args)
1379
- if not action or action not in known_tools:
1380
- parts: List[str] = []
1381
- if thought:
1382
- parts.append(str(thought))
1383
- # Only include `action` in the reply if it looks like
1384
- # real response text (not empty, not a suspicious short
1385
- # sentinel like "answer"). Skip empty.
1386
- if action and action.strip():
1387
- parts.append(str(action))
1388
- if action_input and str(action_input).strip():
1389
- parts.append(str(action_input))
1390
- final_answer = "\n\n".join(parts) or (
1391
- "(agent emitted an unrecognized action and no text)"
1392
- )
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:
1393
1384
  if self.verbose:
1394
- preview = (action or "<empty>")[:60]
1395
1385
  print(
1396
- f"\x1B[1;33m[loop] implicit-final: action "
1397
- f"'{preview}' is not a registered tool and not "
1398
- f"a Final_Answer variant; treating turn as "
1399
- f"Final_Answer\x1B[0m"
1386
+ f"\x1B[3;33m[loop] normalized action "
1387
+ f"'{action}' -> '{normalized_action}'\x1B[0m"
1400
1388
  )
1401
- yield {"type": "final", "content": final_answer}
1402
- break
1403
-
1404
- # Loop-level spiral check same (action, args) as the last
1405
- # turn? Count it. Three in a row and we bail with a synthesized
1406
- # Final_Answer built from whatever real data was gathered
1407
- # before the spiral. Catches BOTH the "refused" spiral and
1408
- # the "tool not found" spiral (gpt-4o-mini did both in
1409
- # traces). Cheaper than dispatch — we abort before the tool
1410
- # 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).
1411
1397
  try:
1412
1398
  action_sig = f"{action}::{json.dumps(action_input, sort_keys=True, default=repr)}"
1413
1399
  except Exception:
@@ -1442,6 +1428,102 @@ class AgentRunner:
1442
1428
  yield {"type": "final", "content": final_answer}
1443
1429
  break
1444
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
+
1445
1527
  yield {"type": "tool_call", "name": action, "args": action_input}
1446
1528
  if self.verbose:
1447
1529
  print(f"\x1B[3;33m[tool] Invoking '{action}' with args: {action_input}\x1B[0m")
@@ -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
@@ -506,29 +509,79 @@ class AsyncAgentRunner:
506
509
  elif thought:
507
510
  logger.info(f"thought: {thought}")
508
511
 
509
- # Implicit-final guardrail: `action` is empty OR not a
510
- # registered tool AND not a Final_Answer variant. Route to
511
- # Final_Answer built from the text the model provided.
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.
512
529
  known_tools = set(self.registry.sync_std) | set(self.registry.sync_struct) \
513
530
  | set(self.registry.async_std) | set(self.registry.async_struct)
514
531
  if not action or action not in known_tools:
515
- parts: List[str] = []
516
- if thought:
517
- parts.append(str(thought))
518
- if action and action.strip():
519
- parts.append(str(action))
520
- if action_input and str(action_input).strip():
521
- parts.append(str(action_input))
522
- final_answer = "\n\n".join(parts) or (
523
- "(agent emitted an unrecognized action and no text)"
532
+ looks_like_tool_id = (
533
+ bool(action) and " " not in action and len(action) < 80
524
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
+ )
525
578
  if self.verbose:
526
579
  preview = (action or "<empty>")[:60]
527
580
  print(
528
581
  f"\x1B[1;33m[loop] implicit-final: action "
529
582
  f"'{preview}' is not a registered tool and not "
530
- f"a Final_Answer variant; treating turn as "
531
- f"Final_Answer\x1B[0m"
583
+ f"a Final_Answer variant; returning action_input "
584
+ f"as final (Thought NOT leaked)\x1B[0m"
532
585
  )
533
586
  break
534
587
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-dev
3
- Version: 3.0.3
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.3"
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