nookplot-runtime 0.5.13__tar.gz → 0.5.14__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.
@@ -26,6 +26,7 @@ scripts/
26
26
  .storyline-v2-agents.json
27
27
  .populate-content-state.json
28
28
  .biomimicry-activity-state.json
29
+ .cypher-swarm.json
29
30
 
30
31
  # Log files from populate/seed runs
31
32
  *.log
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nookplot-runtime
3
- Version: 0.5.13
3
+ Version: 0.5.14
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
@@ -244,6 +244,21 @@ class AutonomousAgent:
244
244
  return f"team_inv:{data.get('invitationId', '')}"
245
245
  if signal_type in ("team_invitation_accepted", "team_invitation_declined"):
246
246
  return f"team_resp:{data.get('invitationId', '')}"
247
+ # Marketplace signals
248
+ if signal_type == "agreement_created":
249
+ return f"agreement_created:{data.get('agreementId', '')}"
250
+ if signal_type == "work_delivered":
251
+ return f"work_delivered:{data.get('agreementId', '')}"
252
+ if signal_type == "agreement_settled":
253
+ return f"agreement_settled:{data.get('agreementId', '')}"
254
+ if signal_type == "agreement_disputed":
255
+ return f"agreement_disputed:{data.get('agreementId', '')}"
256
+ if signal_type == "agreement_cancelled":
257
+ return f"agreement_cancelled:{data.get('agreementId', '')}"
258
+ if signal_type == "revision_requested":
259
+ return f"revision_requested:{data.get('agreementId', '')}"
260
+ if signal_type == "review_received":
261
+ return f"review_received:{data.get('agreementId', '')}"
247
262
  return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
248
263
 
249
264
  async def _handle_signal(self, data: dict[str, Any]) -> None:
@@ -342,6 +357,25 @@ class AutonomousAgent:
342
357
  self._broadcast("action_skipped", f"📋 Team invitation {signal_type}: {data.get('inviteeAddress', '?')}", {
343
358
  "signalType": signal_type, "invitationId": data.get("invitationId"),
344
359
  })
360
+ # ── Marketplace signals ──
361
+ elif signal_type == "agreement_created":
362
+ await self._handle_agreement_created(data)
363
+ elif signal_type == "work_delivered":
364
+ await self._handle_work_delivered(data)
365
+ elif signal_type == "agreement_settled":
366
+ await self._handle_agreement_settled(data)
367
+ elif signal_type == "agreement_disputed":
368
+ await self._handle_agreement_disputed(data)
369
+ elif signal_type == "agreement_cancelled":
370
+ self._broadcast("action_skipped", f"Agreement #{data.get('agreementId', '?')} cancelled", {
371
+ "signalType": signal_type, "agreementId": data.get("agreementId"),
372
+ })
373
+ elif signal_type == "revision_requested":
374
+ await self._handle_revision_requested(data)
375
+ elif signal_type == "review_received":
376
+ self._broadcast("action_skipped", f"Received {data.get('rating', '?')}-star review on Agreement #{data.get('agreementId', '?')}", {
377
+ "signalType": signal_type, "agreementId": data.get("agreementId"), "rating": data.get("rating"),
378
+ })
345
379
  else:
346
380
  self._broadcast("action_skipped", f"⏭ Unhandled signal type: {signal_type}", {
347
381
  "signalType": signal_type,
@@ -831,6 +865,187 @@ class AutonomousAgent:
831
865
  "action": "team_invitation", "invitationId": invitation_id, "error": str(exc),
832
866
  })
833
867
 
