parcha-agentrun 0.2.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 (29) hide show
  1. parcha_agentrun-0.2.0/.gitignore +11 -0
  2. parcha_agentrun-0.2.0/CHANGELOG.md +20 -0
  3. parcha_agentrun-0.2.0/PKG-INFO +243 -0
  4. parcha_agentrun-0.2.0/README.md +218 -0
  5. parcha_agentrun-0.2.0/pyproject.toml +81 -0
  6. parcha_agentrun-0.2.0/scripts/smoke_staging.py +61 -0
  7. parcha_agentrun-0.2.0/scripts/update_contract.py +43 -0
  8. parcha_agentrun-0.2.0/src/agentrun/__init__.py +129 -0
  9. parcha_agentrun-0.2.0/src/agentrun/_batch.py +86 -0
  10. parcha_agentrun-0.2.0/src/agentrun/_http.py +184 -0
  11. parcha_agentrun-0.2.0/src/agentrun/_ops.py +451 -0
  12. parcha_agentrun-0.2.0/src/agentrun/_poll.py +61 -0
  13. parcha_agentrun-0.2.0/src/agentrun/_version.py +1 -0
  14. parcha_agentrun-0.2.0/src/agentrun/agent.py +833 -0
  15. parcha_agentrun-0.2.0/src/agentrun/client.py +695 -0
  16. parcha_agentrun-0.2.0/src/agentrun/errors.py +164 -0
  17. parcha_agentrun-0.2.0/src/agentrun/models.py +498 -0
  18. parcha_agentrun-0.2.0/src/agentrun/py.typed +0 -0
  19. parcha_agentrun-0.2.0/tests/__init__.py +0 -0
  20. parcha_agentrun-0.2.0/tests/conftest.py +339 -0
  21. parcha_agentrun-0.2.0/tests/fixtures/public-v2.json +11964 -0
  22. parcha_agentrun-0.2.0/tests/test_agent.py +337 -0
  23. parcha_agentrun-0.2.0/tests/test_batches_loops_campaigns.py +215 -0
  24. parcha_agentrun-0.2.0/tests/test_contract.py +159 -0
  25. parcha_agentrun-0.2.0/tests/test_errors.py +111 -0
  26. parcha_agentrun-0.2.0/tests/test_packaged_contract.py +127 -0
  27. parcha_agentrun-0.2.0/tests/test_statuses.py +117 -0
  28. parcha_agentrun-0.2.0/tests/typing/__init__.py +0 -0
  29. parcha_agentrun-0.2.0/tests/typing/test_types.py +55 -0
