agentnova 0.4.0__tar.gz → 0.4.2__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 (98) hide show
  1. {agentnova-0.4.0 → agentnova-0.4.2}/PKG-INFO +5 -9
  2. {agentnova-0.4.0 → agentnova-0.4.2}/README.md +4 -8
  3. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/__init__.py +42 -1
  4. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/acp_plugin.py +99 -2
  5. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/agent.py +51 -1
  6. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/agent_mode.py +23 -7
  7. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/backends/__init__.py +7 -1
  8. agentnova-0.4.2/agentnova/backends/llama_server.py +540 -0
  9. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/cli.py +150 -198
  10. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/config.py +29 -1
  11. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/error_recovery.py +74 -1
  12. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/prompts.py +39 -0
  13. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/00_basic_agent.py +1 -1
  14. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/01_quick_diagnostic.py +1 -1
  15. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/02_tool_test.py +1 -1
  16. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/03_reasoning_test.py +1 -1
  17. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/04_gsm8k_benchmark.py +1 -1
  18. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/05_common_sense.py +1 -1
  19. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/06_causal_reasoning.py +1 -1
  20. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/07_logical_deduction.py +1 -1
  21. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/08_reading_comprehension.py +1 -1
  22. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/09_general_knowledge.py +1 -1
  23. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/10_implicit_reasoning.py +1 -1
  24. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/11_analogical_reasoning.py +1 -1
  25. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/shared_args.py +137 -1
  26. agentnova-0.4.2/agentnova/skills/codebase-audit/SKILL.md +172 -0
  27. agentnova-0.4.2/agentnova/souls/nova-helper/AGENTS.md +98 -0
  28. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/souls/nova-helper/soul.json +3 -2
  29. agentnova-0.4.2/agentnova/souls/nova-skills/AGENTS.md +112 -0
  30. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/souls/nova-skills/soul.json +3 -2
  31. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/tools/builtins.py +500 -2
  32. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova.egg-info/PKG-INFO +5 -9
  33. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova.egg-info/SOURCES.txt +4 -0
  34. {agentnova-0.4.0 → agentnova-0.4.2}/pyproject.toml +1 -1
  35. {agentnova-0.4.0 → agentnova-0.4.2}/LICENSE +0 -0
  36. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/__main__.py +0 -0
  37. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/backends/base.py +0 -0
  38. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/backends/bitnet.py +0 -0
  39. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/backends/ollama.py +0 -0
  40. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/colors.py +0 -0
  41. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/__init__.py +0 -0
  42. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/args_normal.py +0 -0
  43. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/helpers.py +0 -0
  44. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/math_prompts.py +0 -0
  45. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/memory.py +0 -0
  46. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/model_config.py +0 -0
  47. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/model_family_config.py +0 -0
  48. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/models.py +0 -0
  49. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/openresponses.py +0 -0
  50. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/tool_cache.py +0 -0
  51. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/tool_parse.py +0 -0
  52. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/core/types.py +0 -0
  53. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/examples/__init__.py +0 -0
  54. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/model_discovery.py +0 -0
  55. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/orchestrator.py +0 -0
  56. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/py.typed +0 -0
  57. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/__init__.py +0 -0
  58. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/loader.py +0 -0
  59. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/SKILL.md +0 -0
  60. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/__init__.py +0 -0
  61. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/aggregate_benchmark.py +0 -0
  62. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/generate_report.py +0 -0
  63. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/improve_description.py +0 -0
  64. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/init_skill.py +0 -0
  65. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/package_skill.py +0 -0
  66. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/quick_validate.py +0 -0
  67. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/run_eval.py +0 -0
  68. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/run_loop.py +0 -0
  69. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/security_scan.py +0 -0
  70. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/test_package_skill.py +0 -0
  71. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/test_quick_validate.py +0 -0
  72. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/utils.py +0 -0
  73. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/skill-creator/scripts/validate.py +0 -0
  74. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/skills/test-harness/SKILL.md +0 -0
  75. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/soul/__init__.py +0 -0
  76. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/soul/loader.py +0 -0
  77. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/soul/types.py +0 -0
  78. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/souls/nova-helper/IDENTITY.md +0 -0
  79. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/souls/nova-helper/SOUL.md +0 -0
  80. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/souls/nova-helper/STYLE.md +0 -0
  81. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/souls/nova-skills/IDENTITY.md +0 -0
  82. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/souls/nova-skills/SOUL.md +0 -0
  83. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/souls/nova-skills/STYLE.md +0 -0
  84. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/tools/__init__.py +0 -0
  85. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/tools/registry.py +0 -0
  86. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova/tools/sandboxed_repl.py +0 -0
  87. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova.egg-info/dependency_links.txt +0 -0
  88. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova.egg-info/entry_points.txt +0 -0
  89. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova.egg-info/requires.txt +0 -0
  90. {agentnova-0.4.0 → agentnova-0.4.2}/agentnova.egg-info/top_level.txt +0 -0
  91. {agentnova-0.4.0 → agentnova-0.4.2}/localclaw/__init__.py +0 -0
  92. {agentnova-0.4.0 → agentnova-0.4.2}/localclaw-redirect/localclaw/__init__.py +0 -0
  93. {agentnova-0.4.0 → agentnova-0.4.2}/setup.cfg +0 -0
  94. {agentnova-0.4.0 → agentnova-0.4.2}/tests/test_agent.py +0 -0
  95. {agentnova-0.4.0 → agentnova-0.4.2}/tests/test_builtins.py +0 -0
  96. {agentnova-0.4.0 → agentnova-0.4.2}/tests/test_security.py +0 -0
  97. {agentnova-0.4.0 → agentnova-0.4.2}/tests/test_skills.py +0 -0
  98. {agentnova-0.4.0 → agentnova-0.4.2}/tests/test_spec_compliance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentnova
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: ⚛️ AgentNova - A minimal, hackable agentic framework for local LLM inference
5
5
  Author-email: VTSTech <veritas@vts-tech.org>