868
+ # ── Marketplace signal handlers ──
869
+
870
+ async def _parse_and_execute_action(self, text: str) -> None:
871
+ """Parse an LLM response in 'ACTION: <type>\\nPARAMS: <json>' format and dispatch."""
872
+ import re, json as _json
873
+ action_match = re.search(r"ACTION:\s*(\S+)", text, re.IGNORECASE)
874
+ if not action_match:
875
+ if self._verbose:
876
+ logger.debug("[autonomous] No action parsed from response")
877
+ return
878
+ action_type = action_match.group(1).lower()
879
+
880
+ payload: dict[str, Any] = {}
881
+ params_match = re.search(r"PARAMS:\s*(\{[\s\S]*\})", text, re.IGNORECASE)
882
+ if params_match:
883
+ try:
884
+ payload = _json.loads(params_match.group(1))
885
+ except _json.JSONDecodeError:
886
+ if self._verbose:
887
+ logger.debug("[autonomous] Failed to parse PARAMS JSON")
888
+
889
+ await self._handle_action_request({
890
+ "actionType": action_type,
891
+ "payload": payload,
892
+ })
893
+
894
+ async def _handle_agreement_created(self, data: dict[str, Any]) -> None:
895
+ """Provider received a new agreement — decide whether to start work."""
896
+ agreement_id = data.get("agreementId", "")
897
+ buyer_address = data.get("buyerAddress", "")
898
+ escrow_amount = data.get("escrowAmount", "0")
899
+ description = data.get("description", "")
900
+
901
+ try:
902
+ prompt = (
903
+ "You've been hired for a service agreement on Nookplot.\n"
904
+ f"Agreement #{agreement_id}\n"
905
+ f"Buyer: {str(buyer_address)[:12]}...\n"
906
+ f"Escrow: {escrow_amount} (locked until you deliver)\n"
907
+ + (f"Terms: {str(description)[:500]}\n" if description else "")
908
+ + "\nReview the terms and decide your next step.\n\n"
909
+ "Available actions:\n"
910
+ "- deliver_work: Submit your completed work (params: agreementId, deliveryCid)\n"
911
+ "- send_agreement_message: Send a message to the buyer (params: agreementId, messageType='general', content)\n"
912
+ "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>\n"
913
+ "Or respond with ACKNOWLEDGE if you need more time."
914
+ )
915
+
916
+ assert self._generate_response is not None
917
+ response = await self._generate_response(prompt)
918
+ text = (response or "").strip()
919
+
920
+ if "ACKNOWLEDGE" in text.upper() or not text:
921
+ self._broadcast("action_executed", f"Acknowledged Agreement #{agreement_id}", {
922
+ "action": "agreement_acknowledged", "agreementId": agreement_id,
923
+ })
924
+ return
925
+
926
+ await self._parse_and_execute_action(text)
927
+ except Exception as exc:
928
+ self._broadcast("error", f"Agreement created handling failed: {exc}", {
929
+ "action": "agreement_created", "agreementId": agreement_id, "error": str(exc),
930
+ })
931
+
932
+ async def _handle_work_delivered(self, data: dict[str, Any]) -> None:
933
+ """Buyer receives delivery — decide whether to settle, dispute, or request revision."""
934
+ agreement_id = data.get("agreementId", "")
935
+ escrow_amount = data.get("escrowAmount", "0")
936
+
937
+ try:
938
+ prompt = (
939
+ "Work has been delivered for a service agreement on Nookplot.\n"
940
+ f"Agreement #{agreement_id}\n"
941
+ f"Escrow: {escrow_amount}\n"
942
+ "\nYou are the buyer. Review the submission and decide:\n\n"
943
+ "Available actions:\n"
944
+ "- settle_agreement: Approve work and release escrow (params: agreementId)\n"
945
+ "- dispute_agreement: Dispute the delivery (params: agreementId)\n"
946
+ "- send_agreement_message: Request revision (params: agreementId, messageType='revision_request', content)\n"
947
+ "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
948
+ )
949
+
950
+ assert self._generate_response is not None
951
+ response = await self._generate_response(prompt)
952
+ text = (response or "").strip()
953
+
954
+ if text:
955
+ await self._parse_and_execute_action(text)
956
+ except Exception as exc:
957
+ self._broadcast("error", f"Work delivered handling failed: {exc}", {
958
+ "action": "work_delivered", "agreementId": agreement_id, "error": str(exc),
959
+ })
960
+
961
+ async def _handle_agreement_settled(self, data: dict[str, Any]) -> None:
962
+ """Provider receives settlement — optionally submit a review."""
963
+ agreement_id = data.get("agreementId", "")
964
+ escrow_amount = data.get("escrowAmount", "0")
965
+
966
+ try:
967
+ prompt = (
968
+ "A service agreement has been settled on Nookplot — escrow released to you.\n"
969
+ f"Agreement #{agreement_id}\n"
970
+ f"Amount received: {escrow_amount}\n\n"
971
+ "Would you like to submit a review for the buyer?\n\n"
972
+ "Available actions:\n"
973
+ '- submit_review: Leave a review (params: agreementId, rating (1-5), comment)\n'
974
+ "\nFormat:\nACTION: submit_review\n"
975
+ 'PARAMS: {"agreementId": ..., "rating": ..., "comment": "..."}\n'
976
+ "Or respond with SKIP to not leave a review."
977
+ )
978
+
979
+ assert self._generate_response is not None
980
+ response = await self._generate_response(prompt)
981
+ text = (response or "").strip()
982
+
983
+ if "SKIP" in text.upper() or not text:
984
+ self._broadcast("action_executed", f"Agreement #{agreement_id} settled (no review)", {
985
+ "action": "agreement_settled_noted", "agreementId": agreement_id,
986
+ })
987
+ return
988
+
989
+ await self._parse_and_execute_action(text)
990
+ except Exception as exc:
991
+ self._broadcast("error", f"Agreement settled handling failed: {exc}", {
992
+ "action": "agreement_settled", "agreementId": agreement_id, "error": str(exc),
993
+ })
994
+
995
+ async def _handle_agreement_disputed(self, data: dict[str, Any]) -> None:
996
+ """Agent receives dispute notification — submit evidence."""
997
+ agreement_id = data.get("agreementId", "")
998
+
999
+ try:
1000
+ prompt = (
1001
+ "A service agreement has been disputed on Nookplot.\n"
1002
+ f"Agreement #{agreement_id}\n\n"
1003
+ "You should provide evidence to support your position.\n\n"
1004
+ "Available actions:\n"
1005
+ "- send_agreement_message: Submit evidence (params: agreementId, messageType='dispute_evidence', content)\n"
1006
+ "\nFormat:\nACTION: send_agreement_message\n"
1007
+ 'PARAMS: {"agreementId": ..., "messageType": "dispute_evidence", "content": "..."}'
1008
+ )
1009
+
1010
+ assert self._generate_response is not None
1011
+ response = await self._generate_response(prompt)
1012
+ text = (response or "").strip()
1013
+
1014
+ if text:
1015
+ await self._parse_and_execute_action(text)
1016
+ except Exception as exc:
1017
+ self._broadcast("error", f"Agreement disputed handling failed: {exc}", {
1018
+ "action": "agreement_disputed", "agreementId": agreement_id, "error": str(exc),
1019
+ })
1020
+
1021
+ async def _handle_revision_requested(self, data: dict[str, Any]) -> None:
1022
+ """Provider receives revision request — revise and re-deliver."""
1023
+ agreement_id = data.get("agreementId", "")
1024
+ message = data.get("message", "")
1025
+
1026
+ try:
1027
+ prompt = (
1028
+ "The buyer has requested a revision on your delivered work.\n"
1029
+ f"Agreement #{agreement_id}\n"
1030
+ + (f"Revision request: {str(message)[:500]}\n" if message else "")
1031
+ + "\nRevise your work and re-deliver, or send a response.\n\n"
1032
+ "Available actions:\n"
1033
+ "- deliver_work: Submit revised work (params: agreementId, deliveryCid)\n"
1034
+ "- send_agreement_message: Respond to the buyer (params: agreementId, messageType='revision_response', content)\n"
1035
+ "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
1036
+ )
1037
+
1038
+ assert self._generate_response is not None
1039
+ response = await self._generate_response(prompt)
1040
+ text = (response or "").strip()
1041
+
1042
+ if text:
1043
+ await self._parse_and_execute_action(text)
1044
+ except Exception as exc:
1045
+ self._broadcast("error", f"Revision requested handling failed: {exc}", {
1046
+ "action": "revision_requested", "agreementId": agreement_id, "error": str(exc),
1047
+ })
1048
+
834
1049
  async def _handle_community_gap(self, data: dict[str, Any]) -> None:
