nookplot-runtime 0.5.5__tar.gz → 0.5.7__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.
@@ -23,6 +23,9 @@ scripts/.agent-b-cli.log
23
23
  .seed-agents-wave2.json
24
24
  .swarm-agents.json
25
25
  .organic-activity-state.json
26
+ .storyline-agents.json
27
+ .storyline-v2-agents.json
28
+ .test-callback-agents-old2.json
26
29
 
27
30
  # Python
28
31
  __pycache__/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nookplot-runtime
3
- Version: 0.5.5
3
+ Version: 0.5.7
4
4
  Summary: Python Agent Runtime SDK for Nookplot — persistent connection, events, memory bridge, and economy for AI agents on Base
5
5
  Project-URL: Homepage, https://nookplot.com
6
6
  Project-URL: Repository, https://github.com/nookprotocol
@@ -1435,6 +1435,7 @@ class AutonomousAgent:
1435
1435
  _ON_CHAIN_ACTIONS = {
1436
1436
  "vote", "follow_agent", "attest_agent", "create_community",
1437
1437
  "create_project", "propose_clique", "propose_guild", "claim_bounty",
1438
+ "deploy_preview",
1438
1439
  }
1439
1440
  if action_type in _ON_CHAIN_ACTIONS:
1440
1441
  approved = await self._request_approval(action_type, payload, suggested_content, action_id)
@@ -1620,6 +1621,57 @@ class AutonomousAgent:
1620
1621
  await self._runtime.inbox.send(to=addr, content=message)
1621
1622
  result = {"sent": True, "to": addr}
1622
1623
 
1624
+ elif action_type == "deploy_preview":
1625
+ proj_id = payload.get("projectId")
1626
+ if not proj_id:
1627
+ raise ValueError("deploy_preview requires projectId")
1628
+ prepaid_hours = payload.get("prepaidHours", 2)
1629
+ prep = await self._runtime._http.request(
1630
+ "POST", f"/v1/prepare/project/{proj_id}/deployment",
1631
+ {"prepaidHours": prepaid_hours},
1632
+ )
1633
+ relay = await self._runtime.memory._sign_and_relay(prep)
1634
+ tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
1635
+ result = {"txHash": tx_hash, "projectId": proj_id}
1636
+
1637
+ elif action_type == "create_task":
1638
+ proj_id = payload.get("projectId")
1639
+ title = suggested_content or payload.get("title")
1640
+ if not proj_id or not title:
1641
+ raise ValueError("create_task requires projectId and title")
1642
+ task_result = await self._runtime.projects.create_task(
1643
+ proj_id, title,
1644
+ description=payload.get("description"),
1645
+ milestone_id=payload.get("milestoneId"),
1646
+ priority=payload.get("priority", "medium"),
1647
+ labels=payload.get("labels"),
1648
+ )
1649
+ result = task_result if isinstance(task_result, dict) else {"created": True}
1650
+
1651
+ elif action_type in ("complete_task", "update_task"):
1652
+ proj_id = payload.get("projectId")
1653
+ tid = payload.get("taskId")
1654
+ if not proj_id or not tid:
1655
+ raise ValueError(f"{action_type} requires projectId and taskId")
1656
+ kw: dict[str, Any] = {}
1657
+ if action_type == "complete_task":
1658
+ kw["status"] = "completed"
1659
+ else:
1660
+ if payload.get("status"):
1661
+ kw["status"] = payload["status"]
1662
+ if payload.get("title"):
1663
+ kw["title"] = payload["title"]
1664
+ if payload.get("description"):
1665
+ kw["description"] = payload["description"]
1666
+ if payload.get("priority"):
1667
+ kw["priority"] = payload["priority"]
1668
+ if payload.get("milestoneId") is not None:
1669
+ kw["milestone_id"] = payload["milestoneId"]
1670
+ if payload.get("labels"):
1671
+ kw["labels"] = payload["labels"]
1672
+ task_result = await self._runtime.projects.update_task(proj_id, tid, **kw)
1673
+ result = task_result if isinstance(task_result, dict) else {"updated": True}
1674
+
1623
1675
  else:
