agentnode-sdk 0.2.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/PKG-INFO +29 -9
  2. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/README.md +28 -8
  3. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/__init__.py +13 -1
  4. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/client.py +272 -0
  5. agentnode_sdk-0.4.0/agentnode_sdk/detect.py +200 -0
  6. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/installer.py +7 -0
  7. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/models.py +52 -0
  8. agentnode_sdk-0.4.0/agentnode_sdk/runner.py +298 -0
  9. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/pyproject.toml +1 -1
  10. agentnode_sdk-0.4.0/tests/test_auto_upgrade_policy.py +119 -0
  11. agentnode_sdk-0.4.0/tests/test_detect.py +210 -0
  12. agentnode_sdk-0.4.0/tests/test_detect_and_install.py +170 -0
  13. agentnode_sdk-0.4.0/tests/test_runner.py +315 -0
  14. agentnode_sdk-0.4.0/tests/test_smart.py +345 -0
  15. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/.env.example +0 -0
  16. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/.gitignore +0 -0
  17. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode.lock +0 -0
  18. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/async_client.py +0 -0
  19. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/exceptions.py +0 -0
  20. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/tests/__init__.py +0 -0
  21. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/tests/test_async_client.py +0 -0
  22. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/tests/test_client.py +0 -0
  23. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/tests/test_edge_cases.py +0 -0
  24. {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/tests/test_v02.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentnode-sdk
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Python SDK for AgentNode — the open upgrade and discovery infrastructure for AI agents.
5
5
  Project-URL: Homepage, https://agentnode.net
6
6
  Project-URL: Repository, https://github.com/agentnode-ai/agentnode
@@ -34,8 +34,7 @@ pip install agentnode-sdk
34
34
  ## Quick Start
35
35
 
36
36
  ```python
37
- from agentnode_sdk import AgentNodeClient
38
- from agentnode_sdk.installer import load_tool
37
+ from agentnode_sdk import AgentNodeClient, run_tool
39
38
 
40
39
  client = AgentNodeClient(api_key="ank_...")
41
40
 
@@ -50,13 +49,14 @@ resolved = client.resolve(
50
49
  framework="langchain",
51
50
  )
52
51
 
53
- # v0.2: Load specific tools from multi-tool packs
54
- describe = load_tool("csv-analyzer-pack", tool_name="describe")
55
- result = describe({"file_path": "data.csv"})
52
+ # v0.3: Run tools with trust-aware isolation
53
+ # trusted/curated → direct (in-process), verified/unverified → subprocess
54
+ result = run_tool("pdf-reader-pack", file_path="report.pdf")
55
+ print(result.result) # tool output
56
+ print(result.mode_used) # "direct" or "subprocess"
56
57
 
57
- # Single-tool packs — no tool_name needed
58
- extract = load_tool("pdf-reader-pack")
59
- pdf = extract({"file_path": "report.pdf"})
58
+ # Multi-tool packs — specify tool_name
59
+ result = run_tool("csv-analyzer-pack", tool_name="describe", file_path="data.csv")
60
60
  ```
61
61
 
62
62
  ## API Reference
@@ -72,8 +72,28 @@ The main client with typed return models.
72
72
  | `get_package(slug)` | Get package details |
73
73
  | `get_install_metadata(slug)` | Get install info (artifact, permissions, deps) |
74
74
  | `download(slug)` | Track download and get artifact URL |
75
+ | `run_tool(slug, tool_name=, mode=, timeout=, **kwargs)` | Run a tool with optional subprocess isolation (v0.3) |
75
76
  | `load_tool(slug, tool_name=)` | Load a tool function from an installed pack (v0.2) |
76
77
 
78
+ ### `run_tool()` (standalone)
79
+
80
+ Top-level function for running tools with trust-aware execution mode.
81
+
82
+ ```python
83
+ from agentnode_sdk import run_tool
84
+
85
+ result = run_tool("pdf-reader-pack", mode="auto", file_path="report.pdf")
86
+ # result.success, result.result, result.error, result.mode_used, result.duration_ms
87
+ ```
88
+
89
+ | Param | Type | Default | Description |
90
+ |-------|------|---------|-------------|
91
+ | `slug` | str | required | Package slug |
92
+ | `tool_name` | str \| None | None | Tool name for multi-tool packs |
93
+ | `mode` | str | `"auto"` | `"direct"`, `"subprocess"`, or `"auto"` |
94
+ | `timeout` | float | 30.0 | Subprocess timeout in seconds |
95
+ | `**kwargs` | Any | — | Arguments forwarded to the tool function |
96
+
77
97
  ### `AgentNode`
78
98
 
79
99
  Lightweight client returning raw dicts.
@@ -11,8 +11,7 @@ pip install agentnode-sdk
11
11
  ## Quick Start
12
12
 
13
13
  ```python
14
- from agentnode_sdk import AgentNodeClient
15
- from agentnode_sdk.installer import load_tool
14
+ from agentnode_sdk import AgentNodeClient, run_tool
16
15
 
17
16
  client = AgentNodeClient(api_key="ank_...")
18
17
 
@@ -27,13 +26,14 @@ resolved = client.resolve(
27
26
  framework="langchain",
28
27
  )
29
28
 
30
- # v0.2: Load specific tools from multi-tool packs
31
- describe = load_tool("csv-analyzer-pack", tool_name="describe")
32
- result = describe({"file_path": "data.csv"})
29
+ # v0.3: Run tools with trust-aware isolation
30
+ # trusted/curated → direct (in-process), verified/unverified → subprocess
31
+ result = run_tool("pdf-reader-pack", file_path="report.pdf")
32
+ print(result.result) # tool output
33
+ print(result.mode_used) # "direct" or "subprocess"
33
34
 
34
- # Single-tool packs — no tool_name needed
35
- extract = load_tool("pdf-reader-pack")
36
- pdf = extract({"file_path": "report.pdf"})
35
+ # Multi-tool packs — specify tool_name
36
+ result = run_tool("csv-analyzer-pack", tool_name="describe", file_path="data.csv")
37
37
  ```
38
38
 
39
39
  ## API Reference
@@ -49,8 +49,28 @@ The main client with typed return models.
49
49
  | `get_package(slug)` | Get package details |
50
50
  | `get_install_metadata(slug)` | Get install info (artifact, permissions, deps) |
51
51
  | `download(slug)` | Track download and get artifact URL |
52
+ | `run_tool(slug, tool_name=, mode=, timeout=, **kwargs)` | Run a tool with optional subprocess isolation (v0.3) |
52
53
  | `load_tool(slug, tool_name=)` | Load a tool function from an installed pack (v0.2) |
53
54
 
55
+ ### `run_tool()` (standalone)
56
+
57
+ Top-level function for running tools with trust-aware execution mode.
58
+
59
+ ```python
60
+ from agentnode_sdk import run_tool
61
+
62
+ result = run_tool("pdf-reader-pack", mode="auto", file_path="report.pdf")
63
+ # result.success, result.result, result.error, result.mode_used, result.duration_ms
64
+ ```
65
+
66
+ | Param | Type | Default | Description |
67
+ |-------|------|---------|-------------|
68
+ | `slug` | str | required | Package slug |
69
+ | `tool_name` | str \| None | None | Tool name for multi-tool packs |
70
+ | `mode` | str | `"auto"` | `"direct"`, `"subprocess"`, or `"auto"` |
71
+ | `timeout` | float | 30.0 | Subprocess timeout in seconds |
72
+ | `**kwargs` | Any | — | Arguments forwarded to the tool function |
73
+
54
74
  ### `AgentNode`
55
75
 
56
76
  Lightweight client returning raw dicts.
@@ -2,6 +2,7 @@
2
2
 
3
3
  from agentnode_sdk.async_client import AsyncAgentNode
4
4
  from agentnode_sdk.client import AgentNode, AgentNodeClient
5
+ from agentnode_sdk.detect import detect_gap
5
6
  from agentnode_sdk.exceptions import (
6
7
  AgentNodeError,
7
8
  AgentNodeToolError,
@@ -13,26 +14,33 @@ from agentnode_sdk.exceptions import (
13
14
  from agentnode_sdk.installer import load_tool
14
15
  from agentnode_sdk.models import (
15
16
  CanInstallResult,
17
+ DetectAndInstallResult,
18
+ DetectedGap,
16
19
  InstallMetadata,
17
20
  InstallResult,
18
21
  PackageDetail,
19
22
  ResolvedPackage,
20
23
  ResolveResult,
24
+ RunToolResult,
21
25
  SearchHit,
22
26
  SearchResult,
27
+ SmartRunResult,
23
28
  )
29
+ from agentnode_sdk.runner import run_tool
24
30
 
25
31
  # Convenience aliases
26
32
  Client = AgentNodeClient
27
33
  ToolError = AgentNodeToolError
28
34
 
29
- __version__ = "0.2.0"
35
+ __version__ = "0.4.0"
30
36
  __all__ = [
31
37
  "AgentNode",
32
38
  "AsyncAgentNode",
33
39
  "AgentNodeClient",
34
40
  "Client",
41
+ "detect_gap",
35
42
  "load_tool",
43
+ "run_tool",
36
44
  "AgentNodeError",
37
45
  "AgentNodeToolError",
38
46
  "ToolError",
@@ -48,4 +56,8 @@ __all__ = [
48
56
  "InstallMetadata",
49
57
  "InstallResult",
50
58
  "CanInstallResult",
59
+ "RunToolResult",
60
+ "DetectedGap",
61
+ "DetectAndInstallResult",
62
+ "SmartRunResult",
51
63
  ]
@@ -1,6 +1,10 @@
1
1
  """AgentNode API client. Spec §14."""
2
2
  from __future__ import annotations
3
3
 
4
+ import time
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
4
8
  import httpx
5
9
 
6
10
  from agentnode_sdk.exceptions import (
@@ -11,10 +15,12 @@ from agentnode_sdk.exceptions import (
11
15
  ValidationError,
12
16
  )
13
17
  from agentnode_sdk.installer import install_package, load_tool as _load_tool
18
+ from agentnode_sdk.detect import detect_gap
14
19
  from agentnode_sdk.models import (
15
20
  ArtifactInfo,
16
21
  CanInstallResult,
17
22
  CapabilityInfo,
23
+ DetectAndInstallResult,
18
24
  DependencyInfo,
19
25
  InstallMetadata,
20
26
  InstallResult,
@@ -22,9 +28,11 @@ from agentnode_sdk.models import (
22
28
  PermissionsInfo,
23
29
  ResolvedPackage,
24
30
  ResolveResult,
31
+ RunToolResult,
25
32
  ScoreBreakdown,
26
33
  SearchHit,
27
34
  SearchResult,
35
+ SmartRunResult,
28
36
  )
29
37
 
30
38
  DEFAULT_BASE_URL = "https://api.agentnode.net/v1"
@@ -41,6 +49,40 @@ ERROR_CLASS_MAP = {
41
49
  }
42
50
 
43
51
 
52
+ def _resolve_auto_upgrade_policy(
53
+ policy: str | None,
54
+ *,
55
+ auto_install: bool,
56
+ require_verified: bool,
57
+ require_trusted: bool,
58
+ allow_low_confidence: bool,
59
+ ) -> tuple[bool, bool, bool, bool]:
60
+ """Resolve named policy to concrete parameters. Policy overrides individual params."""
61
+ if policy is None:
62
+ return (auto_install, require_verified, require_trusted, allow_low_confidence)
63
+ p = policy.lower()
64
+ if p == "off":
65
+ return (False, True, False, False)
66
+ if p == "safe":
67
+ return (True, True, False, False)
68
+ if p == "strict":
69
+ return (True, False, True, False)
70
+ raise ValueError("auto_upgrade_policy must be 'off', 'safe', or 'strict'")
71
+
72
+
73
+ def _permissions_to_dict(perms: PermissionsInfo | None) -> dict | None:
74
+ """Convert PermissionsInfo to a plain dict for lockfile storage."""
75
+ if perms is None:
76
+ return None
77
+ return {
78
+ "network_level": perms.network_level,
79
+ "filesystem_level": perms.filesystem_level,
80
+ "code_execution_level": perms.code_execution_level,
81
+ "data_access_level": perms.data_access_level,
82
+ "user_approval_level": perms.user_approval_level,
83
+ }
84
+
85
+
44
86
  class AgentNode:
45
87
  """Spec-compliant SDK client (§14.3). Returns plain dicts."""
46
88
 
@@ -479,6 +521,8 @@ class AgentNodeClient:
479
521
  capability_ids=cap_ids,
480
522
  tools=tools,
481
523
  verbose=verbose,
524
+ trust_level=trust_level,
525
+ permissions=_permissions_to_dict(meta.permissions),
482
526
  )
483
527
 
484
528
  return InstallResult(
@@ -608,6 +652,28 @@ class AgentNodeClient:
608
652
  """
609
653
  return _load_tool(slug, tool_name=tool_name)
610
654
 
655
+ def run_tool(
656
+ self,
657
+ slug: str,
658
+ tool_name: str | None = None,
659
+ *,
660
+ mode: str = "auto",
661
+ timeout: float = 30.0,
662
+ **kwargs,
663
+ ) -> RunToolResult:
664
+ """Run an installed tool with optional process isolation.
665
+
666
+ Args:
667
+ slug: Package slug.
668
+ tool_name: Tool name for multi-tool packs.
669
+ mode: ``"direct"``, ``"subprocess"``, or ``"auto"``.
670
+ timeout: Timeout in seconds (subprocess mode only).
671
+ **kwargs: Arguments forwarded to the tool function.
672
+ """
673
+ from agentnode_sdk.runner import run_tool as _run_tool
674
+
675
+ return _run_tool(slug, tool_name, mode=mode, timeout=timeout, **kwargs)
676
+
611
677
  def resolve_and_install(
612
678
  self,
613
679
  capabilities: list[str],
@@ -666,3 +732,209 @@ class AgentNodeClient:
666
732
  )
667
733
 
668
734
  return self.install(best.slug, verbose=verbose)
735
+
736
+ def detect_and_install(
737
+ self,
738
+ error: BaseException,
739
+ *,
740
+ auto_upgrade_policy: str | None = None,
741
+ context: dict[str, str] | None = None,
742
+ auto_install: bool = True,
743
+ require_verified: bool = True,
744
+ require_trusted: bool = False,
745
+ allow_low_confidence: bool = False,
746
+ on_detect: Callable[[str, str, str], None] | None = None,
747
+ on_install: Callable[[str], None] | None = None,
748
+ ) -> DetectAndInstallResult:
749
+ """Detect a capability gap from an error and optionally install.
750
+
751
+ This is the product-level API: your agent failed? AgentNode knows
752
+ what's missing and can acquire it.
753
+
754
+ Args:
755
+ error: The exception that triggered gap detection.
756
+ auto_upgrade_policy: Named policy ('off', 'safe', 'strict').
757
+ Overrides individual params when set.
758
+ context: Optional context hints (e.g. ``{"file": "report.pdf"}``).
759
+ auto_install: Whether to install on detection. Default True.
760
+ require_verified: Only install verified+ packages. Default True.
761
+ require_trusted: Only install trusted+ packages. Default False.
762
+ allow_low_confidence: Allow install on low-confidence detections.
763
+ on_detect: Callback(capability, confidence, error_msg) on detection.
764
+ on_install: Callback(slug) on successful install.
765
+ """
766
+ resolved = _resolve_auto_upgrade_policy(
767
+ auto_upgrade_policy,
768
+ auto_install=auto_install,
769
+ require_verified=require_verified,
770
+ require_trusted=require_trusted,
771
+ allow_low_confidence=allow_low_confidence,
772
+ )
773
+ r_auto_install, r_require_verified, r_require_trusted, r_allow_low = resolved
774
+
775
+ gap = detect_gap(error, context)
776
+ if gap is None:
777
+ return DetectAndInstallResult(
778
+ detected=False,
779
+ auto_upgrade_policy=auto_upgrade_policy,
780
+ error="No capability gap detected",
781
+ )
782
+
783
+ if on_detect is not None:
784
+ on_detect(gap.capability, gap.confidence, str(error))
785
+
786
+ if gap.confidence == "low" and not r_allow_low:
787
+ return DetectAndInstallResult(
788
+ detected=True,
789
+ capability=gap.capability,
790
+ confidence=gap.confidence,
791
+ installed=False,
792
+ auto_upgrade_policy=auto_upgrade_policy,
793
+ error="Low-confidence detection blocked",
794
+ )
795
+
796
+ if not r_auto_install:
797
+ return DetectAndInstallResult(
798
+ detected=True,
799
+ capability=gap.capability,
800
+ confidence=gap.confidence,
801
+ installed=False,
802
+ auto_upgrade_policy=auto_upgrade_policy,
803
+ )
804
+
805
+ install_result = self.resolve_and_install(
806
+ [gap.capability],
807
+ require_trusted=r_require_trusted,
808
+ require_verified=r_require_verified,
809
+ )
810
+
811
+ if install_result.installed and on_install is not None:
812
+ on_install(install_result.slug)
813
+
814
+ return DetectAndInstallResult(
815
+ detected=True,
816
+ capability=gap.capability,
817
+ confidence=gap.confidence,
818
+ installed=install_result.installed,
819
+ install_result=install_result,
820
+ auto_upgrade_policy=auto_upgrade_policy,
821
+ )
822
+
823
+ def smart_run(
824
+ self,
825
+ fn: Callable[[], Any],
826
+ *,
827
+ auto_upgrade_policy: str | None = None,
828
+ auto_install: bool = True,
829
+ require_verified: bool = True,
830
+ require_trusted: bool = False,
831
+ allow_low_confidence: bool = False,
832
+ context: dict[str, str] | None = None,
833
+ on_detect: Callable[[str, str, str], None] | None = None,
834
+ on_install: Callable[[str], None] | None = None,
835
+ ) -> SmartRunResult:
836
+ """Run a callable with automatic gap detection and retry.
837
+
838
+ Wraps your logic: if it fails due to a missing capability,
839
+ AgentNode detects the gap, installs the skill, and retries once.
840
+
841
+ Args:
842
+ fn: Zero-argument callable to execute.
843
+ auto_upgrade_policy: Named policy ('off', 'safe', 'strict').
844
+ auto_install: Whether to auto-install on detection.
845
+ require_verified: Only install verified+ packages.
846
+ require_trusted: Only install trusted+ packages.
847
+ allow_low_confidence: Allow install on low-confidence detections.
848
+ context: Optional context hints for detection.
849
+ on_detect: Callback(capability, confidence, error_msg) on detection.
850
+ on_install: Callback(slug) on successful install.
851
+ """
852
+ start = time.monotonic()
853
+
854
+ # Attempt 1
855
+ caught: Exception | None = None
856
+ try:
857
+ result = fn()
858
+ elapsed = (time.monotonic() - start) * 1000
859
+ return SmartRunResult(
860
+ success=True,
861
+ result=result,
862
+ duration_ms=elapsed,
863
+ auto_upgrade_policy=auto_upgrade_policy,
864
+ )
865
+ except Exception as exc:
866
+ caught = exc
867
+ original_error = str(exc)
868
+
869
+ # Detect and install
870
+ detect_result = self.detect_and_install(
871
+ caught,
872
+ auto_upgrade_policy=auto_upgrade_policy,
873
+ context=context,
874
+ auto_install=auto_install,
875
+ require_verified=require_verified,
876
+ require_trusted=require_trusted,
877
+ allow_low_confidence=allow_low_confidence,
878
+ on_detect=on_detect,
879
+ on_install=on_install,
880
+ )
881
+
882
+ if not detect_result.detected:
883
+ elapsed = (time.monotonic() - start) * 1000
884
+ return SmartRunResult(
885
+ success=False,
886
+ error=original_error,
887
+ original_error=original_error,
888
+ duration_ms=elapsed,
889
+ auto_upgrade_policy=auto_upgrade_policy,
890
+ )
891
+
892
+ if not detect_result.installed:
893
+ elapsed = (time.monotonic() - start) * 1000
894
+ return SmartRunResult(
895
+ success=False,
896
+ error=detect_result.error or original_error,
897
+ detected_capability=detect_result.capability,
898
+ detection_confidence=detect_result.confidence,
899
+ original_error=original_error,
900
+ duration_ms=elapsed,
901
+ auto_upgrade_policy=auto_upgrade_policy,
902
+ )
903
+
904
+ # Attempt 2 (exactly once)
905
+ installed_slug = (
906
+ detect_result.install_result.slug if detect_result.install_result else None
907
+ )
908
+ installed_version = (
909
+ detect_result.install_result.version if detect_result.install_result else None
910
+ )
911
+
912
+ try:
913
+ result = fn()
914
+ elapsed = (time.monotonic() - start) * 1000
915
+ return SmartRunResult(
916
+ success=True,
917
+ result=result,
918
+ upgraded=True,
919
+ installed_slug=installed_slug,
920
+ installed_version=installed_version,
921
+ detected_capability=detect_result.capability,
922
+ detection_confidence=detect_result.confidence,
923
+ duration_ms=elapsed,
924
+ original_error=original_error,
925
+ auto_upgrade_policy=auto_upgrade_policy,
926
+ )
927
+ except Exception as retry_exc:
928
+ elapsed = (time.monotonic() - start) * 1000
929
+ return SmartRunResult(
930
+ success=False,
931
+ error=str(retry_exc),
932
+ upgraded=True,
933
+ installed_slug=installed_slug,
934
+ installed_version=installed_version,
935
+ detected_capability=detect_result.capability,
936
+ detection_confidence=detect_result.confidence,
937
+ duration_ms=elapsed,
938
+ original_error=original_error,
939
+ auto_upgrade_policy=auto_upgrade_policy,
940
+ )