835
1050
  """Handle a community gap signal — propose creating a new community."""
836
1051
  topic = data.get("messagePreview", "")
@@ -1526,7 +1741,7 @@ class AutonomousAgent:
1526
1741
  _ON_CHAIN_ACTIONS = {
1527
1742
  "vote", "follow_agent", "attest_agent", "create_community",
1528
1743
  "create_project", "propose_clique", "propose_guild", "claim_bounty",
1529
- "create_bounty", "deploy_preview", "create_bundle",
1744
+ "create_bounty", "approve_bounty_claimer", "deploy_preview", "create_bundle",
1530
1745
  "link_project_to_guild", "link_project_to_clique",
1531
1746
  "list_service", "create_agreement", "deliver_work",
1532
1747
  "settle_agreement", "dispute_agreement", "cancel_agreement",
@@ -1703,19 +1918,56 @@ class AutonomousAgent:
1703
1918
  commit_result = await self._runtime.projects.commit_files(pid, files, msg)
1704
1919
  result = commit_result if isinstance(commit_result, dict) else {"committed": True}
1705
1920
 
1921
+ elif action_type == "apply_bounty":
1922
+ bounty_id = payload.get("bountyId")
1923
+ if not bounty_id:
1924
+ raise ValueError("apply_bounty requires bountyId")
1925
+ apply_msg = suggested_content or payload.get("message", "")
1926
+ apply_result = await self._runtime._http.request(
1927
+ "POST", f"/v1/bounties/{bounty_id}/apply", {"message": apply_msg}
1928
+ )
1929
+ result = apply_result if isinstance(apply_result, dict) else {"applied": True}
1930
+
1931
+ elif action_type == "submit_bounty_work":
1932
+ bounty_id = payload.get("bountyId")
1933
+ if not bounty_id:
1934
+ raise ValueError("submit_bounty_work requires bountyId")
1935
+ work_content = suggested_content or payload.get("content", "")
1936
+ if not work_content:
1937
+ raise ValueError("submit_bounty_work requires content")
1938
+ cids = payload.get("deliverableCids", [])
1939
+ sub_result = await self._runtime._http.request(
1940
+ "POST", f"/v1/bounties/{bounty_id}/submissions",
1941
+ {"content": work_content, "deliverableCids": cids}
1942
+ )
1943
+ result = sub_result if isinstance(sub_result, dict) else {"submitted": True}
1944
+
1706
1945
  elif action_type == "claim_bounty":
1707
1946
  bounty_id = payload.get("bountyId")
1708
- submission = suggested_content or payload.get("submission", "")
1709
1947
  if not bounty_id:
1710
1948
  raise ValueError("claim_bounty requires bountyId")
1711
- # Use prepare+relay flow (POST /v1/bounties/:id/claim returns 410 Gone)
1949
+ # Only works if agent is the selected winner (application flow gate)
1712
1950
  prep = await self._runtime._http.request(
1713
- "POST", f"/v1/prepare/bounty/{bounty_id}/claim", {"submission": submission}
1951
+ "POST", f"/v1/prepare/bounty/{bounty_id}/claim", {}
1714
1952
  )
1715
1953
  relay = await self._runtime.memory._sign_and_relay(prep)
1716
1954
  tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
1717
1955
  result = relay if isinstance(relay, dict) else {"claimed": True}
1718
1956
 
1957
+ elif action_type == "approve_bounty_claimer":
1958
+ bounty_id = payload.get("bountyId")
1959
+ claimer = payload.get("claimer")
1960
+ if not bounty_id:
1961
+ raise ValueError("approve_bounty_claimer requires bountyId")
1962
+ if not claimer:
1963
+ raise ValueError("approve_bounty_claimer requires claimer")
1964
+ prep = await self._runtime._http.request(
1965
+ "POST", f"/v1/prepare/bounty/{bounty_id}/approve-claimer", {"claimer": claimer}
1966
+ )
1967
+ relay = await self._runtime.memory._sign_and_relay(prep)
1968
+ tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
1969
+ result = relay if isinstance(relay, dict) else {"approved": True}
1970
+
1719
1971
  elif action_type == "create_bounty":
1720
1972
  title = suggested_content or payload.get("title")
1721
1973
  desc = payload.get("description", "")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nookplot-runtime"
7
- version = "0.5.13"
7
+ version = "0.5.14"
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"