oneshot-python 0.37.0__tar.gz → 0.39.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 (50) hide show
  1. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/PKG-INFO +1 -1
  2. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/README.md +12 -2
  3. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/oneshot/__init__.py +2 -0
  4. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/oneshot/_types.py +1 -0
  5. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/oneshot/client.py +261 -78
  6. oneshot_python-0.39.0/oneshot/lookup.py +252 -0
  7. oneshot_python-0.39.0/oneshot/lookup_manifest.py +2168 -0
  8. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/pyproject.toml +1 -1
  9. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_compute.py +46 -0
  10. oneshot_python-0.39.0/tests/test_compute_manifest.py +142 -0
  11. oneshot_python-0.39.0/tests/test_declared_routes.py +110 -0
  12. oneshot_python-0.39.0/tests/test_declared_worker_reads.py +77 -0
  13. oneshot_python-0.39.0/tests/test_inbox_notifications.py +84 -0
  14. oneshot_python-0.39.0/tests/test_lookup.py +274 -0
  15. oneshot_python-0.39.0/tests/test_paid_call_proof.py +185 -0
  16. oneshot_python-0.39.0/tests/test_person_quote_credits.py +82 -0
  17. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_phones_pending.py +7 -1
  18. oneshot_python-0.39.0/tests/test_update_build.py +38 -0
  19. oneshot_python-0.37.0/tests/test_paid_call_proof.py +0 -101
  20. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/.gitignore +0 -0
  21. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/oneshot/_errors.py +0 -0
  22. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/oneshot/physical_mail.py +0 -0
  23. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/oneshot/x402.py +0 -0
  24. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/__init__.py +0 -0
  25. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/conftest.py +0 -0
  26. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_access_token.py +0 -0
  27. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_approvals.py +0 -0
  28. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_balance.py +0 -0
  29. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_browser.py +0 -0
  30. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_budgets.py +0 -0
  31. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_charge_amount.py +0 -0
  32. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_compute_outputs.py +0 -0
  33. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_domains.py +0 -0
  34. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_domains_signed.py +0 -0
  35. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_email_payload.py +0 -0
  36. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_emergency_error.py +0 -0
  37. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_hunts.py +0 -0
  38. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_inbound_voice.py +0 -0
  39. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_linkedin.py +0 -0
  40. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_max_cost_header.py +0 -0
  41. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_payment_rejection.py +0 -0
  42. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_physical_mail.py +0 -0
  43. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_poll_backoff.py +0 -0
  44. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_request_id.py +0 -0
  45. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_signed_job_and_analytics.py +0 -0
  46. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_tag_receipt_value.py +0 -0
  47. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_top_up_credits.py +0 -0
  48. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_wait_false.py +0 -0
  49. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/tests/test_x402.py +0 -0
  50. {oneshot_python-0.37.0 → oneshot_python-0.39.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oneshot-python
3
- Version: 0.37.0
3
+ Version: 0.39.0
4
4
  Summary: Core Python SDK for the OneShot API — HTTP client with x402 payment handling — 48 tools
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -244,7 +244,7 @@ except LinkedInConnectRequiredError:
244
244
 
245
245
  Account reads stop offering reconnect after confirming upstream deletion.
246
246
  Missing accounts are excluded from the active list; `include_revoked=True`
247
- lets you inspect their `deleted_upstream` status and `reconnect_required=False`.
247
+ lets you inspect their `connection_deleted` status and `reconnect_required=False`.
248
248
  Authentication failures and timeouts remain ordinary errors and do not instruct
249
249
  the consumer to create a new connection.
250
250
 
@@ -315,6 +315,16 @@ client.hunts.leaderboard() # public predictor leaderboard
315
315
 
316
316
  Async variants prefix the method with `a` (`alist`, `astatus`, `apredict`, `acapture`, `acollection`, `aleaderboard`). `list`/`status`/`collection`/`leaderboard` are free reads and work for both wallet and access-token sessions. `predict`/`capture` debit a stake and require Soul Hunt's wallet-signed write proof, which an access-token (credits-only) session cannot produce — those two raise `ValidationError` locally rather than reaching the network. The client never forwards the OneShot bearer token to Soul Hunt; only the wallet address and, for wallet sessions, the signed `x-agent-proof` go out.
317
317
 
318
+ ## Declared routes and inbox methods (#1273)
319
+
320
+ Every route in the API registry states an agent tool. The ones without a typed method are in the lookup
321
+ manifest and reached with `call_lookup(name, **fields)`, for example `call_lookup("company_filings", cik="320193")`
322
+ (`acall_lookup` for async): a free route is a signed
323
+ call, a paid one goes through `call_tool` and its quote. LangChain and GAME build their tools from the same
324
+ manifest. The inbox, SMS inbox and notification routes also have methods: `inbox`, `inbox_get`, `sms_inbox`,
325
+ `sms_inbox_get`, `notifications`, `mark_notification_read`, `mark_all_notifications_read` (each with an `a`
326
+ async form).
327
+
318
328
  ## Requirements
319
329
 
320
330
  - Python 3.10+
@@ -324,7 +334,7 @@ Async variants prefix the method with `a` (`alist`, `astatus`, `apredict`, `acap
324
334
  ## Links
325
335
 
326
336
  - [Documentation](https://docs.oneshotagent.com/sdk/installation#install-via-pip-python)
327
- - [LangChain integration](https://pypi.org/project/langchain-oneshot/): 48 tools as LangChain BaseTool
337
+ - [LangChain integration](https://pypi.org/project/langchain-oneshot/): 48 tools plus the lookup tools as LangChain BaseTool
328
338
  - [GAME plugin](https://pypi.org/project/game-plugin-oneshot/): Virtuals Protocol integration
329
339
  - [TypeScript SDK](https://www.npmjs.com/package/@oneshot-agent/sdk)
330
340
  - [MCP Server](https://www.npmjs.com/package/@oneshot-agent/mcp-server)
@@ -39,9 +39,11 @@ from oneshot._types import (
39
39
  DomainPoolStatusResult,
40
40
  )
41
41
  from oneshot.client import OneShotClient
42
+ from oneshot.lookup import lookup_tools
42
43
  from oneshot.x402 import sign_payment_authorization
43
44
 
44
45
  __all__ = [
46
+ "lookup_tools",
45
47
  "PhysicalMail", "PostalAddress", "MailQuote", "MailOrder",
46
48
  "OneShotClient",
47
49
  "OneShotError",
@@ -127,6 +127,7 @@ class ComputeCancelResult(TypedDict, total=False):
127
127
  goal_id: str
128
128
  status: str
129
129
  remaining_budget: str
130
+ message: str
130
131
 
131
132
 
132
133
  class ComputeTaskResponseResult(TypedDict, total=False):
@@ -44,6 +44,7 @@ from oneshot._types import (
44
44
  DomainPoolListResult,
45
45
  DomainPoolStatusResult,
46
46
  )
47
+ from oneshot.lookup import PAID_PRICING, find_lookup_tool, lookup_query, lookup_request
47
48
  from oneshot.x402 import (
48
49
  build_zero_cost_authorization,
49
50
  encode_payment_header,
@@ -59,13 +60,13 @@ try:
59
60
 
60
61
  SDK_VERSION = _pkg_version("oneshot-python")
61
62
  except Exception: # pragma: no cover - editable/source runs without dist metadata
62
- SDK_VERSION = "0.37.0"
63
+ SDK_VERSION = "0.39.0"
63
64
 
64
65
  _BASE_URL = "https://win.oneshotagent.com"
65
66
  _HUNT_BASE_URL = "https://api.soulhunt.ai"
66
67
  _ACCESS_TOKEN_RE = re.compile(r"^oneshot_[0-9a-f]{64}$")
67
68
  # Endpoints whose admission reads a "submit"-scoped agent proof (same list as the TypeScript SDK).
68
- _DURABLE_ADMISSION_ENDPOINT = re.compile(r"(enrich/(profile|email)|verify/email|physical-mail/send)$")
69
+ _DURABLE_ADMISSION_ENDPOINT = re.compile(r"(lookup/person-(profile|email|email-status)|lookup/place-permits|physical-mail/send)$")
69
70
  _CHAIN_ID = 8453
70
71
  _USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
71
72
 
@@ -74,6 +75,7 @@ _USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
74
75
  _POLL_BACKOFF = (0.3, 0.6, 1.0, 2.0) # seconds
75
76
  _POLL_RETRY_BASE = 2.0 # seconds, transient-error retry backoff base
76
77
  _MAX_POLL_RETRIES = 3
78
+ _BUILD_TRANSPORT_KWARGS = ("max_cost", "timeout_sec", "wait", "approval_id", "idempotency_key")
77
79
 
78
80
 
79
81
  def _build_email_payload(
@@ -159,6 +161,24 @@ def _parse_policy_rejection(resp: Any) -> Optional[OneShotError]:
159
161
  return None
160
162
 
161
163
 
164
+ def _parse_invalid_proof(resp: Any) -> Optional[tuple[str, str]]:
165
+ """``(reason, message)`` when ``resp`` is the API's 401 ``invalid_proof`` refusal (#1437), else None."""
166
+ if resp.status_code != 401:
167
+ return None
168
+ try:
169
+ data = resp.json()
170
+ except Exception: # noqa: BLE001 - non-JSON body
171
+ return None
172
+ if not isinstance(data, dict) or data.get("error") != "invalid_proof":
173
+ return None
174
+ reason = data.get("reason")
175
+ message = data.get("message")
176
+ return (
177
+ reason if isinstance(reason, str) else "invalid",
178
+ message if isinstance(message, str) else "The agent proof could not be verified.",
179
+ )
180
+
181
+
162
182
  def _parse_budget_rejection(resp: Any) -> Optional[BudgetExceededError]:
163
183
  """Map a 403 ``budget_exceeded`` body onto :class:`BudgetExceededError`.
164
184
 
@@ -666,6 +686,29 @@ class OneShotClient:
666
686
  if self.debug:
667
687
  print(f"[OneShot] {msg}")
668
688
 
689
+ async def _post_proved(self, client: httpx.AsyncClient, url: str, build_headers: Any, payload: dict[str, Any]) -> Any:
690
+ """POST a leg that proves the wallet. ``build_headers`` returns the headers
691
+ with a freshly signed proof each time it is called.
692
+
693
+ The API refuses a request whose proof it rejected, saying why (#1437). A
694
+ proof outside its window or sent twice is cured by signing a new one, so
695
+ the leg is sent again once. Any other reason, or a second refusal, is
696
+ raised with the reason. Nothing is paid in answer to this refusal.
697
+ """
698
+ resp = await client.post(url, headers=build_headers(), json=payload)
699
+ refusal = _parse_invalid_proof(resp)
700
+ if refusal is None:
701
+ return resp
702
+ reason, message = refusal
703
+ if reason in ("expired", "already_used"):
704
+ self._log(f"Wallet proof refused ({reason}); signing a new proof and retrying once")
705
+ resp = await client.post(url, headers=build_headers(), json=payload)
706
+ refusal = _parse_invalid_proof(resp)
707
+ if refusal is None:
708
+ return resp
709
+ reason, message = refusal
710
+ raise OneShotError(f"The API refused the wallet proof ({reason}): {message}")
711
+
669
712
  async def aensure_budgets_synced(self) -> None:
670
713
  """Push ``budgets`` / ``alert_email`` to the server once (async).
671
714
 
@@ -774,6 +817,44 @@ class OneShotClient:
774
817
  )
775
818
  )
776
819
 
820
+ def call_lookup(self, name: str, /, **params: Any) -> Any:
821
+ """Call a lookup tool by its manifest name, e.g. ``call_lookup("company_filings", cik="320193")``.
822
+
823
+ The tool's name is positional only, so a tool whose own input is called
824
+ ``name`` can be given it: ``call_lookup("company_legal_identifier", name="Apple Inc.")``.
825
+
826
+ Sends the params through :meth:`call_tool`. Params that are ``None`` are
827
+ left out. An unknown name raises ``ValidationError``; see
828
+ :func:`oneshot.lookup.lookup_tools` for the names.
829
+ """
830
+ entry = find_lookup_tool(name)
831
+ method, pricing, path, payload = lookup_request(entry, params)
832
+ if pricing in PAID_PRICING:
833
+ return self.call_tool(path, payload)
834
+ return asyncio.get_event_loop().run_until_complete(self._acall_free_lookup(method, path, payload))
835
+
836
+ async def acall_lookup(self, name: str, /, **params: Any) -> Any:
837
+ """Async form of :meth:`call_lookup`, sent through :meth:`acall_tool`."""
838
+ entry = find_lookup_tool(name)
839
+ method, pricing, path, payload = lookup_request(entry, params)
840
+ if pricing in PAID_PRICING:
841
+ return await self.acall_tool(path, payload)
842
+ return await self._acall_free_lookup(method, path, payload)
843
+
844
+ async def _acall_free_lookup(self, method: str, path: str, payload: dict[str, Any]) -> Any:
845
+ """A route with no payment of its own (a goal read or control, an inbox, a notification, a number or
846
+ account setting): a GET sends the params as the query, the other methods are write-scoped signed calls;
847
+ the ``{"success", "data"}`` envelope is stripped as the compute methods do."""
848
+ if method == "GET":
849
+ return self._unwrap_data(await self.acall_free_get(path, lookup_query(payload) or None))
850
+ if method == "PATCH":
851
+ return self._unwrap_data(await self.acall_free_patch(path, payload, scope="write"))
852
+ if method == "PUT":
853
+ return self._unwrap_data(await self.acall_free_put(path, payload))
854
+ if method == "DELETE":
855
+ return self._unwrap_data(await self.acall_free_delete(path, scope="write"))
856
+ return self._unwrap_data(await self.acall_free_post(path, payload, scope="write"))
857
+
777
858
  def wait_for_result(
778
859
  self,
779
860
  request_id: str,
@@ -870,11 +951,16 @@ class OneShotClient:
870
951
  # before they ever get to the 402 dance unless x-quote-id rides
871
952
  # on this very first attempt, so an already-obtained quote_id
872
953
  # must be sent from the start, not just on the paid retry.
873
- initial_headers = self._with_approval(self._headers(max_cost=max_cost, idempotency_key=idempotency_key), approval_id)
874
- self._prove_wallet(initial_headers, endpoint)
875
- if quote_id:
876
- initial_headers["x-quote-id"] = quote_id
877
- resp = await client.post(url, headers=initial_headers, json=payload)
954
+ initial_headers_base = self._with_approval(self._headers(max_cost=max_cost, idempotency_key=idempotency_key), approval_id)
955
+
956
+ def _initial_headers() -> dict[str, str]:
957
+ headers = dict(initial_headers_base)
958
+ self._prove_wallet(headers, endpoint)
959
+ if quote_id:
960
+ headers["x-quote-id"] = quote_id
961
+ return headers
962
+
963
+ resp = await self._post_proved(client, url, _initial_headers, payload)
878
964
 
879
965
  # The spend-budget gate rejects at quote time (403) so nothing is
880
966
  # signed; surface it as the typed error.
@@ -1002,16 +1088,21 @@ class OneShotClient:
1002
1088
  resource=parsed_req.get("resource"),
1003
1089
  extensions=parsed_req.get("extensions"),
1004
1090
  )
1005
- headers = {
1091
+ zero_cost_base = {
1006
1092
  **self._with_approval(self._headers(max_cost=max_cost, idempotency_key=idempotency_key), approval_id),
1007
1093
  "payment-signature": encode_payment_header(auth),
1008
1094
  }
1009
- # The zero-cost authorization is an unsigned stub, so this leg
1010
- # still has to prove the wallet for the credits to be spent.
1011
- self._prove_wallet(headers, endpoint)
1012
- if quote_id:
1013
- headers["x-quote-id"] = quote_id
1014
- resp2 = await client.post(url, headers=headers, json=payload)
1095
+
1096
+ def _zero_cost_headers() -> dict[str, str]:
1097
+ # The zero-cost authorization is an unsigned stub, so this leg
1098
+ # still has to prove the wallet for the credits to be spent.
1099
+ headers = dict(zero_cost_base)
1100
+ self._prove_wallet(headers, endpoint)
1101
+ if quote_id:
1102
+ headers["x-quote-id"] = quote_id
1103
+ return headers
1104
+
1105
+ resp2 = await self._post_proved(client, url, _zero_cost_headers, payload)
1015
1106
  else:
1016
1107
  auth = sign_payment_authorization(
1017
1108
  private_key=self._private_key,
@@ -1429,7 +1520,12 @@ class OneShotClient:
1429
1520
  return self._unwrap_data(await self.acall_free_get(f"/v1/compute/{goal_id}/budget"))
1430
1521
 
1431
1522
  def cancel_compute_goal(self, goal_id: str, reason: Optional[str] = None) -> Any:
1432
- """Cancel a goal; remaining budget is credited. Blocking."""
1523
+ """Cancel a goal; remaining budget is credited. Blocking.
1524
+
1525
+ When reservations are still in flight the server answers 202 and the
1526
+ refund is deferred: the result then has ``status == "pending_settlement"``
1527
+ (no exception is raised).
1528
+ """
1433
1529
  return asyncio.get_event_loop().run_until_complete(
1434
1530
  self.acancel_compute_goal(goal_id, reason)
1435
1531
  )
@@ -1437,11 +1533,17 @@ class OneShotClient:
1437
1533
  async def acancel_compute_goal(
1438
1534
  self, goal_id: str, reason: Optional[str] = None
1439
1535
  ) -> Any:
1440
- """Cancel a goal; remaining budget is credited. Async."""
1536
+ """Cancel a goal; remaining budget is credited. Async.
1537
+
1538
+ A 202 ``pending_settlement`` answer is returned as a result, not raised.
1539
+ """
1441
1540
  payload: dict[str, Any] = {"reason": reason} if reason is not None else {}
1442
- return self._unwrap_data(
1541
+ result = self._unwrap_data(
1443
1542
  await self.acall_free_post(f"/v1/compute/{goal_id}/cancel", payload, scope="write")
1444
1543
  )
1544
+ if isinstance(result, dict) and result.get("status") == "pending_settlement":
1545
+ result = {"goal_id": goal_id, **{k: v for k, v in result.items() if k != "success"}}
1546
+ return result
1445
1547
 
1446
1548
  def respond_to_compute_task(
1447
1549
  self,
@@ -1510,7 +1612,11 @@ class OneShotClient:
1510
1612
  )
1511
1613
 
1512
1614
  async def afund_compute_goal(self, goal_id: str, amount: float) -> Any:
1513
- """Top up budget on a recurring goal. Quote+pay (x402) via `acall_tool`."""
1615
+ """Top up budget on a recurring goal, or on any goal that ended with
1616
+ ``end_reason == "budget_exhausted"`` (it is set back to ``active`` and woken, keeping
1617
+ its tasks and workspace). Other endings and passed deadlines are refused; a refund
1618
+ still settling answers a retryable ``refund_pending`` (409). Quote+pay (x402) via
1619
+ `acall_tool`."""
1514
1620
  if not amount or amount <= 0:
1515
1621
  raise ValidationError("amount must be a positive number", "amount")
1516
1622
  return await self.acall_tool(
@@ -1967,63 +2073,63 @@ class OneShotClient:
1967
2073
  approval_id = kwargs.pop("approval_id", None)
1968
2074
  return await self.acall_tool("/v1/tools/sms/send", {"message": message, "to_number": to_number, **kwargs}, approval_id=approval_id)
1969
2075
 
1970
- def people_search(self, **kwargs: Any) -> Any:
2076
+ def person_find(self, **kwargs: Any) -> Any:
1971
2077
  """Search for people by job title, company, etc. Blocking."""
1972
- return self.call_tool("/v1/tools/research/people", kwargs)
2078
+ return self.call_tool("/v1/tools/find/person", kwargs)
1973
2079
 
1974
- async def apeople_search(self, **kwargs: Any) -> Any:
2080
+ async def aperson_find(self, **kwargs: Any) -> Any:
1975
2081
  """Search for people by job title, company, etc. Async."""
1976
- return await self.acall_tool("/v1/tools/research/people", kwargs)
2082
+ return await self.acall_tool("/v1/tools/find/person", kwargs)
1977
2083
 
1978
- def enrich_profile(self, **kwargs: Any) -> Any:
2084
+ def person_profile(self, **kwargs: Any) -> Any:
1979
2085
  """Enrich a person's profile from LinkedIn URL, email, or name. Blocking."""
1980
- return self.call_tool("/v1/tools/enrich/profile", kwargs)
2086
+ return self.call_tool("/v1/tools/lookup/person-profile", kwargs)
1981
2087
 
1982
- async def aenrich_profile(self, **kwargs: Any) -> Any:
2088
+ async def aperson_profile(self, **kwargs: Any) -> Any:
1983
2089
  """Enrich a person's profile from LinkedIn URL, email, or name. Async."""
1984
- return await self.acall_tool("/v1/tools/enrich/profile", kwargs)
2090
+ return await self.acall_tool("/v1/tools/lookup/person-profile", kwargs)
1985
2091
 
1986
- def company_search(self, **kwargs: Any) -> Any:
2092
+ def company_find(self, **kwargs: Any) -> Any:
1987
2093
  """Search for companies by name, domain, industry, location, size, etc. Blocking."""
1988
- return self.call_tool("/v1/tools/research/company", kwargs)
2094
+ return self.call_tool("/v1/tools/find/company", kwargs)
1989
2095
 
1990
- async def acompany_search(self, **kwargs: Any) -> Any:
2096
+ async def acompany_find(self, **kwargs: Any) -> Any:
1991
2097
  """Search for companies by name, domain, industry, location, size, etc. Async."""
1992
- return await self.acall_tool("/v1/tools/research/company", kwargs)
2098
+ return await self.acall_tool("/v1/tools/find/company", kwargs)
1993
2099
 
1994
- def enrich_company(self, **kwargs: Any) -> Any:
2100
+ def company_profile(self, **kwargs: Any) -> Any:
1995
2101
  """Enrich a company from domain, name, LinkedIn URL, or ticker. Blocking."""
1996
- return self.call_tool("/v1/tools/enrich/company", kwargs)
2102
+ return self.call_tool("/v1/tools/lookup/company-profile", kwargs)
1997
2103
 
1998
- async def aenrich_company(self, **kwargs: Any) -> Any:
2104
+ async def acompany_profile(self, **kwargs: Any) -> Any:
1999
2105
  """Enrich a company from domain, name, LinkedIn URL, or ticker. Async."""
2000
- return await self.acall_tool("/v1/tools/enrich/company", kwargs)
2106
+ return await self.acall_tool("/v1/tools/lookup/company-profile", kwargs)
2001
2107
 
2002
- def local_search(self, **kwargs: Any) -> Any:
2108
+ def business_find(self, **kwargs: Any) -> Any:
2003
2109
  """Discover local businesses (restaurants, contractors, practices) by category/keywords x location. Blocking."""
2004
- return self.call_tool("/v1/tools/local/search", kwargs)
2110
+ return self.call_tool("/v1/tools/find/business", kwargs)
2005
2111
 
2006
- async def alocal_search(self, **kwargs: Any) -> Any:
2112
+ async def abusiness_find(self, **kwargs: Any) -> Any:
2007
2113
  """Discover local businesses (restaurants, contractors, practices) by category/keywords x location. Async."""
2008
- return await self.acall_tool("/v1/tools/local/search", kwargs)
2114
+ return await self.acall_tool("/v1/tools/find/business", kwargs)
2009
2115
 
2010
- def local_resolve(self, name: str, **kwargs: Any) -> Any:
2116
+ def business_resolve(self, name: str, **kwargs: Any) -> Any:
2011
2117
  """Resolve a business name + one locating field (address/city/postal_code/phone) to its domain, phone, status. Blocking."""
2012
- return self.call_tool("/v1/tools/local/resolve", {"name": name, **kwargs})
2118
+ return self.call_tool("/v1/tools/resolve/business", {"name": name, **kwargs})
2013
2119
 
2014
- async def alocal_resolve(self, name: str, **kwargs: Any) -> Any:
2120
+ async def abusiness_resolve(self, name: str, **kwargs: Any) -> Any:
2015
2121
  """Resolve a business name + one locating field (address/city/postal_code/phone) to its domain, phone, status. Async."""
2016
- return await self.acall_tool("/v1/tools/local/resolve", {"name": name, **kwargs})
2122
+ return await self.acall_tool("/v1/tools/resolve/business", {"name": name, **kwargs})
2017
2123
 
2018
- def gov_solicitations(self, naics: list[str], **kwargs: Any) -> Any:
2124
+ def record_solicitations_find(self, naics: list[str], **kwargs: Any) -> Any:
2019
2125
  """Federal Sources Sought / Presolicitation notices (SAM.gov) by NAICS code with the contracting officer's contact. Result carries `data_as_of` (snapshot behind the rows). Blocking."""
2020
- return self.call_tool("/v1/tools/gov/solicitations", {"naics": naics, **kwargs})
2126
+ return self.call_tool("/v1/tools/find/record-solicitations", {"naics": naics, **kwargs})
2021
2127
 
2022
- async def agov_solicitations(self, naics: list[str], **kwargs: Any) -> Any:
2128
+ async def arecord_solicitations_find(self, naics: list[str], **kwargs: Any) -> Any:
2023
2129
  """Federal Sources Sought / Presolicitation notices (SAM.gov) by NAICS code with the contracting officer's contact. Result carries `data_as_of` (snapshot behind the rows). Async."""
2024
- return await self.acall_tool("/v1/tools/gov/solicitations", {"naics": naics, **kwargs})
2130
+ return await self.acall_tool("/v1/tools/find/record-solicitations", {"naics": naics, **kwargs})
2025
2131
 
2026
- def find_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
2132
+ def person_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
2027
2133
  """Find a person's email address. Blocking."""
2028
2134
  payload: dict[str, Any] = {"company_domain": company_domain, **kwargs}
2029
2135
  if full_name:
@@ -2032,9 +2138,9 @@ class OneShotClient:
2032
2138
  payload["first_name"] = first_name
2033
2139
  if last_name:
2034
2140
  payload["last_name"] = last_name
2035
- return self.call_tool("/v1/tools/enrich/email", payload)
2141
+ return self.call_tool("/v1/tools/lookup/person-email", payload)
2036
2142
 
2037
- async def afind_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
2143
+ async def aperson_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
2038
2144
  """Find a person's email address. Async."""
2039
2145
  payload: dict[str, Any] = {"company_domain": company_domain, **kwargs}
2040
2146
  if full_name:
@@ -2043,63 +2149,63 @@ class OneShotClient:
2043
2149
  payload["first_name"] = first_name
2044
2150
  if last_name:
2045
2151
  payload["last_name"] = last_name
2046
- return await self.acall_tool("/v1/tools/enrich/email", payload)
2152
+ return await self.acall_tool("/v1/tools/lookup/person-email", payload)
2047
2153
 
2048
- def verify_email(self, email: str) -> Any:
2154
+ def person_email_status(self, email: str) -> Any:
2049
2155
  """Verify email deliverability. Blocking."""
2050
- return self.call_tool("/v1/tools/verify/email", {"email": email})
2156
+ return self.call_tool("/v1/tools/lookup/person-email-status", {"email": email})
2051
2157
 
2052
- async def averify_email(self, email: str) -> Any:
2158
+ async def aperson_email_status(self, email: str) -> Any:
2053
2159
  """Verify email deliverability. Async."""
2054
- return await self.acall_tool("/v1/tools/verify/email", {"email": email})
2160
+ return await self.acall_tool("/v1/tools/lookup/person-email-status", {"email": email})
2055
2161
 
2056
- def deep_research_person(self, **kwargs: Any) -> Any:
2162
+ def person_dossier(self, **kwargs: Any) -> Any:
2057
2163
  """Deep research on a person. Blocking."""
2058
- return self.call_tool("/v1/tools/research/person", kwargs)
2164
+ return self.call_tool("/v1/tools/lookup/person-dossier", kwargs)
2059
2165
 
2060
- async def adeep_research_person(self, **kwargs: Any) -> Any:
2166
+ async def aperson_dossier(self, **kwargs: Any) -> Any:
2061
2167
  """Deep research on a person. Async."""
2062
- return await self.acall_tool("/v1/tools/research/person", kwargs)
2168
+ return await self.acall_tool("/v1/tools/lookup/person-dossier", kwargs)
2063
2169
 
2064
- def social_profiles(self, **kwargs: Any) -> Any:
2170
+ def person_social_accounts(self, **kwargs: Any) -> Any:
2065
2171
  """Discover social media profiles. Blocking."""
2066
- return self.call_tool("/v1/tools/research/social", kwargs)
2172
+ return self.call_tool("/v1/tools/lookup/person-social-accounts", kwargs)
2067
2173
 
2068
- async def asocial_profiles(self, **kwargs: Any) -> Any:
2174
+ async def aperson_social_accounts(self, **kwargs: Any) -> Any:
2069
2175
  """Discover social media profiles. Async."""
2070
- return await self.acall_tool("/v1/tools/research/social", kwargs)
2176
+ return await self.acall_tool("/v1/tools/lookup/person-social-accounts", kwargs)
2071
2177
 
2072
- def article_search(self, name: str, company: str, **kwargs: Any) -> Any:
2178
+ def person_articles(self, name: str, company: str, **kwargs: Any) -> Any:
2073
2179
  """Find articles by or about a person. Blocking."""
2074
- return self.call_tool("/v1/tools/research/articles", {"name": name, "company": company, **kwargs})
2180
+ return self.call_tool("/v1/tools/lookup/person-articles", {"name": name, "company": company, **kwargs})
2075
2181
 
2076
- async def aarticle_search(self, name: str, company: str, **kwargs: Any) -> Any:
2182
+ async def aperson_articles(self, name: str, company: str, **kwargs: Any) -> Any:
2077
2183
  """Find articles by or about a person. Async."""
2078
- return await self.acall_tool("/v1/tools/research/articles", {"name": name, "company": company, **kwargs})
2184
+ return await self.acall_tool("/v1/tools/lookup/person-articles", {"name": name, "company": company, **kwargs})
2079
2185
 
2080
- def person_newsfeed(self, social_media_url: str, **kwargs: Any) -> Any:
2186
+ def person_posts(self, social_media_url: str, **kwargs: Any) -> Any:
2081
2187
  """Get recent social posts from a person. Blocking."""
2082
- return self.call_tool("/v1/tools/research/newsfeed", {"social_media_url": social_media_url, **kwargs})
2188
+ return self.call_tool("/v1/tools/lookup/person-posts", {"social_media_url": social_media_url, **kwargs})
2083
2189
 
2084
- async def aperson_newsfeed(self, social_media_url: str, **kwargs: Any) -> Any:
2190
+ async def aperson_posts(self, social_media_url: str, **kwargs: Any) -> Any:
2085
2191
  """Get recent social posts from a person. Async."""
2086
- return await self.acall_tool("/v1/tools/research/newsfeed", {"social_media_url": social_media_url, **kwargs})
2192
+ return await self.acall_tool("/v1/tools/lookup/person-posts", {"social_media_url": social_media_url, **kwargs})
2087
2193
 
2088
2194
  def person_interests(self, **kwargs: Any) -> Any:
2089
2195
  """Analyze a person's interests from their digital footprint. Blocking."""
2090
- return self.call_tool("/v1/tools/research/interests", kwargs)
2196
+ return self.call_tool("/v1/tools/lookup/person-interests", kwargs)
2091
2197
 
2092
2198
  async def aperson_interests(self, **kwargs: Any) -> Any:
2093
2199
  """Analyze a person's interests from their digital footprint. Async."""
2094
- return await self.acall_tool("/v1/tools/research/interests", kwargs)
2200
+ return await self.acall_tool("/v1/tools/lookup/person-interests", kwargs)
2095
2201
 
2096
- def person_interactions(self, social_media_url: str, **kwargs: Any) -> Any:
2202
+ def person_followers(self, social_media_url: str, **kwargs: Any) -> Any:
2097
2203
  """Get a person's social interactions. Blocking."""
2098
- return self.call_tool("/v1/tools/research/interactions", {"social_media_url": social_media_url, **kwargs})
2204
+ return self.call_tool("/v1/tools/lookup/person-followers", {"social_media_url": social_media_url, **kwargs})
2099
2205
 
2100
- async def aperson_interactions(self, social_media_url: str, **kwargs: Any) -> Any:
2206
+ async def aperson_followers(self, social_media_url: str, **kwargs: Any) -> Any:
2101
2207
  """Get a person's social interactions. Async."""
2102
- return await self.acall_tool("/v1/tools/research/interactions", {"social_media_url": social_media_url, **kwargs})
2208
+ return await self.acall_tool("/v1/tools/lookup/person-followers", {"social_media_url": social_media_url, **kwargs})
2103
2209
 
2104
2210
  def commerce_search(self, query: str, *, limit: int = 10, **kwargs: Any) -> Any:
2105
2211
  """Search for products. Blocking."""
@@ -2127,6 +2233,16 @@ class OneShotClient:
2127
2233
  """Build a website. Async."""
2128
2234
  return await self.acall_tool("/v1/tools/build", {"product": product, **kwargs})
2129
2235
 
2236
+ def update_build(self, build_id: str, changes: str, **kwargs: Any) -> Any:
2237
+ """Apply natural-language changes to an existing build (same route as build). Blocking."""
2238
+ transport = {k: kwargs.pop(k) for k in _BUILD_TRANSPORT_KWARGS if k in kwargs}
2239
+ return self.call_tool("/v1/tools/build", {"build_id": build_id, "changes": changes, **kwargs}, **transport)
2240
+
2241
+ async def aupdate_build(self, build_id: str, changes: str, **kwargs: Any) -> Any:
2242
+ """Apply natural-language changes to an existing build (same route as build). Async."""
2243
+ transport = {k: kwargs.pop(k) for k in _BUILD_TRANSPORT_KWARGS if k in kwargs}
2244
+ return await self.acall_tool("/v1/tools/build", {"build_id": build_id, "changes": changes, **kwargs}, **transport)
2245
+
2130
2246
  def get_balance(self) -> dict:
2131
2247
  """Get unified balance (on-chain USDC + credits). Blocking."""
2132
2248
  return self.call_free_get("/v1/tools/balance")
@@ -2135,6 +2251,69 @@ class OneShotClient:
2135
2251
  """Get unified balance (on-chain USDC + credits). Async."""
2136
2252
  return await self.acall_free_get("/v1/tools/balance")
2137
2253
 
2254
+ # Inboxes and notifications (#1273). Free signed reads and writes of the agent's own data.
2255
+
2256
+ @staticmethod
2257
+ def _inbox_params(**values: Any) -> Optional[dict[str, str]]:
2258
+ params = {k: ("true" if v is True else "false" if v is False else str(v)) for k, v in values.items() if v is not None}
2259
+ return params or None
2260
+
2261
+ def inbox(self, *, since: Optional[str] = None, limit: Optional[int] = None, include_body: Optional[bool] = None) -> Any:
2262
+ """List emails received by the agent, newest first. Free. Blocking."""
2263
+ return self.call_free_get("/v1/tools/inbox", self._inbox_params(since=since, limit=limit, include_body=include_body))
2264
+
2265
+ async def ainbox(self, *, since: Optional[str] = None, limit: Optional[int] = None, include_body: Optional[bool] = None) -> Any:
2266
+ """List emails received by the agent, newest first. Free. Async."""
2267
+ return await self.acall_free_get("/v1/tools/inbox", self._inbox_params(since=since, limit=limit, include_body=include_body))
2268
+
2269
+ def inbox_get(self, email_id: str) -> Any:
2270
+ """One received email with its body and attachments. Free. Blocking."""
2271
+ return self.call_free_get(f"/v1/tools/inbox/{quote(email_id, safe='')}")
2272
+
2273
+ async def ainbox_get(self, email_id: str) -> Any:
2274
+ """One received email with its body and attachments. Free. Async."""
2275
+ return await self.acall_free_get(f"/v1/tools/inbox/{quote(email_id, safe='')}")
2276
+
2277
+ def sms_inbox(self, *, since: Optional[str] = None, limit: Optional[int] = None, from_number: Optional[str] = None) -> Any:
2278
+ """List text messages received by the agent. Free. Blocking."""
2279
+ return self.call_free_get("/v1/tools/sms/inbox", self._inbox_params(since=since, limit=limit, **{"from": from_number}))
2280
+
2281
+ async def asms_inbox(self, *, since: Optional[str] = None, limit: Optional[int] = None, from_number: Optional[str] = None) -> Any:
2282
+ """List text messages received by the agent. Free. Async."""
2283
+ return await self.acall_free_get("/v1/tools/sms/inbox", self._inbox_params(since=since, limit=limit, **{"from": from_number}))
2284
+
2285
+ def sms_inbox_get(self, message_id: str) -> Any:
2286
+ """One received text message. Free. Blocking."""
2287
+ return self.call_free_get(f"/v1/tools/sms/inbox/{quote(message_id, safe='')}")
2288
+
2289
+ async def asms_inbox_get(self, message_id: str) -> Any:
2290
+ """One received text message. Free. Async."""
2291
+ return await self.acall_free_get(f"/v1/tools/sms/inbox/{quote(message_id, safe='')}")
2292
+
2293
+ def notifications(self, *, unread: Optional[bool] = None, limit: Optional[int] = None) -> Any:
2294
+ """List the agent's notifications (job completions, failures, budget warnings). Free. Blocking."""
2295
+ return self.call_free_get("/v1/tools/notifications", self._inbox_params(unread=unread, limit=limit))
2296
+
2297
+ async def anotifications(self, *, unread: Optional[bool] = None, limit: Optional[int] = None) -> Any:
2298
+ """List the agent's notifications (job completions, failures, budget warnings). Free. Async."""
2299
+ return await self.acall_free_get("/v1/tools/notifications", self._inbox_params(unread=unread, limit=limit))
2300
+
2301
+ def mark_notification_read(self, notification_id: str) -> Any:
2302
+ """Mark one notification as read. Free. Blocking."""
2303
+ return self.call_free_patch(f"/v1/tools/notifications/{quote(notification_id, safe='')}/read", scope="write")
2304
+
2305
+ async def amark_notification_read(self, notification_id: str) -> Any:
2306
+ """Mark one notification as read. Free. Async."""
2307
+ return await self.acall_free_patch(f"/v1/tools/notifications/{quote(notification_id, safe='')}/read", scope="write")
2308
+
2309
+ def mark_all_notifications_read(self) -> Any:
2310
+ """Mark every notification as read. Free. Blocking."""
2311
+ return self.call_free_patch("/v1/tools/notifications/read-all", scope="write")
2312
+
2313
+ async def amark_all_notifications_read(self) -> Any:
2314
+ """Mark every notification as read. Free. Async."""
2315
+ return await self.acall_free_patch("/v1/tools/notifications/read-all", scope="write")
2316
+
2138
2317
  # `pause`/`resume` sit behind the API's paidEndpointLimiter (a rate limiter,
2139
2318
  # not a price) — no x402 quote, so they use `call_free_post`, not `call_tool`.
2140
2319
 
@@ -2336,8 +2515,8 @@ class OneShotClient:
2336
2515
  @staticmethod
2337
2516
  def _is_phones_pending(result: Any) -> bool:
2338
2517
  """Detects whether a result is awaiting Apollo's async phone-reveal
2339
- webhook. Tolerates two shapes: the unwrapped result ``{phones_pending}``
2340
- and the deep_research_person wrapper ``{result: {phones_pending}}``.
2518
+ webhook. Accepts legacy flat results and person_dossier's
2519
+ ``result.profile.data.phones_pending``.
2341
2520
 
2342
2521
  Mirrors the TS SDK's _isPhonesPending — kept in lockstep so the worker
2343
2522
  contract works for both SDKs.
@@ -2349,6 +2528,10 @@ class OneShotClient:
2349
2528
  inner = result.get("result")
2350
2529
  if isinstance(inner, dict) and inner.get("phones_pending") is True:
2351
2530
  return True
2531
+ profile = inner.get("profile") if isinstance(inner, dict) else None
2532
+ data = profile.get("data") if isinstance(profile, dict) else None
2533
+ if isinstance(data, dict) and data.get("phones_pending") is True:
2534
+ return True
2352
2535
  return False
2353
2536
 
2354
2537
  async def _poll_for_phones(