6
6
  Maintainer-email: VTSTech <veritas@vts-tech.org>
@@ -32,7 +32,7 @@ Requires-Dist: black>=23.0; extra == "dev"
32
32
  Requires-Dist: ruff>=0.1.0; extra == "dev"
33
33
  Dynamic: license-file
34
34
 
35
- # ⚛️ AgentNova R04.0
35
+ # ⚛️ AgentNova R04.2
36
36
 
37
37
  **Status: Alpha**
38
38
 
@@ -56,9 +56,10 @@ Inspired by the architecture of OpenClaw, rebuilt from scratch for local-first o
56
56
 
57
57
  | Document | Description |
58
58
  |----------|-------------|
59
- | [Architecture.md](https://github.com/VTSTech/AgentNova/blob/main/Architecture.md) | Technical documentation for developers (directory structure, core design, orchestrator modes) |
59
+ | [ARCH.md](https://github.com/VTSTech/AgentNova/blob/main/ARCH.md) | Technical documentation for developers (directory structure, core design, orchestrator modes) |
60
60
  | [CHANGELOG.md](https://github.com/VTSTech/AgentNova/blob/main/CHANGELOG.md) | Version history and release notes (includes LocalClaw history) |
61
61
  | [TESTS.md](https://github.com/VTSTech/AgentNova/blob/main/TESTS.md) | Benchmark results, model recommendations, and testing guide |
62
+ | [CREDITS.md](https://github.com/VTSTech/AgentNova/blob/main/CREDITS.md) | Acknowledges every project, inspiration, API, model creator, and specification that makes AgentNova possible |
62
63
 
63
64
  ## Features
64
65
 
@@ -376,10 +377,5 @@ MIT License - See LICENSE file for details.
376
377
 
377
378
  ## Contributing
378
379
 
379
- Contributions welcome! Please read the contributing guidelines first.
380
+ Contributions welcome!
380
381
 
381
- ## Acknowledgments
382
-
383
- - Built for local inference with [Ollama](https://ollama.ai)
384
- - Optimized for small, efficient models
385
- - Inspired by ReAct and other agentic frameworks
@@ -1,4 +1,4 @@
1
- # ⚛️ AgentNova R04.0
1
+ # ⚛️ AgentNova R04.2
2
2
 
3
3
  **Status: Alpha**
4
4
 
@@ -22,9 +22,10 @@ Inspired by the architecture of OpenClaw, rebuilt from scratch for local-first o
22
22
 
23
23
  | Document | Description |
24
24
  |----------|-------------|
25
- | [Architecture.md](https://github.com/VTSTech/AgentNova/blob/main/Architecture.md) | Technical documentation for developers (directory structure, core design, orchestrator modes) |
25
+ | [ARCH.md](https://github.com/VTSTech/AgentNova/blob/main/ARCH.md) | Technical documentation for developers (directory structure, core design, orchestrator modes) |
26
26
  | [CHANGELOG.md](https://github.com/VTSTech/AgentNova/blob/main/CHANGELOG.md) | Version history and release notes (includes LocalClaw history) |
27
27
  | [TESTS.md](https://github.com/VTSTech/AgentNova/blob/main/TESTS.md) | Benchmark results, model recommendations, and testing guide |
28
+ | [CREDITS.md](https://github.com/VTSTech/AgentNova/blob/main/CREDITS.md) | Acknowledges every project, inspiration, API, model creator, and specification that makes AgentNova possible |
28
29
 
29
30
  ## Features
30
31
 
@@ -342,10 +343,5 @@ MIT License - See LICENSE file for details.
342
343
 
343
344
  ## Contributing
344
345
 
345
- Contributions welcome! Please read the contributing guidelines first.
346
+ Contributions welcome!
346
347
 
347
- ## Acknowledgments
348
-
349
- - Built for local inference with [Ollama](https://ollama.ai)
350
- - Optimized for small, efficient models
351
- - Inspired by ReAct and other agentic frameworks
@@ -28,10 +28,51 @@ Example Usage:
28
28
  agent = Agent(model="qwen2.5:0.5b", soul="/path/to/soul/package")
29
29
  """
30
30
 
31
- __version__ = "0.4.0"
31
+ __version__ = "0.4.2"
32
32
  __author__ = "VTSTech"
33
33
  __status__ = "Alpha"
34
34
 
35
+
36
+ def _get_git_short_hash() -> str:
37
+ """
38
+ Get the short git commit hash of the repository.
39
+
40
+ Works when running from source (has .git directory).
41
+ Returns empty string when installed via pip or git is unavailable.
42
+ """
43
+ import subprocess
44
+ try:
45
+ # Try to find the repo root from this file's location
46
+ # Walk up from agentnova/ looking for .git
47
+ import os
48
+ check_dir = os.path.dirname(os.path.abspath(__file__))
49
+ for _ in range(5): # don't walk up more than 5 levels
50
+ if os.path.isdir(os.path.join(check_dir, ".git")):
51
+ # We're inside a git repo — get the short hash
52
+ result = subprocess.run(
53
+ ["git", "rev-parse", "--short", "HEAD"],
54
+ cwd=check_dir,
55
+ capture_output=True,
56
+ text=True,
57
+ timeout=5,
58
+ )
59
+ if result.returncode == 0:
60
+ return result.stdout.strip()
61
+ break
62
+ parent = os.path.dirname(check_dir)
63
+ if parent == check_dir:
64
+ break
65
+ check_dir = parent
66
+ except Exception:
67
+ pass
68
+ return ""
69
+
70
+
71
+ # Append git commit hash to version if available
72
+ _git_hash = _get_git_short_hash()
73
+ if _git_hash:
74
+ __version__ = f"{__version__}-{_git_hash}"
75
+
35
76
  from .agent import Agent
36
77
  from .agent_mode import AgentMode, AgentState, TaskPlan
37
78
  from .orchestrator import Orchestrator, AgentCard
@@ -4,7 +4,7 @@
4
4
  This plugin connects AgentNova agents to an ACP (Agent Control Panel) server,
5
5
  enabling real-time monitoring, token tracking, and STOP/Resume control.
6
6
 
7
- ACP Specification: v1.0.5 (A2A Compliance)
7
+ ACP Specification: v1.0.6 (A2A Compliance)
8
8
  Compliance: Full (mandatory requirements, hints, orphan handling, nudge support, batch ops, shutdown, A2A, JSON-RPC 2.0, primary agent nudge delivery)
9
9
 
10
10
  Features:
@@ -25,6 +25,11 @@ Features:
25
25
  - contextId tracking for session continuity (1.0.4)
26
26
  - primary_agent in /api/whoami response (1.0.5)
27
27
  - Nudges delivered only to primary agent (1.0.5)
28
+ - CSRF protection optional, disabled by default (1.0.6)
29
+ - CORS preflight support (1.0.6)
30
+ - GET /api/nudge polling support (1.0.6)
31
+ - POST /api/todos/toggle (1.0.6)
32
+ - Bootstrap activity auto-completed (no orphans)
28
33
 
29
34
  Usage:
30
35
  from agentnova import Agent
@@ -246,6 +251,9 @@ class ACPPlugin:
246
251
  self._capabilities_override = capabilities # Store for later, derive from tools if not provided
247
252
  self.endpoint = endpoint # v1.0.4: A2A endpoint
248
253
 
254
+ # 1.0.6: CSRF disabled by default per spec §4.2
255
+ self._csrf_enabled: bool | None = None # Unknown until first request
256
+
249
257
  self._csrf_token: str | None = None
250
258
  self._csrf_expiry: float = 0
251
259
  self._current_activity_id: str | None = None
@@ -339,7 +347,8 @@ class ACPPlugin:
339
347
  "Authorization": f"Basic {self.auth}",
340
348
  "Content-Type": "application/json",
341
349
  }
342
- if method == "POST" and self._csrf_token:
350
+ # 1.0.6: Only send CSRF token if enabled (disabled by default per spec §4.2)
351
+ if method == "POST" and self._csrf_token and self._csrf_enabled:
343
352
  headers["X-CSRF-Token"] = self._csrf_token
344
353
 
345
354
  url = f"{self.base_url}{endpoint}"
@@ -377,6 +386,9 @@ class ACPPlugin:
377
386
  now = time.time()
378
387
  if self._csrf_token is None or (now - self._csrf_expiry) > 3000:
379
388
  resp = self._request("/api/csrf-token")
389
+ # 1.0.6: CSRF is optional and disabled by default per spec §4.2
390
+ # Track the enabled state so we can skip header when not needed
391
+ self._csrf_enabled = resp.get("csrf_enabled", False)
380
392
  token = resp.get("csrf_token")
381
393
  # If CSRF is disabled, use empty string sentinel so we don't re-fetch
382
394
  self._csrf_token = token if token else ""
@@ -889,11 +901,35 @@ class ACPPlugin:
889
901
  "metadata": metadata,
890
902
  })
891
903
 
904
+ def check_nudge(self) -> dict:
905
+ """
906
+ Check if a nudge is pending without logging a new activity.
907
+
908
+ 1.0.6: GET /api/nudge returns {nudge, has_pending} for polling.
909
+ Useful for checking nudge status between steps without
910
+ triggering a full /api/action call.
911
+
912
+ Returns
913
+ -------
914
+ dict
915
+ Response with 'nudge' and 'has_pending' fields
916
+
917
+ Example
918
+ -------
919
+ >>> result = acp.check_nudge()
920
+ >>> if result.get("has_pending"):
921
+ ... print(f"Pending nudge: {result['nudge'].get('message')}")
922
+ >>> acp.ack_nudge()
923
+ """
924
+ return self._request("/api/nudge")
925
+
892
926
  def ack_nudge(self) -> dict:
893
927
  """
894
928
  Acknowledge a pending nudge.
895
929
 
896
930
  Call this after processing a nudge that had requires_ack=true.
931
+
932
+ 1.0.6: Also clears the nudge by setting it to None server-side.
897
933
  """
898
934
  resp = self._request("/api/nudge/ack", "POST", {})
899
935
  self._pending_nudge = None
@@ -1636,6 +1672,67 @@ class ACPPlugin:
1636
1672
  resp = self._request("/api/todos")
1637
1673
  return resp.get("todos", [])
1638
1674
 
1675
+ def add_todo(self, content: str, priority: str = "medium", status: str = "pending") -> dict:
1676
+ """
1677
+ Add a single TODO item (1.0.6).
1678
+
1679
+ Unlike sync_todos() which replaces the entire list, this adds one item.
1680
+
1681
+ Parameters
1682
+ ----------
1683
+ content : str
1684
+ Task description
1685
+ priority : str
1686
+ "high", "medium" (default), or "low"
1687
+ status : str
1688
+ "pending" (default), "in_progress", or "completed"
1689
+
1690
+ Returns
1691
+ -------
1692
+ dict
1693
+ API response
1694
+
1695
+ Example
1696
+ -------
1697
+ >>> acp.add_todo("Fix the login bug", priority="high")
1698
+ >>> acp.add_todo("Run tests", priority="low", status="in_progress")
1699
+ """
1700
+ metadata = self._build_metadata()
1701
+ return self._request("/api/todos/add", "POST", {
1702
+ "todo": {"content": content, "priority": priority, "status": status},
1703
+ "agent_name": self.agent_name,
1704
+ })
1705
+
1706
+ def toggle_todo(self, todo_id: str) -> dict:
1707
+ """
1708
+ Toggle a TODO item's status between pending and completed (1.0.6).
1709
+
1710
+ Uses POST /api/todos/toggle which flips a TODO between
1711
+ pending ↔ completed in a single call.
1712
+
1713
+ Parameters
1714
+ ----------
1715
+ todo_id : str
1716
+ The TODO item ID to toggle
1717
+
1718
+ Returns
1719
+ -------
1720
+ dict
1721
+ Response with 'todo' and 'toggled' fields
1722
+ """
1723
+ return self._request("/api/todos/toggle", "POST", {
1724
+ "id": todo_id,
1725
+ })
1726
+
1727
+ def clear_completed_todos(self) -> dict:
1728
+ """
1729
+ Clear completed TODOs (1.0.6).
1730
+
1731
+ Removes all TODOs with status="completed" from the list.
1732
+ """
1733
+ return self._request("/api/todos/clear", "POST", {})
1734
+
1735
+
1639
1736
  def bootstrap(self, claim_primary: bool = True) -> dict:
1640
1737
  """
1641
1738
  Bootstrap ACP session - check status, establish identity, claim primary.
@@ -30,9 +30,12 @@ from .core.tool_parse import ToolParser
30
30
  from .core.error_recovery import (
31
31
  ErrorRecoveryTracker,
32
32
  build_enhanced_observation,
33
+ build_retry_context,
33
34
  is_error_result,
34
35
  DEFAULT_MAX_CONSECUTIVE_FAILURES,
35
36
  DEFAULT_MAX_TOTAL_FAILURES,
37
+ DEFAULT_MAX_TOOL_RETRIES,
38
+ DEFAULT_RETRY_ON_ERROR,
36
39
  )
37
40
  from .core.openresponses import (
38
41
  Response, ResponseStatus, ItemStatus,
@@ -120,6 +123,9 @@ class Agent:
120
123
  allowed_tools: list[str] | None = None,
121
124
  # Skills injection
122
125
  skills_prompt: str | None = None,
126
+ # Retry-with-error-feedback
127
+ retry_on_error: bool = DEFAULT_RETRY_ON_ERROR,
128
+ max_tool_retries: int = DEFAULT_MAX_TOOL_RETRIES,
123
129
  **kwargs,
124
130
  ):
125
131
  """
@@ -142,6 +148,8 @@ class Agent:
142
148
  tool_choice: Control tool invocation ("auto", "required", "none", or specific tool name)
143
149
  allowed_tools: List of tools the model is allowed to invoke (subset of tools)
144
150
  skills_prompt: Optional skill instructions to append to the system prompt
151
+ retry_on_error: Whether to retry failed tool calls with error feedback (default: True)
152
+ max_tool_retries: Maximum retries per tool call failure (default: 2)
145
153
  **kwargs: Additional configuration
146
154
  """
147
155
  self.model = model
@@ -159,6 +167,17 @@ class Agent:
159
167
  self._top_p = top_p
160
168
  self._num_predict = num_predict
161
169
 
170
+ # Retry-with-error-feedback
171
+ self._retry_on_error = retry_on_error
172
+ self._max_tool_retries = max_tool_retries
173
+
174
+ # Dangerous tool confirmation callback.
175
+ # When set, any tool with dangerous=True must be approved by
176
+ # this callback before execution. The callback receives
177
+ # (tool_name, args) and returns True (allow) or False (deny).
178
+ # If not set, dangerous tools execute without confirmation.
179
+ self._confirm_dangerous = kwargs.pop("confirm_dangerous", None)
180
+
162
181
  # Initialize backend
163
182
  if backend is None:
164
183
  self.backend = get_default_backend()
@@ -320,6 +339,9 @@ class Agent:
320
339
  max_consecutive_failures=DEFAULT_MAX_CONSECUTIVE_FAILURES,
321
340
  max_total_failures=DEFAULT_MAX_TOTAL_FAILURES,
322
341
  )
342
+
343
+ if self.debug:
344
+ print(f"[Agent] Retry on error: {self._retry_on_error}, max_tool_retries: {self._max_tool_retries}")
323
345
 
324
346
  @property
325
347
  def _is_comp_mode(self) -> bool:
@@ -649,6 +671,18 @@ Final Answer: <the answer>
649
671
  name=tool_name,
650
672
  content=str(result),
651
673
  )
674
+ # Native tool calls also get retry context on error
675
+ if is_error and self._retry_on_error:
676
+ retry_msg = build_retry_context(
677
+ tool_name=tool_name,
678
+ tool_args=tool_args,
679
+ tracker=self._error_tracker,
680
+ max_tool_retries=self._max_tool_retries,
681
+ )
682
+ if retry_msg:
683
+ if self.debug:
684
+ print(f" [Retry Context] Adding retry hint for native tool call: {tool_name}")
685
+ self.memory.add("user", retry_msg)
652
686
  else:
653
687
  # Use error recovery module for enhanced observation
654
688
  observation_msg = build_enhanced_observation(
@@ -656,7 +690,9 @@ Final Answer: <the answer>
656
690
  result=str(result),
657
691
  tracker=self._error_tracker,
658
692
  available_tools=self.tools.names(),
659
- is_error=is_error
693
+ is_error=is_error,
694
+ retry_on_error=self._retry_on_error,
695
+ tool_args=tool_args,
660
696
  )
661
697
 
662
698
  # Update expecting_final_answer flag
@@ -1091,6 +1127,8 @@ Final Answer: <the answer>
1091
1127
  tracker=self._error_tracker,
1092
1128
  available_tools=self.tools.names(),
1093
1129
  is_error=is_error,
1130
+ retry_on_error=self._retry_on_error,
1131
+ tool_args=tool_args,
1094
1132
  )
1095
1133
 
1096
1134
  if is_error:
@@ -1356,12 +1394,24 @@ Final Answer: <the answer>
1356
1394
 
1357
1395
  OpenResponses: Tool execution is straightforward - no synthesis.
1358
1396
  Arguments must come from the model.
1397
+
1398
+ If the tool is marked dangerous=True and a confirm_dangerous
1399
+ callback is set, the callback is invoked before execution.
1400
+ Returning False from the callback blocks the tool call.
1359
1401
  """
1360
1402
  tool = self.tools.get(name)
1361
1403
 
1362
1404
  if tool is None:
1363
1405
  return f"Error: Unknown tool '{name}'. Available tools: {self.tools.names()}"
1364
1406
 
1407
+ # Confirmation gate for dangerous tools
1408
+ if getattr(tool, 'dangerous', False) and self._confirm_dangerous is not None:
1409
+ if not self._confirm_dangerous(name, args):
1410
+ return (
1411
+ f"Tool '{name}' was blocked by user confirmation. "
1412
+ f"The tool is marked as dangerous and was not approved."
1413
+ )
1414
+
1365
1415
  # Normalize arguments with tool-specific aliases
1366
1416
  expected_params = [p.name for p in tool.params]
1367
1417
 
@@ -611,9 +611,20 @@ Example: [{{"description": "Step 1"}}, {{"description": "Step 2"}}]"""
611
611
  })
612
612
 
613
613
  try:
614
- # Simple prompt - let the agent use its existing context
615
- # This works better with Modelfile system prompts
616
- step_prompt = step.description
614
+ # Build step prompt with plan context for multi-step awareness.
615
+ # The agent reuses the same memory instance across steps, so it
616
+ # already has access to previous step results. We add a brief
617
+ # context header so it knows where it is in the overall plan.
618
+ if self.plan and self.plan.total_steps > 1:
619
+ step_idx = self.plan.current_step_index + 1
620
+ step_count = self.plan.total_steps
621
+ step_prompt = (
622
+ f"[Step {step_idx} of {step_count}] "
623
+ f"Goal: {self.plan.goal}\n\n"
624
+ f"{step.description}"
625
+ )
626
+ else:
627
+ step_prompt = step.description
617
628
 
618
629
  # Run the agent with the step prompt
619
630
  if self.verbose:
@@ -746,11 +757,16 @@ Example: [{{"description": "Step 1"}}, {{"description": "Step 2"}}]"""
746
757
  def _inject_context(self, message: str):
747
758
  """
748
759
  Inject a context message into the agent's memory.
749
- Used to provide awareness prompts during long executions.
760
+
761
+ Used to provide awareness prompts during long executions so the
762
+ agent stays focused on the goal and knows where it is in the plan.
763
+ The message is added as a 'user' role message so the model
764
+ processes it on the next step.
750
765
  """
751
- # This would add a system message to the agent's memory
752
- # Implementation depends on Agent class internals
753
- pass
766
+ try:
767
+ self.agent.memory.add("user", message)
768
+ except Exception:
769
+ pass # Non-fatal — context injection is best-effort
754
770
 
755
771
 
756
772
  # ------------------------------------------------------------------ #
@@ -8,7 +8,8 @@ Written by VTSTech — https://www.vts-tech.org
8
8
  from .base import BaseBackend
9
9
  from .ollama import OllamaBackend
10
10
  from .bitnet import BitNetBackend
11
- from ..config import AGENTNOVA_BACKEND, OLLAMA_BASE_URL, BITNET_BASE_URL
11
+ from .llama_server import LlamaServerBackend
12
+ from ..config import AGENTNOVA_BACKEND, OLLAMA_BASE_URL, BITNET_BASE_URL, LLAMA_SERVER_BASE_URL
12
13
  from ..core.types import ApiMode
13
14
 
14
15
 
@@ -16,6 +17,8 @@ from ..core.types import ApiMode
16
17
  _BACKENDS: dict[str, type[BaseBackend]] = {
17
18
  "ollama": OllamaBackend,
18
19
  "bitnet": BitNetBackend,
20
+ "llama-server": LlamaServerBackend,
21
+ "llama_server": LlamaServerBackend, # alias
19
22
  }
20
23
 
21
24
 
@@ -47,6 +50,8 @@ def get_backend(name: str, timeout: int | None = None, api_mode: ApiMode | str |
47
50
  kwargs["base_url"] = OLLAMA_BASE_URL
48
51
  elif name_lower == "bitnet":
49
52
  kwargs["base_url"] = BITNET_BASE_URL
53
+ elif name_lower in ("llama-server", "llama_server"):
54
+ kwargs["base_url"] = LLAMA_SERVER_BASE_URL
50
55
 
51
56
  # Create BackendConfig with timeout if specified
52
57
  if "config" not in kwargs and timeout is not None:
@@ -92,6 +97,7 @@ __all__ = [
92
97
  "BaseBackend",
93
98
  "OllamaBackend",
94
99
  "BitNetBackend",
100
+ "LlamaServerBackend",
95
101
  "get_backend",
96
102
  "get_default_backend",
97
103
  "register_backend",