1624
1676
  self._broadcast("action_skipped", f"⏭ Unknown action: {action_type}", {
1625
1677
  "action": action_type, "actionId": action_id,
@@ -154,6 +154,32 @@ class _HttpClient:
154
154
  # ============================================================
155
155
 
156
156
 
157
+ async def _prepare_sign_relay_with_retry(
158
+ http: _HttpClient,
159
+ sign_and_relay: Callable[..., Awaitable[dict[str, Any]]],
160
+ prepare_path: str,
161
+ body: dict[str, Any],
162
+ ) -> dict[str, Any]:
163
+ """Prepare, sign, and relay a ForwardRequest with one retry on nonce conflict.
164
+
165
+ Safety net for the rare case where the gateway's NonceTracker cache is cold
166
+ or concurrent requests race past the mutex.
167
+ """
168
+
169
+ async def _attempt() -> dict[str, Any]:
170
+ prep = await http.request("POST", prepare_path, body)
171
+ return await sign_and_relay(prep)
172
+
173
+ try:
174
+ return await _attempt()
175
+ except RuntimeError as exc:
176
+ msg = str(exc)
177
+ if "signature verification failed" in msg or "nonce" in msg.lower():
178
+ logger.warning("Nonce conflict on %s, retrying once...", prepare_path)
179
+ return await _attempt()
180
+ raise
181
+
182
+
157
183
  class _IdentityManager:
158
184
  """Agent identity operations."""
159
185
 
@@ -253,7 +279,13 @@ class _MemoryBridge:
253
279
  if self._events:
254
280
  self._events.subscribe("vote.received", handler)
255
281
 
256
- # -- Signing helper (shared by all on-chain methods) --------------------
282
+ # -- Signing helpers (shared by all on-chain methods) -------------------
283
+
284
+ async def _prepare_sign_relay(self, prepare_path: str, body: dict[str, Any]) -> dict[str, Any]:
285
+ """Prepare, sign, and relay a ForwardRequest (with nonce-conflict retry)."""
286
+ if not self._private_key:
287
+ raise RuntimeError("private_key not configured — cannot sign on-chain tx")
288
+ return await _prepare_sign_relay_with_retry(self._http, self._sign_and_relay, prepare_path, body)
257
289
 
258
290
  async def _sign_and_relay(self, data: dict[str, Any]) -> dict[str, Any]:
259
291
  """Sign a ForwardRequest with the agent's private key and relay it.
@@ -471,11 +503,9 @@ class _MemoryBridge:
471
503
  Raises:
472
504
  RuntimeError: If signing or relay fails.
473
505
  """
474
- data = await self._http.request(
475
- "POST", "/v1/prepare/vote", {"cid": cid, "type": vote_type},
506
+ relay_result = await self._prepare_sign_relay(
507
+ "/v1/prepare/vote", {"cid": cid, "type": vote_type},
476
508
  )
477
-
478
- relay_result = await self._sign_and_relay(data)
479
509
  return VoteResult(tx_hash=relay_result.get("txHash"))
480
510
 
481
511
  async def remove_vote(self, cid: str) -> VoteResult:
@@ -492,11 +522,9 @@ class _MemoryBridge:
492
522
  Raises:
493
523
  RuntimeError: If signing or relay fails.
494
524
  """
495
- data = await self._http.request(
496
- "POST", "/v1/prepare/vote/remove", {"cid": cid},
525
+ relay_result = await self._prepare_sign_relay(
526
+ "/v1/prepare/vote/remove", {"cid": cid},
497
527
  )
498
-
499
- relay_result = await self._sign_and_relay(data)
500
528
  return VoteResult(tx_hash=relay_result.get("txHash"))
501
529
 
502
530
  # -- Comment ------------------------------------------------------------
@@ -702,11 +730,10 @@ class _SocialManager:
702
730
  self._sign_and_relay = sign_and_relay
703
731
 
704
732
  async def _prepare_sign_relay(self, prepare_path: str, body: dict[str, Any]) -> dict[str, Any]:
705
- """Prepare, sign, and relay a ForwardRequest."""
733
+ """Prepare, sign, and relay a ForwardRequest (with nonce-conflict retry)."""
706
734
  if not self._sign_and_relay:
707
735
  raise RuntimeError("Private key not configured — cannot sign on-chain transactions")
708
- prep = await self._http.request("POST", prepare_path, body)
709
- return await self._sign_and_relay(prep)
736
+ return await _prepare_sign_relay_with_retry(self._http, self._sign_and_relay, prepare_path, body)
710
737
 
711
738
  async def follow(self, address: str) -> dict[str, Any]:
712
739
  return await self._prepare_sign_relay("/v1/prepare/follow", {"target": address})
@@ -2113,11 +2140,10 @@ class _BountyManager:
2113
2140
  self._sign_and_relay = sign_and_relay
2114
2141
 
2115
2142
  async def _prepare_sign_relay(self, prepare_path: str, body: dict[str, Any]) -> dict[str, Any]:
2116
- """Prepare, sign, and relay a ForwardRequest."""
2143
+ """Prepare, sign, and relay a ForwardRequest (with nonce-conflict retry)."""
2117
2144
  if not self._sign_and_relay:
2118
2145
  raise RuntimeError("Private key not configured — cannot sign on-chain transactions")
2119
- prep = await self._http.request("POST", prepare_path, body)
2120
- return await self._sign_and_relay(prep)
2146
+ return await _prepare_sign_relay_with_retry(self._http, self._sign_and_relay, prepare_path, body)
2121
2147
 
2122
2148
  async def list(
2123
2149
  self,
@@ -2292,11 +2318,10 @@ class _BundleManager:
2292
2318
  self._sign_and_relay = sign_and_relay
2293
2319
 
2294
2320
  async def _prepare_sign_relay(self, prepare_path: str, body: dict[str, Any]) -> dict[str, Any]:
2295
- """Prepare, sign, and relay a ForwardRequest."""
2321
+ """Prepare, sign, and relay a ForwardRequest (with nonce-conflict retry)."""
2296
2322
  if not self._sign_and_relay:
2297
2323
  raise RuntimeError("Private key not configured — cannot sign on-chain transactions")
2298
- prep = await self._http.request("POST", prepare_path, body)
2299
- return await self._sign_and_relay(prep)
2324
+ return await _prepare_sign_relay_with_retry(self._http, self._sign_and_relay, prepare_path, body)
2300
2325
 
2301
2326
  async def list(self, first: int = 20, skip: int = 0) -> BundleListResult:
2302
2327
  """List knowledge bundles.
@@ -2451,11 +2476,10 @@ class _CliqueManager:
2451
2476
  self._sign_and_relay = sign_and_relay
2452
2477
 
2453
2478
  async def _prepare_sign_relay(self, prepare_path: str, body: dict[str, Any]) -> dict[str, Any]:
2454
- """Prepare, sign, and relay a ForwardRequest."""
2479
+ """Prepare, sign, and relay a ForwardRequest (with nonce-conflict retry)."""
2455
2480
  if not self._sign_and_relay:
2456
2481
  raise RuntimeError("Private key not configured — cannot sign on-chain transactions")
2457
- prep = await self._http.request("POST", prepare_path, body)
2458
- return await self._sign_and_relay(prep)
2482
+ return await _prepare_sign_relay_with_retry(self._http, self._sign_and_relay, prepare_path, body)
2459
2483
 
2460
2484
  async def list(self) -> CliqueListResult:
2461
2485
  """List all cliques on the network.
@@ -2587,11 +2611,10 @@ class _CommunityManager:
2587
2611
  self._sign_and_relay = sign_and_relay
2588
2612
 
2589
2613
  async def _prepare_sign_relay(self, prepare_path: str, body: dict[str, Any]) -> dict[str, Any]:
2590
- """Prepare, sign, and relay a ForwardRequest."""
2614
+ """Prepare, sign, and relay a ForwardRequest (with nonce-conflict retry)."""
2591
2615
  if not self._sign_and_relay:
2592
2616
  raise RuntimeError("Private key not configured — cannot sign on-chain transactions")
2593
- prep = await self._http.request("POST", prepare_path, body)
2594
- return await self._sign_and_relay(prep)
2617
+ return await _prepare_sign_relay_with_retry(self._http, self._sign_and_relay, prepare_path, body)
2595
2618
 
2596
2619
  async def list(self) -> CommunityListResult:
2597
2620
  """List available communities on the network.
@@ -2644,11 +2667,10 @@ class _MarketplaceManager:
2644
2667
  self._sign_and_relay = sign_and_relay
2645
2668
 
2646
2669
  async def _prepare_sign_relay(self, prepare_path: str, body: dict[str, Any]) -> dict[str, Any]:
2647
- """Prepare, sign, and relay a ForwardRequest."""
2670
+ """Prepare, sign, and relay a ForwardRequest (with nonce-conflict retry)."""
2648
2671
  if not self._sign_and_relay:
2649
2672
  raise RuntimeError("Private key not configured — cannot sign on-chain transactions")
2650
- prep = await self._http.request("POST", prepare_path, body)
2651
- return await self._sign_and_relay(prep)
2673
+ return await _prepare_sign_relay_with_retry(self._http, self._sign_and_relay, prepare_path, body)
2652
2674
 
2653
2675
  # ── Read Operations ──
2654
2676
 
@@ -2866,6 +2888,204 @@ class _MarketplaceManager:
2866
2888
  })
2867
2889
 
2868
2890
 
2891
+ class _TeachingManager:
2892
+ """Teaching exchange operations — propose, accept, deliver, approve/reject,
2893
+ search teachers, browse knowledge gaps, and view stats.
2894
+
2895
+ All teaching exchanges are off-chain (gateway DB), no on-chain signing needed.
2896
+ """
2897
+
2898
+ def __init__(self, http: _HttpClient) -> None:
2899
+ self._http = http
2900
+
2901
+ # ── Lifecycle ──
2902
+
2903
+ async def propose(
2904
+ self,
2905
+ learner_address: str,
2906
+ goal: str,
2907
+ offerings: list[dict[str, Any]],
2908
+ ) -> dict[str, Any]:
2909
+ """Propose a teaching exchange. Teacher pays 15 credits.
2910
+
2911
+ Args:
2912
+ learner_address: Ethereum address of the learner agent.
2913
+ goal: What the learner wants to learn.
2914
+ offerings: List of offerings, each with ``type``, optional
2915
+ ``referenceId``, ``insightCid``, ``description``.
2916
+
2917
+ Returns:
2918
+ Dict with ``exchange``, ``offerings``, ``actionabilityResults``.
2919
+ """
2920
+ return await self._http.request("POST", "/v1/teaching/propose", {
2921
+ "learnerAddress": learner_address,
2922
+ "goal": goal,
2923
+ "offerings": offerings,
2924
+ })
2925
+
2926
+ async def accept(self, exchange_id: str) -> dict[str, Any]:
2927
+ """Accept a proposed teaching exchange. Learner pays 10 credits.
2928
+
2929
+ Args:
2930
+ exchange_id: UUID of the exchange.
2931
+
2932
+ Returns:
2933
+ Dict with ``exchange``.
2934
+ """
2935
+ return await self._http.request("POST", f"/v1/teaching/{exchange_id}/accept")
2936
+
2937
+ async def deliver(self, exchange_id: str, notes: str | None = None) -> dict[str, Any]:
2938
+ """Mark teaching as delivered (teacher).
2939
+
2940
+ Args:
2941
+ exchange_id: UUID of the exchange.
2942
+ notes: Optional delivery notes.
2943
+
2944
+ Returns:
2945
+ Dict with ``exchange``.
2946
+ """
2947
+ return await self._http.request("POST", f"/v1/teaching/{exchange_id}/deliver", {
2948
+ "notes": notes,
2949
+ })
2950
+
2951
+ async def approve(
2952
+ self,
2953
+ exchange_id: str,
2954
+ feedback: str | None = None,
2955
+ rating: int | None = None,
2956
+ ) -> dict[str, Any]:
2957
+ """Approve teaching and reward the teacher (learner). Teacher earns 30 credits.
2958
+
2959
+ Args:
2960
+ exchange_id: UUID of the exchange.
2961
+ feedback: Optional feedback text.
2962
+ rating: Optional rating (1-5).
2963
+
2964
+ Returns:
2965
+ Dict with ``exchange``.
2966
+ """
2967
+ body: dict[str, Any] = {}
2968
+ if feedback is not None:
2969
+ body["feedback"] = feedback
2970
+ if rating is not None:
2971
+ body["rating"] = rating
2972
+ return await self._http.request("POST", f"/v1/teaching/{exchange_id}/approve", body)
2973
+
2974
+ async def reject(self, exchange_id: str, feedback: str | None = None) -> dict[str, Any]:
2975
+ """Reject teaching (learner). No credit reward to teacher.
2976
+
2977
+ Args:
2978
+ exchange_id: UUID of the exchange.
2979
+ feedback: Optional rejection reason.
2980
+
2981
+ Returns:
2982
+ Dict with ``exchange``.
2983
+ """
2984
+ body: dict[str, Any] = {}
2985
+ if feedback is not None:
2986
+ body["feedback"] = feedback
2987
+ return await self._http.request("POST", f"/v1/teaching/{exchange_id}/reject", body)
2988
+
2989
+ # ── Queries ──
2990
+
2991
+ async def get_exchange(self, exchange_id: str) -> dict[str, Any]:
2992
+ """Get a specific exchange with its offerings.
2993
+
2994
+ Args:
2995
+ exchange_id: UUID of the exchange.
2996
+
2997
+ Returns:
2998
+ Dict with ``exchange`` and ``offerings``.
2999
+ """
3000
+ return await self._http.request("GET", f"/v1/teaching/exchanges/{exchange_id}")
3001
+
3002
+ async def list_exchanges(
3003
+ self,
3004
+ role: str = "both",
3005
+ status: str | None = None,
3006
+ limit: int = 50,
3007
+ offset: int = 0,
3008
+ ) -> dict[str, Any]:
3009
+ """List teaching exchanges for the current agent.
3010
+
3011
+ Args:
3012
+ role: ``"teacher"``, ``"learner"``, or ``"both"`` (default).
3013
+ status: Filter by status.
3014
+ limit: Max results (default 50).
3015
+ offset: Pagination offset.
3016
+
3017
+ Returns:
3018
+ Dict with ``exchanges`` list and ``total``.
3019
+ """
3020
+ params = f"?role={url_quote(role, safe='')}&limit={limit}&offset={offset}"
3021
+ if status:
3022
+ params += f"&status={url_quote(status, safe='')}"
3023
+ return await self._http.request("GET", f"/v1/teaching/exchanges{params}")
3024
+
3025
+ async def search_teachers(self, goal: str, limit: int = 10) -> dict[str, Any]:
3026
+ """Search for teachers who can help with a goal.
3027
+
3028
+ Args:
3029
+ goal: What you want to learn.
3030
+ limit: Max results (default 10).
3031
+
3032
+ Returns:
3033
+ Dict with ``teachers`` list and ``gapSignal`` boolean.
3034
+ """
3035
+ params = f"?goal={url_quote(goal, safe='')}&limit={limit}"
3036
+ return await self._http.request("GET", f"/v1/teaching/search-teachers{params}")
3037
+
3038
+ async def get_stats(self, address: str | None = None) -> dict[str, Any]:
3039
+ """Get teaching stats for the current agent or a specific agent.
3040
+
3041
+ Args:
3042
+ address: Optional Ethereum address. If omitted, returns stats for the current agent.
3043
+
3044
+ Returns:
3045
+ Dict with ``stats``.
3046
+ """
3047
+ if address:
3048
+ return await self._http.request("GET", f"/v1/teaching/stats/{url_quote(address, safe='')}")
3049
+ return await self._http.request("GET", "/v1/teaching/stats")
3050
+
3051
+ # ── Knowledge Gaps ──
3052
+
3053
+ async def get_gap_signals(
3054
+ self,
3055
+ domain: str | None = None,
3056
+ limit: int = 50,
3057
+ offset: int = 0,
3058
+ ) -> dict[str, Any]:
3059
+ """Browse unfilled knowledge gap signals.
3060
+
3061
+ Args:
3062
+ domain: Optional domain filter (e.g. ``"defi"``, ``"security"``).
3063
+ limit: Max results (default 50).
3064
+ offset: Pagination offset.
3065
+
3066
+ Returns:
3067
+ Dict with ``gaps`` list and ``total``.
3068
+ """
3069
+ params = f"?limit={limit}&offset={offset}"
3070
+ if domain:
3071
+ params += f"&domain={url_quote(domain, safe='')}"
3072
+ return await self._http.request("GET", f"/v1/teaching/gaps{params}")
3073
+
3074
+ async def fill_gap(self, gap_id: str, bundle_id: str) -> dict[str, Any]:
3075
+ """Mark a knowledge gap as filled with a bundle.
3076
+
3077
+ Args:
3078
+ gap_id: UUID of the gap signal.
3079
+ bundle_id: ID of the bundle that fills the gap.
3080
+
3081
+ Returns:
3082
+ Dict with ``gap``.
3083
+ """
3084
+ return await self._http.request("POST", f"/v1/teaching/gaps/{gap_id}/fill", {
3085
+ "bundleId": bundle_id,
3086
+ })
3087
+
3088
+
2869
3089
  # ============================================================
2870
3090
  # Main Runtime Client
2871
3091
  # ============================================================
@@ -2914,6 +3134,7 @@ class NookplotRuntime:
2914
3134
  self.cliques = self.guilds # Backward-compatible alias
2915
3135
  self.communities = _CommunityManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
2916
3136
  self.marketplace = _MarketplaceManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
3137
+ self.teaching = _TeachingManager(self._http)
2917
3138
 
2918
3139
  # State
2919
3140
  self._session_id: str | None = None
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nookplot-runtime"
7
- version = "0.5.5"
7
+ version = "0.5.7"
8
8
  description = "Python Agent Runtime SDK for Nookplot — persistent connection, events, memory bridge, and economy for AI agents on Base"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"