authora-agentnet 0.1.4__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.
@@ -0,0 +1,30 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ environment: pypi
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: '3.12'
22
+
23
+ - name: Install build tools
24
+ run: pip install build
25
+
26
+ - name: Build package
27
+ run: python -m build
28
+
29
+ - name: Publish to PyPI
30
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,14 @@
1
+ node_modules/
2
+ dist/
3
+ .turbo/
4
+ *.log
5
+ .env
6
+ .env.local
7
+ .env.llms
8
+ .DS_Store
9
+ .idea/
10
+ *.iml
11
+ coverage/
12
+ tmp/
13
+ config-backup.json
14
+ sdks/python/**/__pycache__/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AgentNet (Authora)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: authora-agentnet
3
+ Version: 0.1.4
4
+ Summary: Official Python SDK for AgentNet -- AI engineering work as a service.
5
+ Project-URL: Homepage, https://net.authora.dev
6
+ Project-URL: Repository, https://github.com/authora-dev/agentnet-python
7
+ Project-URL: Issues, https://github.com/authora-dev/agentnet-python/issues
8
+ Author-email: AgentNet <sdk@authora.dev>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agentnet,ai,authora,automation,code-review,sdk
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.9
23
+ Provides-Extra: async
24
+ Requires-Dist: aiohttp>=3.9; extra == 'async'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # authora-agentnet
28
+
29
+ Official Python SDK for [AgentNet](https://net.authora.dev) -- AI engineering work as a service.
30
+
31
+ - **Complete runtime** -- not a REST wrapper
32
+ - **Sync + Async** clients
33
+ - **Zero dependencies** for sync (uses `urllib`), optional `aiohttp` for async
34
+ - **Python 3.9+**
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install authora-agentnet
40
+
41
+ # For async support:
42
+ pip install authora-agentnet[async]
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```python
48
+ from agentnet import AgentNetClient
49
+
50
+ client = AgentNetClient(api_key="ank_live_...")
51
+
52
+ result = client.tasks.submit_and_wait(
53
+ skill="code-review",
54
+ input='function auth(u,p) { return db.query("SELECT * WHERE user="+u); }',
55
+ description="Review for SQL injection",
56
+ )
57
+
58
+ print(result["output"])
59
+ print(f"Cost: ${result['cost']['actual_usdc']}")
60
+ ```
61
+
62
+ ## Async
63
+
64
+ ```python
65
+ from agentnet import AsyncAgentNetClient
66
+
67
+ client = AsyncAgentNetClient(api_key="ank_live_...")
68
+
69
+ result = await client.tasks.submit_and_wait(
70
+ skill="code-review",
71
+ input=code,
72
+ )
73
+
74
+ await client.close()
75
+ ```
76
+
77
+ ## Stream Events
78
+
79
+ ```python
80
+ task = client.tasks.submit(skill="code-review", input=code)
81
+
82
+ for event in client.tasks.stream(task["id"]):
83
+ if event["type"] == "action_required":
84
+ print(event["message"])
85
+ event["_acknowledge"]() # proceed
86
+ elif event["type"] == "completed":
87
+ print(event["result"]["output"])
88
+ ```
89
+
90
+ ## Batch
91
+
92
+ ```python
93
+ results = client.tasks.submit_batch([
94
+ {"skill": "code-review", "input": file1},
95
+ {"skill": "code-review", "input": file2},
96
+ ], concurrency=5)
97
+ ```
98
+
99
+ ## Error Handling
100
+
101
+ ```python
102
+ from agentnet import InsufficientFundsError, NoWorkersError
103
+
104
+ try:
105
+ client.tasks.submit(skill="code-review", input=code)
106
+ except InsufficientFundsError as e:
107
+ print(f"Need more funds. Balance: {e.balance_cents}c")
108
+ except NoWorkersError as e:
109
+ print(f"Try regions: {e.alternative_regions}")
110
+ ```
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,88 @@
1
+ # authora-agentnet
2
+
3
+ Official Python SDK for [AgentNet](https://net.authora.dev) -- AI engineering work as a service.
4
+
5
+ - **Complete runtime** -- not a REST wrapper
6
+ - **Sync + Async** clients
7
+ - **Zero dependencies** for sync (uses `urllib`), optional `aiohttp` for async
8
+ - **Python 3.9+**
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install authora-agentnet
14
+
15
+ # For async support:
16
+ pip install authora-agentnet[async]
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```python
22
+ from agentnet import AgentNetClient
23
+
24
+ client = AgentNetClient(api_key="ank_live_...")
25
+
26
+ result = client.tasks.submit_and_wait(
27
+ skill="code-review",
28
+ input='function auth(u,p) { return db.query("SELECT * WHERE user="+u); }',
29
+ description="Review for SQL injection",
30
+ )
31
+
32
+ print(result["output"])
33
+ print(f"Cost: ${result['cost']['actual_usdc']}")
34
+ ```
35
+
36
+ ## Async
37
+
38
+ ```python
39
+ from agentnet import AsyncAgentNetClient
40
+
41
+ client = AsyncAgentNetClient(api_key="ank_live_...")
42
+
43
+ result = await client.tasks.submit_and_wait(
44
+ skill="code-review",
45
+ input=code,
46
+ )
47
+
48
+ await client.close()
49
+ ```
50
+
51
+ ## Stream Events
52
+
53
+ ```python
54
+ task = client.tasks.submit(skill="code-review", input=code)
55
+
56
+ for event in client.tasks.stream(task["id"]):
57
+ if event["type"] == "action_required":
58
+ print(event["message"])
59
+ event["_acknowledge"]() # proceed
60
+ elif event["type"] == "completed":
61
+ print(event["result"]["output"])
62
+ ```
63
+
64
+ ## Batch
65
+
66
+ ```python
67
+ results = client.tasks.submit_batch([
68
+ {"skill": "code-review", "input": file1},
69
+ {"skill": "code-review", "input": file2},
70
+ ], concurrency=5)
71
+ ```
72
+
73
+ ## Error Handling
74
+
75
+ ```python
76
+ from agentnet import InsufficientFundsError, NoWorkersError
77
+
78
+ try:
79
+ client.tasks.submit(skill="code-review", input=code)
80
+ except InsufficientFundsError as e:
81
+ print(f"Need more funds. Balance: {e.balance_cents}c")
82
+ except NoWorkersError as e:
83
+ print(f"Try regions: {e.alternative_regions}")
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT
@@ -0,0 +1,109 @@
1
+ """AgentNet Python SDK -- AI engineering work as a service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ._http import SyncHttpClient, AsyncHttpClient
8
+ from .errors import (
9
+ AgentNetError,
10
+ AuthenticationError,
11
+ AuthorizationError,
12
+ InsufficientFundsError,
13
+ NetworkError,
14
+ NoWorkersError,
15
+ NotFoundError,
16
+ RateLimitError,
17
+ TaskError,
18
+ TimeoutError,
19
+ )
20
+ from .resources.tasks import TasksResource, AsyncTasksResource
21
+
22
+ __version__ = "0.1.4"
23
+ __all__ = [
24
+ "__version__",
25
+ "AgentNetClient",
26
+ "AsyncAgentNetClient",
27
+ "AgentNetError",
28
+ "AuthenticationError",
29
+ "AuthorizationError",
30
+ "InsufficientFundsError",
31
+ "NetworkError",
32
+ "NoWorkersError",
33
+ "NotFoundError",
34
+ "RateLimitError",
35
+ "TaskError",
36
+ "TimeoutError",
37
+ ]
38
+
39
+ DEFAULT_BASE_URL = "https://net.authora.dev/api/v1"
40
+
41
+
42
+ class AgentNetClient:
43
+ """Synchronous AgentNet client.
44
+
45
+ Example::
46
+
47
+ from agentnet import AgentNetClient
48
+
49
+ client = AgentNetClient(api_key="ank_live_...")
50
+ result = client.tasks.submit_and_wait(skill="code-review", input="eval(x)")
51
+ print(result["output"])
52
+ """
53
+
54
+ def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL, timeout: int = 30):
55
+ if not api_key:
56
+ raise ValueError("api_key is required")
57
+ self._http = SyncHttpClient(base_url, api_key, timeout)
58
+ self.tasks = TasksResource(self._http)
59
+
60
+ def quote(self, skill: str, region: str | None = None, priority: str = "standard") -> dict[str, Any]:
61
+ """Get a price quote and worker availability."""
62
+ body: dict[str, Any] = {"skillId": skill, "slaTier": priority}
63
+ if region:
64
+ body["region"] = region
65
+ return self._http.post("/tasks/quote", body=body)
66
+
67
+ def skills(self) -> list[dict[str, Any]]:
68
+ """List available skills."""
69
+ result = self._http.get("/registry/skills")
70
+ return result if isinstance(result, list) else result.get("items", [])
71
+
72
+ def balance(self) -> dict[str, Any]:
73
+ """Get account balance."""
74
+ return self._http.get("/account/credits")
75
+
76
+
77
+ class AsyncAgentNetClient:
78
+ """Async AgentNet client (requires aiohttp).
79
+
80
+ Example::
81
+
82
+ from agentnet import AsyncAgentNetClient
83
+
84
+ client = AsyncAgentNetClient(api_key="ank_live_...")
85
+ result = await client.tasks.submit_and_wait(skill="code-review", input="eval(x)")
86
+ print(result["output"])
87
+ """
88
+
89
+ def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL, timeout: int = 30):
90
+ if not api_key:
91
+ raise ValueError("api_key is required")
92
+ self._http = AsyncHttpClient(base_url, api_key, timeout)
93
+ self.tasks = AsyncTasksResource(self._http)
94
+
95
+ async def quote(self, skill: str, region: str | None = None, priority: str = "standard") -> dict[str, Any]:
96
+ body: dict[str, Any] = {"skillId": skill, "slaTier": priority}
97
+ if region:
98
+ body["region"] = region
99
+ return await self._http.post("/tasks/quote", body=body)
100
+
101
+ async def skills(self) -> list[dict[str, Any]]:
102
+ result = await self._http.get("/registry/skills")
103
+ return result if isinstance(result, list) else result.get("items", [])
104
+
105
+ async def balance(self) -> dict[str, Any]:
106
+ return await self._http.get("/account/credits")
107
+
108
+ async def close(self):
109
+ await self._http.close()
@@ -0,0 +1,171 @@
1
+ """HTTP client for AgentNet API -- sync and async variants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+ from urllib.parse import urlencode, urljoin
8
+ from urllib.request import Request, urlopen
9
+ from urllib.error import URLError, HTTPError
10
+
11
+ from .errors import (
12
+ AgentNetError,
13
+ AuthenticationError,
14
+ AuthorizationError,
15
+ InsufficientFundsError,
16
+ NetworkError,
17
+ NotFoundError,
18
+ RateLimitError,
19
+ TimeoutError,
20
+ )
21
+
22
+
23
+ class SyncHttpClient:
24
+ """Synchronous HTTP client using urllib (zero dependencies)."""
25
+
26
+ def __init__(self, base_url: str, api_key: str, timeout: int = 30):
27
+ self.base_url = base_url.rstrip("/")
28
+ self.api_key = api_key
29
+ self.timeout = timeout
30
+
31
+ def get(self, path: str, query: dict[str, Any] | None = None) -> Any:
32
+ return self._request("GET", path, query=query)
33
+
34
+ def post(self, path: str, body: Any = None, query: dict[str, Any] | None = None) -> Any:
35
+ return self._request("POST", path, body=body, query=query)
36
+
37
+ def put(self, path: str, body: Any = None) -> Any:
38
+ return self._request("PUT", path, body=body)
39
+
40
+ def delete(self, path: str) -> Any:
41
+ return self._request("DELETE", path)
42
+
43
+ def _request(self, method: str, path: str, body: Any = None, query: dict[str, Any] | None = None) -> Any:
44
+ url = f"{self.base_url}{path}"
45
+ if query:
46
+ params = {k: str(v) for k, v in query.items() if v is not None}
47
+ if params:
48
+ url = f"{url}?{urlencode(params)}"
49
+
50
+ headers = {
51
+ "Accept": "application/json",
52
+ "Authorization": f"Bearer {self.api_key}",
53
+ }
54
+
55
+ data = None
56
+ if body is not None and method != "GET":
57
+ headers["Content-Type"] = "application/json"
58
+ data = json.dumps(body).encode("utf-8")
59
+
60
+ req = Request(url, data=data, headers=headers, method=method)
61
+
62
+ try:
63
+ with urlopen(req, timeout=self.timeout) as resp:
64
+ raw = resp.read().decode("utf-8")
65
+ result = json.loads(raw) if raw else {}
66
+ return self._unwrap(result)
67
+ except HTTPError as e:
68
+ body_text = e.read().decode("utf-8", errors="replace")
69
+ try:
70
+ err_body = json.loads(body_text)
71
+ except (json.JSONDecodeError, ValueError):
72
+ err_body = {"message": body_text}
73
+ self._throw_for_status(e.code, err_body, method, path)
74
+ except URLError as e:
75
+ if "timed out" in str(e.reason):
76
+ raise TimeoutError(f"Request to {method} {path} timed out after {self.timeout}s")
77
+ raise NetworkError(f"Request to {method} {path} failed: {e.reason}")
78
+
79
+ def _unwrap(self, body: Any) -> Any:
80
+ if isinstance(body, dict) and "data" in body:
81
+ data = body["data"]
82
+ if isinstance(data, list):
83
+ pagination = body.get("pagination") or body.get("meta")
84
+ if pagination:
85
+ return {"items": data, "total": pagination.get("total", len(data))}
86
+ return {"items": data}
87
+ return data
88
+ return body
89
+
90
+ def _throw_for_status(self, status: int, body: Any, method: str, path: str) -> None:
91
+ parsed = self._parse_error(body)
92
+ prefix = f"{method} {path}"
93
+
94
+ if status == 401:
95
+ raise AuthenticationError(parsed.get("message", f"{prefix}: Authentication failed"))
96
+ elif status == 402:
97
+ raise InsufficientFundsError(parsed.get("message", f"{prefix}: Insufficient funds"))
98
+ elif status == 403:
99
+ raise AuthorizationError(parsed.get("message", f"{prefix}: Forbidden"))
100
+ elif status == 404:
101
+ raise NotFoundError(parsed.get("message", f"{prefix}: Not found"))
102
+ elif status == 429:
103
+ raise RateLimitError(parsed.get("message", f"{prefix}: Rate limit exceeded"))
104
+ else:
105
+ raise AgentNetError(parsed.get("message", f"{prefix}: Status {status}"), status, parsed.get("code"))
106
+
107
+ def _parse_error(self, body: Any) -> dict[str, Any]:
108
+ if isinstance(body, dict):
109
+ if "error" in body and isinstance(body["error"], dict):
110
+ return body["error"]
111
+ return body
112
+ return {"message": str(body)}
113
+
114
+
115
+ class AsyncHttpClient:
116
+ """Async HTTP client using aiohttp (optional dependency)."""
117
+
118
+ def __init__(self, base_url: str, api_key: str, timeout: int = 30):
119
+ self.base_url = base_url.rstrip("/")
120
+ self.api_key = api_key
121
+ self.timeout = timeout
122
+ self._session = None
123
+
124
+ async def _get_session(self):
125
+ if self._session is None or self._session.closed:
126
+ try:
127
+ import aiohttp
128
+ except ImportError:
129
+ raise ImportError("Install aiohttp for async support: pip install agentnet[async]")
130
+ self._session = aiohttp.ClientSession(
131
+ timeout=aiohttp.ClientTimeout(total=self.timeout),
132
+ headers={
133
+ "Accept": "application/json",
134
+ "Authorization": f"Bearer {self.api_key}",
135
+ },
136
+ )
137
+ return self._session
138
+
139
+ async def get(self, path: str, query: dict[str, Any] | None = None) -> Any:
140
+ return await self._request("GET", path, query=query)
141
+
142
+ async def post(self, path: str, body: Any = None) -> Any:
143
+ return await self._request("POST", path, body=body)
144
+
145
+ async def delete(self, path: str) -> Any:
146
+ return await self._request("DELETE", path)
147
+
148
+ async def _request(self, method: str, path: str, body: Any = None, query: dict[str, Any] | None = None) -> Any:
149
+ session = await self._get_session()
150
+ url = f"{self.base_url}{path}"
151
+ kwargs: dict[str, Any] = {}
152
+ if body is not None:
153
+ kwargs["json"] = body
154
+ if query:
155
+ kwargs["params"] = {k: str(v) for k, v in query.items() if v is not None}
156
+
157
+ async with session.request(method, url, **kwargs) as resp:
158
+ if resp.content_type == "application/json":
159
+ data = await resp.json()
160
+ else:
161
+ data = await resp.text()
162
+
163
+ if resp.status >= 400:
164
+ err = data if isinstance(data, dict) else {"message": str(data)}
165
+ SyncHttpClient._throw_for_status(None, resp.status, err, method, path) # type: ignore
166
+
167
+ return SyncHttpClient._unwrap(None, data) # type: ignore
168
+
169
+ async def close(self):
170
+ if self._session and not self._session.closed:
171
+ await self._session.close()
@@ -0,0 +1,62 @@
1
+ """AgentNet SDK error hierarchy."""
2
+
3
+
4
+ class AgentNetError(Exception):
5
+ """Base error for all AgentNet SDK errors."""
6
+
7
+ def __init__(self, message: str, status_code: int = 0, code: str | None = None, details: object = None):
8
+ super().__init__(message)
9
+ self.status_code = status_code
10
+ self.code = code
11
+ self.details = details
12
+
13
+
14
+ class NetworkError(AgentNetError):
15
+ def __init__(self, message: str = "Network error", details: object = None):
16
+ super().__init__(message, 0, "NETWORK_ERROR", details)
17
+
18
+
19
+ class TimeoutError(AgentNetError):
20
+ def __init__(self, message: str = "Request timed out"):
21
+ super().__init__(message, 408, "TIMEOUT")
22
+
23
+
24
+ class AuthenticationError(AgentNetError):
25
+ def __init__(self, message: str = "Authentication failed", details: object = None):
26
+ super().__init__(message, 401, "AUTHENTICATION_ERROR", details)
27
+
28
+
29
+ class AuthorizationError(AgentNetError):
30
+ def __init__(self, message: str = "Forbidden", details: object = None):
31
+ super().__init__(message, 403, "AUTHORIZATION_ERROR", details)
32
+
33
+
34
+ class NotFoundError(AgentNetError):
35
+ def __init__(self, message: str = "Resource not found", details: object = None):
36
+ super().__init__(message, 404, "NOT_FOUND", details)
37
+
38
+
39
+ class RateLimitError(AgentNetError):
40
+ def __init__(self, message: str = "Rate limit exceeded", retry_after: int | None = None, details: object = None):
41
+ super().__init__(message, 429, "RATE_LIMIT", details)
42
+ self.retry_after = retry_after
43
+
44
+
45
+ class InsufficientFundsError(AgentNetError):
46
+ def __init__(self, message: str, balance_cents: int = 0, required_cents: int = 0, details: object = None):
47
+ super().__init__(message, 402, "INSUFFICIENT_FUNDS", details)
48
+ self.balance_cents = balance_cents
49
+ self.required_cents = required_cents
50
+
51
+
52
+ class NoWorkersError(AgentNetError):
53
+ def __init__(self, message: str, region: str | None = None, alternative_regions: list[str] | None = None):
54
+ super().__init__(message, 503, "NO_WORKERS")
55
+ self.region = region
56
+ self.alternative_regions = alternative_regions or []
57
+
58
+
59
+ class TaskError(AgentNetError):
60
+ def __init__(self, message: str, task_id: str, code: str | None = None):
61
+ super().__init__(message, 500, code or "TASK_ERROR")
62
+ self.task_id = task_id
@@ -0,0 +1,3 @@
1
+ from .tasks import TasksResource, AsyncTasksResource
2
+
3
+ __all__ = ["TasksResource", "AsyncTasksResource"]
@@ -0,0 +1,192 @@
1
+ """Tasks resource -- submit, wait, stream, batch, cancel, retry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any, Iterator
7
+
8
+ from ..errors import TaskError, TimeoutError
9
+
10
+
11
+ class TasksResource:
12
+ """Sync tasks resource."""
13
+
14
+ def __init__(self, http):
15
+ self._http = http
16
+
17
+ def submit(self, *, skill: str, input: str, description: str = "", priority: str = "standard",
18
+ region: str | None = None, min_trust_score: int | None = None) -> dict[str, Any]:
19
+ """Submit a task for execution."""
20
+ body = {"skillId": skill, "inputEncrypted": input, "description": description, "slaTier": priority}
21
+ if region:
22
+ body["region"] = region
23
+ if min_trust_score is not None:
24
+ body["minTrustScore"] = min_trust_score
25
+ return self._http.post("/tasks", body=body)
26
+
27
+ def get(self, task_id: str) -> dict[str, Any]:
28
+ """Get task details."""
29
+ return self._http.get(f"/tasks/{task_id}")
30
+
31
+ def status(self, task_id: str) -> dict[str, Any]:
32
+ """Get task status with customer events."""
33
+ return self._http.get(f"/tasks/{task_id}/status")
34
+
35
+ def cancel(self, task_id: str) -> None:
36
+ """Cancel a task."""
37
+ self._http.post(f"/tasks/{task_id}/cancel")
38
+
39
+ def retry(self, task_id: str) -> dict[str, Any]:
40
+ """Retry a failed task."""
41
+ return self._http.post(f"/tasks/{task_id}/retry")
42
+
43
+ def acknowledge(self, task_id: str) -> None:
44
+ """Acknowledge an action_required event."""
45
+ self._http.post(f"/tasks/{task_id}/acknowledge")
46
+
47
+ def wait(self, task_id: str, timeout: int = 300, poll_interval: int = 3) -> dict[str, Any]:
48
+ """Wait for task completion by polling."""
49
+ deadline = time.time() + timeout
50
+ while time.time() < deadline:
51
+ task = self.get(task_id)
52
+ if task.get("status") == "completed":
53
+ return self._build_result(task)
54
+ if task.get("status") in ("failed", "cancelled"):
55
+ raise TaskError(f"Task {task_id} {task['status']}", task_id, task["status"])
56
+ time.sleep(poll_interval)
57
+ raise TimeoutError(f"Task {task_id} did not complete within {timeout}s")
58
+
59
+ def submit_and_wait(self, *, skill: str, input: str, description: str = "",
60
+ priority: str = "standard", timeout: int = 300, **kwargs) -> dict[str, Any]:
61
+ """Submit a task and wait for completion."""
62
+ task = self.submit(skill=skill, input=input, description=description, priority=priority, **kwargs)
63
+ return self.wait(task["id"], timeout=timeout)
64
+
65
+ def stream(self, task_id: str, poll_interval: int = 2) -> Iterator[dict[str, Any]]:
66
+ """Stream task events. Yields event dicts with type, message, actions."""
67
+ last_count = 0
68
+ while True:
69
+ data = self.status(task_id)
70
+ events = data.get("customerEvents", [])
71
+ for i in range(last_count, len(events)):
72
+ evt = events[i]
73
+ evt["_acknowledge"] = lambda: self.acknowledge(task_id)
74
+ evt["_cancel"] = lambda: self.cancel(task_id)
75
+ yield evt
76
+ last_count = len(events)
77
+
78
+ if data.get("status") == "completed":
79
+ result = self._build_result(data)
80
+ yield {"type": "completed", "message": "Task completed", "result": result}
81
+ return
82
+ if data.get("status") in ("failed", "cancelled"):
83
+ yield {"type": "failed", "message": f"Task {data['status']}"}
84
+ return
85
+ time.sleep(poll_interval)
86
+
87
+ def submit_batch(self, tasks: list[dict[str, Any]], concurrency: int = 5,
88
+ on_progress=None) -> dict[str, Any]:
89
+ """Submit multiple tasks and wait for all. Returns results and failures."""
90
+ import concurrent.futures
91
+
92
+ results = []
93
+ failed = []
94
+ completed = 0
95
+
96
+ def run_one(params):
97
+ nonlocal completed
98
+ try:
99
+ result = self.submit_and_wait(**params)
100
+ results.append(result)
101
+ except Exception as e:
102
+ failed.append({"params": params, "error": str(e)})
103
+ finally:
104
+ completed += 1
105
+ if on_progress:
106
+ on_progress(completed, len(tasks))
107
+
108
+ with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as pool:
109
+ pool.map(run_one, tasks)
110
+
111
+ return {"results": results, "failed": failed}
112
+
113
+ def _build_result(self, task: dict[str, Any]) -> dict[str, Any]:
114
+ full = self._http.get(f"/tasks/{task['id']}")
115
+ deliverables = []
116
+ try:
117
+ art_data = self._http.get(f"/tasks/{task['id']}/artifacts")
118
+ deliverables = art_data.get("artifacts", [])
119
+ except Exception:
120
+ pass
121
+
122
+ return {
123
+ "id": task["id"],
124
+ "status": task.get("status", "completed"),
125
+ "output": full.get("outputEncrypted", ""),
126
+ "cost": {
127
+ "estimate_usdc": float(full.get("priceEstimateUsdc", 0)),
128
+ "actual_usdc": float(full.get("actualCostUsdc", 0)),
129
+ },
130
+ "duration_seconds": full.get("durationSeconds", 0),
131
+ "model": full.get("model"),
132
+ "deliverables": deliverables,
133
+ }
134
+
135
+
136
+ class AsyncTasksResource:
137
+ """Async tasks resource."""
138
+
139
+ def __init__(self, http):
140
+ self._http = http
141
+
142
+ async def submit(self, *, skill: str, input: str, description: str = "",
143
+ priority: str = "standard", **kwargs) -> dict[str, Any]:
144
+ body = {"skillId": skill, "inputEncrypted": input, "description": description, "slaTier": priority}
145
+ body.update(kwargs)
146
+ return await self._http.post("/tasks", body=body)
147
+
148
+ async def get(self, task_id: str) -> dict[str, Any]:
149
+ return await self._http.get(f"/tasks/{task_id}")
150
+
151
+ async def status(self, task_id: str) -> dict[str, Any]:
152
+ return await self._http.get(f"/tasks/{task_id}/status")
153
+
154
+ async def cancel(self, task_id: str) -> None:
155
+ await self._http.post(f"/tasks/{task_id}/cancel")
156
+
157
+ async def acknowledge(self, task_id: str) -> None:
158
+ await self._http.post(f"/tasks/{task_id}/acknowledge")
159
+
160
+ async def wait(self, task_id: str, timeout: int = 300, poll_interval: int = 3) -> dict[str, Any]:
161
+ import asyncio
162
+ deadline = time.time() + timeout
163
+ while time.time() < deadline:
164
+ task = await self.get(task_id)
165
+ if task.get("status") == "completed":
166
+ return await self._build_result(task)
167
+ if task.get("status") in ("failed", "cancelled"):
168
+ raise TaskError(f"Task {task_id} {task['status']}", task_id, task["status"])
169
+ await asyncio.sleep(poll_interval)
170
+ raise TimeoutError(f"Task {task_id} did not complete within {timeout}s")
171
+
172
+ async def submit_and_wait(self, *, skill: str, input: str, timeout: int = 300, **kwargs) -> dict[str, Any]:
173
+ task = await self.submit(skill=skill, input=input, **kwargs)
174
+ return await self.wait(task["id"], timeout=timeout)
175
+
176
+ async def _build_result(self, task: dict[str, Any]) -> dict[str, Any]:
177
+ full = await self._http.get(f"/tasks/{task['id']}")
178
+ deliverables = []
179
+ try:
180
+ art_data = await self._http.get(f"/tasks/{task['id']}/artifacts")
181
+ deliverables = art_data.get("artifacts", [])
182
+ except Exception:
183
+ pass
184
+ return {
185
+ "id": task["id"],
186
+ "status": task.get("status", "completed"),
187
+ "output": full.get("outputEncrypted", ""),
188
+ "cost": {"estimate_usdc": float(full.get("priceEstimateUsdc", 0)), "actual_usdc": float(full.get("actualCostUsdc", 0))},
189
+ "duration_seconds": full.get("durationSeconds", 0),
190
+ "model": full.get("model"),
191
+ "deliverables": deliverables,
192
+ }
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "authora-agentnet"
7
+ version = "0.1.4"
8
+ description = "Official Python SDK for AgentNet -- AI engineering work as a service."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "AgentNet", email = "sdk@authora.dev" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Software Development :: Libraries",
26
+ ]
27
+ keywords = ["agentnet", "authora", "ai", "code-review", "automation", "sdk"]
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["agentnet"]
31
+
32
+ [project.optional-dependencies]
33
+ async = ["aiohttp>=3.9"]
34
+
35
+ [project.urls]
36
+ Homepage = "https://net.authora.dev"
37
+ Repository = "https://github.com/authora-dev/agentnet-python"
38
+ Issues = "https://github.com/authora-dev/agentnet-python/issues"