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.
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/PKG-INFO +29 -9
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/README.md +28 -8
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/__init__.py +13 -1
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/client.py +272 -0
- agentnode_sdk-0.4.0/agentnode_sdk/detect.py +200 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/installer.py +7 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/models.py +52 -0
- agentnode_sdk-0.4.0/agentnode_sdk/runner.py +298 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/pyproject.toml +1 -1
- agentnode_sdk-0.4.0/tests/test_auto_upgrade_policy.py +119 -0
- agentnode_sdk-0.4.0/tests/test_detect.py +210 -0
- agentnode_sdk-0.4.0/tests/test_detect_and_install.py +170 -0
- agentnode_sdk-0.4.0/tests/test_runner.py +315 -0
- agentnode_sdk-0.4.0/tests/test_smart.py +345 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/.env.example +0 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/.gitignore +0 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode.lock +0 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/async_client.py +0 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/agentnode_sdk/exceptions.py +0 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/tests/__init__.py +0 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/tests/test_async_client.py +0 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/tests/test_client.py +0 -0
- {agentnode_sdk-0.2.0 → agentnode_sdk-0.4.0}/tests/test_edge_cases.py +0 -0
- {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.
|
|
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.
|
|
54
|
-
|
|
55
|
-
result =
|
|
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
|
-
#
|
|
58
|
-
|
|
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.
|
|
31
|
-
|
|
32
|
-
result =
|
|
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
|
-
#
|
|
35
|
-
|
|
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.
|
|
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
|
+
)
|