@@ -0,0 +1,11 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .venv/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ .env
11
+ .env.*
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ First release of the rewritten SDK. Requires the unified v2 backend.
6
+
7
+ - Published as `parcha-agentrun` on PyPI. The module is still `agentrun`. PyPI rejected `agentrun-ai` as too similar to an existing project.
8
+
9
+ - Targets the unified v2 API. Default base URL is `https://api.grep.ai/api/v2`.
10
+ - `AgentRun` and `AsyncAgentRun` clients cover agents, builds, runs, batches, loops, skills, integrations, campaigns, and usage.
11
+ - `Agent` and `AsyncAgent` wrap one agent: define it, build it from a description, or reuse it by ID.
12
+ - Typed responses as pydantic models. Ships `py.typed`; checked with pyright strict and mypy strict.
13
+ - Transport is `httpx2` with `anyio` for async. No automatic retries; idempotency keys are passed through.
14
+ - Errors: one class per HTTP status family, plus `InvalidResponseError`, `BuildError`, and `PollTimeoutError`.
15
+ - CSV batches through `BatchCSV`, with the same limits the API enforces.
16
+ - Run, batch, and row statuses are mapped to a short SDK vocabulary; the original value is kept in `raw_status`.
17
+ - Request limits from the API contract (skill lengths, `max_cost_usd`, batch name) are checked locally.
18
+ - Error messages carry the API's reason and request ID from its `{"error": {...}}` envelope.
19
+ - Verified live against staging: run, idempotent replay, wait, cancel, and 404 handling.
20
+ - `tests/fixtures/public-v2.json` carries the backend's OpenAPI contract; every SDK request is validated against it in tests.
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.5
2
+ Name: parcha-agentrun
3
+ Version: 0.2.0
4
+ Summary: Python SDK for AgentRun: create agents, run them, and collect results
5
+ Project-URL: Homepage, https://agentrun.ai
6
+ Project-URL: Documentation, https://agentrun.ai/docs
7
+ Project-URL: Repository, https://github.com/Parcha-ai/agentrun-py
8
+ Project-URL: Issues, https://github.com/Parcha-ai/agentrun-py/issues
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: anyio<5,>=4.4
21
+ Requires-Dist: httpx2<3,>=2.12
22
+ Requires-Dist: pydantic<3,>=2.7
23
+ Requires-Dist: typing-extensions>=4.12
24
+ Description-Content-Type: text/markdown
25
+
26
+ # AgentRun Python SDK
27
+
28
+ Create agents, run them on your inputs, and collect the results. Sync and async clients, typed responses, Python 3.10 or newer.
29
+
30
+ ```sh
31
+ pip install parcha-agentrun
32
+ ```
33
+
34
+ The package on PyPI is `parcha-agentrun`. The module you import is `agentrun`.
35
+
36
+ ## Quick start
37
+
38
+ Set `AGENTRUN_API_KEY` in your environment, then:
39
+
40
+ ```python
41
+ from agentrun import Agent
42
+
43
+ agent = Agent(
44
+ name="Company verifier",
45
+ instructions="Verify the company and cite primary sources.",
46
+ )
47
+ run = agent.run("Verify acme.example")
48
+ result = agent.wait(run.id)
49
+
50
+ print(result.status) # "completed", "failed", "cancelled", or "blocked"
51
+ print(result.markdown) # the report, or None
52
+ ```
53
+
54
+ `Agent` saves itself on the first call that needs it. Save `agent.id` and pass `Agent(agent_id=...)` next time to reuse it. Two things to know about `wait`. It returns for every terminal status, so check `result.status` before using the report. It gives up after 10 minutes by default and raises `PollTimeoutError`, but the run keeps going on the server. Call `agent.get_run(run.id)` later to fetch it.
55
+
56
+ ## Build an agent from a description
57
+
58
+ ```python
59
+ agent = Agent.create("Research companies and cite sources.")
60
+ # Save agent.build_id if this process might stop.
61
+ agent.wait_until_ready() # builds can take a few minutes
62
+ run = agent.run("Research acme.example")
63
+ ```
64
+
65
+ Resume an unfinished build with `Agent(build_id=...)`. Any run method waits for the build first. A build that fails raises `BuildError`; read `error.build.error` for the reason.
66
+
67
+ ## Async
68
+
69
+ Every class has an async twin with the same methods. Works under asyncio and trio.
70
+
71
+ ```python
72
+ from agentrun import AsyncAgent
73
+
74
+ async def main() -> None:
75
+ agent = AsyncAgent(agent_id="agent-123")
76
+ run = await agent.run("Verify acme.example")
77
+ result = await agent.wait(run.id)
78
+ ```
79
+
80
+ ## Statuses
81
+
82
+ The API reports progress in its own vocabulary. The SDK maps it to a short list so `is_done` and your code stay simple. The original value is always in `raw_status`.
83
+
84
+ | `run.status` | Means | Done |
85
+ | --- | --- | --- |
86
+ | `queued` | Accepted, not started. | no |
87
+ | `moderation` | Content check in progress. | no |
88
+ | `running` | Working. | no |
89
+ | `blocked` | Needs your input, or content was rejected. | yes |
90
+ | `completed` | Report is ready. | yes |
91
+ | `failed` | Ran and failed, or never ran. | yes |
92
+ | `cancelled` | Cancelled, deleted, or superseded. | yes |
93
+
94
+ Batches use `queued`, `running`, `completed`, `completed_with_errors`, `failed`, and `cancelled`. Batch rows use `queued`, `running`, `completed`, and `failed`. A status the SDK does not recognise passes through unchanged.
95
+
96
+ ## Structured output
97
+
98
+ Pass a pydantic model or a JSON schema dict as `output_schema`. The result's `structured_output` is a dict; `output_as` validates it into your model.
99
+
100
+ ```python
101
+ from pydantic import BaseModel
102
+
103
+ class Finding(BaseModel):
104
+ verified: bool
105
+ evidence: str
106
+
107
+ run = agent.run("Verify acme.example", output_schema=Finding)
108
+ finding = agent.wait(run.id).output_as(Finding)
109
+ ```
110
+
111
+ ## Batches
112
+
113
+ Run up to 100 inputs at once. The SDK checks the limits before sending anything.
114
+
115
+ ```python
116
+ batch = agent.batch(
117
+ ["Verify acme.example", "Verify beta.example"],
118
+ name="Q3 checks",
119
+ max_concurrent=5,
120
+ idempotency_key="q3-checks-001",
121
+ )
122
+ done = agent.wait_batch(batch.id)
123
+ for row in agent.get_batch_results(batch.id).results:
124
+ print(row.question, row.status, row.run_id)
125
+ ```
126
+
127
+ A CSV works too. Name the column that holds each input. The other columns come back as `row.row_data`.
128
+
129
+ ```python
130
+ from agentrun import BatchCSV
131
+
132
+ batch = agent.batch(BatchCSV("companies.csv", input_column="website"))
133
+ ```
134
+
135
+ `BatchCSV` accepts a path, CSV text, bytes, or an open file. Files are limited to 1 MB.
136
+
137
+ `wait_batch` returns when the batch completes, fails, is cancelled, or pauses for attention. Check `batch.paused_reason`. `get_batch(id, include_rows=False)` skips per-row data on large batches. `cancel_batch` stops new rows; rows already running finish. `retry_batch` re-runs failed rows and spends credits.
138
+
139
+ ## Loops
140
+
141
+ A loop is a batch that repeats on a schedule. The first run starts immediately.
142
+
143
+ ```python
144
+ loop = agent.loop(
145
+ "Check acme.example for new filings",
146
+ {"frequency": "weekly", "schedule_config": {"hour": 9, "minute": 0, "day_of_week": 0}, "timezone": "America/New_York"},
147
+ name="Weekly filings",
148
+ max_credits=20,
149
+ )
150
+ history = agent.get_loop(loop.id)
151
+ agent.pause_loop(loop.id)
152
+ agent.delete_loop(loop.id, cancel_active=True)
153
+ ```
154
+
155
+ `frequency` is `daily`, `weekly`, or `monthly`. Weekly uses `day_of_week` with 0 as Monday. Monthly uses `day_of_month` from 1 to 28. `max_credits` caps each occurrence, not the lifetime total. Inputs are frozen at creation; `update_loop` changes only the name and cadence.
156
+
157
+ ## Campaigns
158
+
159
+ A campaign is a multi-stage plan. Save it as a draft, review it, then approve it with the digest of the plan you reviewed.
160
+
161
+ ```python
162
+ draft = agent.create_campaign(plan)
163
+ # read draft.stages and draft.deliverables
164
+ started = agent.approve_campaign(draft.id, draft.review_digest)
165
+ final = agent.wait_campaign(started.id)
166
+ artifact = agent.download_campaign_artifact(final.id, "companies")
167
+ open(artifact.filename or "companies.xlsx", "wb").write(artifact.content)
168
+ ```
169
+
170
+ An agent configured with `run_mode="campaign"` and a `campaign_plan` can start its template directly with `agent.campaign({"goal": "..."})`. That call starts work immediately. Approving with a stale digest raises `ConflictError`.
171
+
172
+ ## Skills and integrations
173
+
174
+ ```python
175
+ from agentrun import AgentRun
176
+
177
+ client = AgentRun()
178
+ skills = client.list_skills(query="company")
179
+ tools = client.list_integrations(query="company")
180
+
181
+ agent = client.agent(
182
+ name="Company verifier",
183
+ instructions="Verify the company and cite evidence.",
184
+ skills=[s.name for s in skills.skills],
185
+ tools=[t.name for t in tools.tools if not t.always_on],
186
+ )
187
+ ```
188
+
189
+ Save your own Markdown as a skill with `client.create_skill(name, description, content)`. Creating a skill with an existing name updates it; the response has `created=False`.
190
+
191
+ ## Errors
192
+
193
+ Every error inherits from `AgentRunError`.
194
+
195
+ | Error | When |
196
+ | --- | --- |
197
+ | `AuthenticationError` | 401. Bad or missing API key. |
198
+ | `PermissionDeniedError` | 403. The key lacks a scope or does not own the resource. |
199
+ | `NotFoundError` | 404. |
200
+ | `ConflictError` | 409. Stale review digest, or a loop already running. |
201
+ | `UnprocessableEntityError` | 422. `error.body["detail"]` lists the bad fields. |
202
+ | `RateLimitError` | 429. |
203
+ | `ServiceUnavailableError` | 503. Down, or the feature is not enabled for your account. |
204
+ | `APIConnectionError` | No HTTP response at all. `APITimeoutError` for a slow one. |
205
+ | `InvalidResponseError` | 2xx but not the JSON the SDK expects. Usually a wrong `base_url`. |
206
+ | `BuildError` | An agent build failed. |
207
+ | `PollTimeoutError` | A `wait*` call hit its local deadline. Also a `TimeoutError`. |
208
+
209
+ `APIError` subclasses carry `status_code`, `body`, and `request_id`. Include the request ID when you report a problem. The SDK checks the limits the API enforces before sending anything, such as skill content of at least 50 characters or `max_cost_usd` between 0.5 and 1000, and raises `ValueError` locally. The SDK never retries a request on its own. Pass the same `idempotency_key` to `run`, `batch`, `loop`, or `create_campaign` when you retry, so the server does not start the work twice.
210
+
211
+ ## Configuration
212
+
213
+ | Setting | Argument | Environment variable | Default |
214
+ | --- | --- | --- | --- |
215
+ | API key | `api_key=` | `AGENTRUN_API_KEY` | required |
216
+ | API root | `base_url=` | `AGENTRUN_BASE_URL` | `https://api.grep.ai/api/v2` |
217
+ | Per-request timeout | `timeout=` | | 30 seconds |
218
+
219
+ `base_url` must use HTTPS. Plain HTTP is allowed only for localhost. Redirects are never followed. Use `with AgentRun() as client:` or `async with AsyncAgentRun() as client:` to close the connection pool.
220
+
221
+ ## Types
222
+
223
+ The package ships `py.typed`. Responses are pydantic models, so `run.status` and `batch.rows` have the types your editor shows. Records keep any field the server adds later, reachable as an attribute or with `record["field"]`. `id` is always the record's ID, whatever the API calls it.
224
+
225
+ ## Development
226
+
227
+ ```sh
228
+ uv sync --group dev
229
+ uv run pytest
230
+ uv run ruff check src tests
231
+ uv run pyright
232
+ uv run mypy
233
+ uv build && uv run twine check dist/*
234
+ ```
235
+
236
+ Tests run against a fake API. Every test runs twice, once for the sync client and once for the async client.
237
+
238
+ Two tests tie the SDK to the backend:
239
+
240
+ - `tests/test_contract.py` validates every request the SDK sends, and every canned response, against `tests/fixtures/public-v2.json`, the backend's generated OpenAPI document. Refresh it with `python scripts/update_contract.py <path-or-url>` when the API changes.
241
+ - `tests/test_packaged_contract.py` starts a real local HTTP server and runs the same script the backend uses to verify packaged SDKs.
242
+
243
+ `scripts/smoke_staging.py` exercises a live deployment. It is read-only unless you pass `--run`.
@@ -0,0 +1,218 @@
1
+ # AgentRun Python SDK
2
+
3
+ Create agents, run them on your inputs, and collect the results. Sync and async clients, typed responses, Python 3.10 or newer.
4
+
5
+ ```sh
6
+ pip install parcha-agentrun
7
+ ```
8
+
9
+ The package on PyPI is `parcha-agentrun`. The module you import is `agentrun`.
10
+
11
+ ## Quick start
12
+
13
+ Set `AGENTRUN_API_KEY` in your environment, then:
14
+
15
+ ```python
16
+ from agentrun import Agent
17
+
18
+ agent = Agent(
19
+ name="Company verifier",
20
+ instructions="Verify the company and cite primary sources.",
21
+ )
22
+ run = agent.run("Verify acme.example")
23
+ result = agent.wait(run.id)
24
+
25
+ print(result.status) # "completed", "failed", "cancelled", or "blocked"
26
+ print(result.markdown) # the report, or None
27
+ ```
28
+
29
+ `Agent` saves itself on the first call that needs it. Save `agent.id` and pass `Agent(agent_id=...)` next time to reuse it. Two things to know about `wait`. It returns for every terminal status, so check `result.status` before using the report. It gives up after 10 minutes by default and raises `PollTimeoutError`, but the run keeps going on the server. Call `agent.get_run(run.id)` later to fetch it.
30
+
31
+ ## Build an agent from a description
32
+
33
+ ```python
34
+ agent = Agent.create("Research companies and cite sources.")
35
+ # Save agent.build_id if this process might stop.
36
+ agent.wait_until_ready() # builds can take a few minutes
37
+ run = agent.run("Research acme.example")
38
+ ```
39
+
40
+ Resume an unfinished build with `Agent(build_id=...)`. Any run method waits for the build first. A build that fails raises `BuildError`; read `error.build.error` for the reason.
41
+
42
+ ## Async
43
+
44
+ Every class has an async twin with the same methods. Works under asyncio and trio.
45
+
46
+ ```python
47
+ from agentrun import AsyncAgent
48
+
49
+ async def main() -> None:
50
+ agent = AsyncAgent(agent_id="agent-123")
51
+ run = await agent.run("Verify acme.example")
52
+ result = await agent.wait(run.id)
53
+ ```
54
+
55
+ ## Statuses
56
+
57
+ The API reports progress in its own vocabulary. The SDK maps it to a short list so `is_done` and your code stay simple. The original value is always in `raw_status`.
58
+
59
+ | `run.status` | Means | Done |
60
+ | --- | --- | --- |
61
+ | `queued` | Accepted, not started. | no |
62
+ | `moderation` | Content check in progress. | no |
63
+ | `running` | Working. | no |
64
+ | `blocked` | Needs your input, or content was rejected. | yes |
65
+ | `completed` | Report is ready. | yes |
66
+ | `failed` | Ran and failed, or never ran. | yes |
67
+ | `cancelled` | Cancelled, deleted, or superseded. | yes |
68
+
69
+ Batches use `queued`, `running`, `completed`, `completed_with_errors`, `failed`, and `cancelled`. Batch rows use `queued`, `running`, `completed`, and `failed`. A status the SDK does not recognise passes through unchanged.
70
+
71
+ ## Structured output
72
+
73
+ Pass a pydantic model or a JSON schema dict as `output_schema`. The result's `structured_output` is a dict; `output_as` validates it into your model.
74
+
75
+ ```python
76
+ from pydantic import BaseModel
77
+
78
+ class Finding(BaseModel):
79
+ verified: bool
80
+ evidence: str
81
+
82
+ run = agent.run("Verify acme.example", output_schema=Finding)
83
+ finding = agent.wait(run.id).output_as(Finding)
84
+ ```
85
+
86
+ ## Batches
87
+
88
+ Run up to 100 inputs at once. The SDK checks the limits before sending anything.
89
+
90
+ ```python
91
+ batch = agent.batch(
92
+ ["Verify acme.example", "Verify beta.example"],
93
+ name="Q3 checks",
94
+ max_concurrent=5,
95
+ idempotency_key="q3-checks-001",
96
+ )
97
+ done = agent.wait_batch(batch.id)
98
+ for row in agent.get_batch_results(batch.id).results:
99
+ print(row.question, row.status, row.run_id)
100
+ ```
101
+
102
+ A CSV works too. Name the column that holds each input. The other columns come back as `row.row_data`.
103
+
104
+ ```python
105
+ from agentrun import BatchCSV
106
+
107
+ batch = agent.batch(BatchCSV("companies.csv", input_column="website"))
108
+ ```
109
+
110
+ `BatchCSV` accepts a path, CSV text, bytes, or an open file. Files are limited to 1 MB.
111
+
112
+ `wait_batch` returns when the batch completes, fails, is cancelled, or pauses for attention. Check `batch.paused_reason`. `get_batch(id, include_rows=False)` skips per-row data on large batches. `cancel_batch` stops new rows; rows already running finish. `retry_batch` re-runs failed rows and spends credits.
113
+
114
+ ## Loops
115
+
116
+ A loop is a batch that repeats on a schedule. The first run starts immediately.
117
+
118
+ ```python
119
+ loop = agent.loop(
120
+ "Check acme.example for new filings",
121
+ {"frequency": "weekly", "schedule_config": {"hour": 9, "minute": 0, "day_of_week": 0}, "timezone": "America/New_York"},
122
+ name="Weekly filings",
123
+ max_credits=20,
124
+ )
125
+ history = agent.get_loop(loop.id)
126
+ agent.pause_loop(loop.id)
127
+ agent.delete_loop(loop.id, cancel_active=True)
128
+ ```
129
+
130
+ `frequency` is `daily`, `weekly`, or `monthly`. Weekly uses `day_of_week` with 0 as Monday. Monthly uses `day_of_month` from 1 to 28. `max_credits` caps each occurrence, not the lifetime total. Inputs are frozen at creation; `update_loop` changes only the name and cadence.
131
+
132
+ ## Campaigns
133
+
134
+ A campaign is a multi-stage plan. Save it as a draft, review it, then approve it with the digest of the plan you reviewed.
135
+
136
+ ```python
137
+ draft = agent.create_campaign(plan)
138
+ # read draft.stages and draft.deliverables
139
+ started = agent.approve_campaign(draft.id, draft.review_digest)
140
+ final = agent.wait_campaign(started.id)
141
+ artifact = agent.download_campaign_artifact(final.id, "companies")
142
+ open(artifact.filename or "companies.xlsx", "wb").write(artifact.content)
143
+ ```
144
+
145
+ An agent configured with `run_mode="campaign"` and a `campaign_plan` can start its template directly with `agent.campaign({"goal": "..."})`. That call starts work immediately. Approving with a stale digest raises `ConflictError`.
146
+
147
+ ## Skills and integrations
148
+
149
+ ```python
150
+ from agentrun import AgentRun
151
+
152
+ client = AgentRun()
153
+ skills = client.list_skills(query="company")
154
+ tools = client.list_integrations(query="company")
155
+
156
+ agent = client.agent(
157
+ name="Company verifier",
158
+ instructions="Verify the company and cite evidence.",
159
+ skills=[s.name for s in skills.skills],
160
+ tools=[t.name for t in tools.tools if not t.always_on],
161
+ )
162
+ ```
163
+
164
+ Save your own Markdown as a skill with `client.create_skill(name, description, content)`. Creating a skill with an existing name updates it; the response has `created=False`.
165
+
166
+ ## Errors
167
+
168
+ Every error inherits from `AgentRunError`.
169
+
170
+ | Error | When |
171
+ | --- | --- |
172
+ | `AuthenticationError` | 401. Bad or missing API key. |
173
+ | `PermissionDeniedError` | 403. The key lacks a scope or does not own the resource. |
174
+ | `NotFoundError` | 404. |
175
+ | `ConflictError` | 409. Stale review digest, or a loop already running. |
176
+ | `UnprocessableEntityError` | 422. `error.body["detail"]` lists the bad fields. |
177
+ | `RateLimitError` | 429. |
178
+ | `ServiceUnavailableError` | 503. Down, or the feature is not enabled for your account. |
179
+ | `APIConnectionError` | No HTTP response at all. `APITimeoutError` for a slow one. |
180
+ | `InvalidResponseError` | 2xx but not the JSON the SDK expects. Usually a wrong `base_url`. |
181
+ | `BuildError` | An agent build failed. |
182
+ | `PollTimeoutError` | A `wait*` call hit its local deadline. Also a `TimeoutError`. |
183
+
184
+ `APIError` subclasses carry `status_code`, `body`, and `request_id`. Include the request ID when you report a problem. The SDK checks the limits the API enforces before sending anything, such as skill content of at least 50 characters or `max_cost_usd` between 0.5 and 1000, and raises `ValueError` locally. The SDK never retries a request on its own. Pass the same `idempotency_key` to `run`, `batch`, `loop`, or `create_campaign` when you retry, so the server does not start the work twice.
185
+
186
+ ## Configuration
187
+
188
+ | Setting | Argument | Environment variable | Default |
189
+ | --- | --- | --- | --- |
190
+ | API key | `api_key=` | `AGENTRUN_API_KEY` | required |
191
+ | API root | `base_url=` | `AGENTRUN_BASE_URL` | `https://api.grep.ai/api/v2` |
192
+ | Per-request timeout | `timeout=` | | 30 seconds |
193
+
194
+ `base_url` must use HTTPS. Plain HTTP is allowed only for localhost. Redirects are never followed. Use `with AgentRun() as client:` or `async with AsyncAgentRun() as client:` to close the connection pool.
195
+
196
+ ## Types
197
+
198
+ The package ships `py.typed`. Responses are pydantic models, so `run.status` and `batch.rows` have the types your editor shows. Records keep any field the server adds later, reachable as an attribute or with `record["field"]`. `id` is always the record's ID, whatever the API calls it.
199
+
200
+ ## Development
201
+
202
+ ```sh
203
+ uv sync --group dev
204
+ uv run pytest
205
+ uv run ruff check src tests
206
+ uv run pyright
207
+ uv run mypy
208
+ uv build && uv run twine check dist/*
209
+ ```
210
+
211
+ Tests run against a fake API. Every test runs twice, once for the sync client and once for the async client.
212
+
213
+ Two tests tie the SDK to the backend:
214
+
215
+ - `tests/test_contract.py` validates every request the SDK sends, and every canned response, against `tests/fixtures/public-v2.json`, the backend's generated OpenAPI document. Refresh it with `python scripts/update_contract.py <path-or-url>` when the API changes.
216
+ - `tests/test_packaged_contract.py` starts a real local HTTP server and runs the same script the backend uses to verify packaged SDKs.
217
+
218
+ `scripts/smoke_staging.py` exercises a live deployment. It is read-only unless you pass `--run`.
@@ -0,0 +1,81 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "parcha-agentrun"
7
+ version = "0.2.0"
8
+ description = "Python SDK for AgentRun: create agents, run them, and collect results"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "httpx2>=2.12,<3",
13
+ "pydantic>=2.7,<3",
14
+ "anyio>=4.4,<5",
15
+ "typing-extensions>=4.12",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
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
+ "Programming Language :: Python :: 3.14",
26
+ "Operating System :: OS Independent",
27
+ "Typing :: Typed",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://agentrun.ai"
32
+ Documentation = "https://agentrun.ai/docs"
33
+ Repository = "https://github.com/Parcha-ai/agentrun-py"
34
+ Issues = "https://github.com/Parcha-ai/agentrun-py/issues"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "pytest>=8.3",
39
+ "pytest-cov>=6",
40
+ "mypy>=1.14",
41
+ "pyright>=1.1.400",
42
+ "ruff>=0.9",
43
+ "twine>=6",
44
+ "jsonschema>=4.23",
45
+ "types-jsonschema>=4.23",
46
+ ]
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/agentrun"]
50
+
51
+ [tool.hatch.build.targets.sdist]
52
+ include = ["src/agentrun", "tests", "scripts", "README.md", "CHANGELOG.md", "pyproject.toml"]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+ addopts = "-q"
57
+
58
+ [tool.ruff]
59
+ line-length = 110
60
+ target-version = "py310"
61
+ src = ["src", "tests"]
62
+
63
+ [tool.ruff.lint]
64
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF", "ANN", "T20"]
65
+ ignore = ["ANN401"]
66
+
67
+ [tool.ruff.lint.per-file-ignores]
68
+ "tests/*" = ["ANN", "T20"]
69
+ "scripts/*" = ["T20"]
70
+
71
+ [tool.pyright]
72
+ include = ["src", "tests"]
73
+ pythonVersion = "3.10"
74
+ typeCheckingMode = "strict"
75
+ reportMissingTypeStubs = false
76
+
77
+ [tool.mypy]
78
+ python_version = "3.10"
79
+ strict = true
80
+ files = ["src", "tests"]
81
+ plugins = ["pydantic.mypy"]
@@ -0,0 +1,61 @@
1
+ """Exercise the SDK against a live deployment. Read-only unless you pass --run.
2
+
3
+ export AGENTRUN_API_KEY=...
4
+ export AGENTRUN_BASE_URL=https://staging-api.grep.ai/api/v2
5
+ python scripts/smoke_staging.py [--run]
6
+
7
+ Never prints the key. ``--run`` starts one low-effort run on the first agent
8
+ you own and waits for it, which spends credits.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import sys
15
+
16
+ from agentrun import Agent, AgentRun, AgentRunError
17
+
18
+
19
+ def main(start_run: bool) -> int:
20
+ base = os.environ.get("AGENTRUN_BASE_URL", "(default)")
21
+ print(f"base_url: {base}")
22
+ with AgentRun() as client:
23
+ usage = client.get_usage()
24
+ print(f"usage: {usage.job_count} jobs, {usage.total_credits_used} credits in period")
25
+ skills = client.list_skills(limit=5)
26
+ print(f"skills: {skills.total} total, first: {[s.name for s in skills.skills]}")
27
+ tools = client.list_integrations(limit=5)
28
+ print(f"integrations: {tools.total} total, servers: {tools.servers[:5]}")
29
+ agents = client.list_agents()
30
+ print(f"agents you own: {len(agents)}")
31
+ page = client.list_runs(limit=3)
32
+ print(f"recent runs: {[(r.id, r.status, r.raw_status) for r in page.items]}")
33
+ if page.items:
34
+ run = client.get_run(page.items[0].id)
35
+ has_report = bool(run.markdown)
36
+ print(
37
+ f"run detail: status={run.status} raw={run.raw_status} done={run.is_done} report={has_report}"
38
+ )
39
+ batches = client.list_batches(limit=3)
40
+ print(f"recent batches: {[(b.id, b.status, b.raw_status) for b in batches.items]}")
41
+ if not start_run:
42
+ print("skipping run (pass --run to start one)")
43
+ return 0
44
+ if not agents:
45
+ print("no agent to run; create one first")
46
+ return 1
47
+ agent = Agent(client, agent_id=agents[0].id)
48
+ run = agent.run("Say hello and stop.", effort="low", idempotency_key="sdk-smoke-hello")
49
+ print(f"started run {run.id} status={run.status}")
50
+ result = agent.wait(run.id, timeout=600, interval=3)
51
+ print(f"finished run {result.id} status={result.status} raw={result.raw_status}")
52
+ print((result.markdown or "")[:300])
53
+ return 0
54
+
55
+
56
+ if __name__ == "__main__":
57
+ try:
58
+ sys.exit(main("--run" in sys.argv[1:]))
59
+ except AgentRunError as error:
60
+ print(f"failed: {type(error).__name__}: {error}")
61
+ sys.exit(1)
@@ -0,0 +1,43 @@
1
+ """Refresh tests/fixtures/public-v2.json from the backend's generated OpenAPI document.
2
+
3
+ Usage:
4
+ python scripts/update_contract.py path/to/public_openapi.json
5
+ python scripts/update_contract.py https://api.grep.ai/api/v2/openapi.json
6
+
7
+ Only the canonical v2 resource paths the SDK calls are kept. Components are
8
+ kept whole so every $ref resolves.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ import sys
16
+ import urllib.request
17
+ from pathlib import Path
18
+
19
+ KEEP = re.compile(r"^/api/v2/(agents|runs|loops|skills|integrations|batches|campaigns|usage)(/|$)")
20
+ TARGET = Path(__file__).resolve().parents[1] / "tests" / "fixtures" / "public-v2.json"
21
+
22
+
23
+ def main(source: str) -> None:
24
+ if source.startswith("http"):
25
+ with urllib.request.urlopen(source, timeout=30) as response:
26
+ document = json.load(response)
27
+ else:
28
+ document = json.loads(Path(source).read_text())
29
+ paths = {p: v for p, v in document["paths"].items() if KEEP.match(p)}
30
+ out = {
31
+ "openapi": document.get("openapi"),
32
+ "info": document.get("info"),
33
+ "paths": paths,
34
+ "components": document["components"],
35
+ }
36
+ TARGET.write_text(json.dumps(out, indent=1, sort_keys=True) + "\n")
37
+ print(f"wrote {TARGET.relative_to(Path.cwd())} with {len(paths)} paths")
38
+
39
+
40
+ if __name__ == "__main__":
41
+ if len(sys.argv) != 2:
42
+ sys.exit(__doc__)
43
+ main(sys.argv[1])