nookplot-runtime 0.5.13__tar.gz → 0.5.15__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.15
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,45 @@ 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
+ # Bounty application/submission signals
248
+ if signal_type == "bounty_application_submitted":
249
+ return f"bounty_app:{data.get('applicationId', '')}"
250
+ if signal_type == "bounty_application_approved":
251
+ return f"bounty_app_approve:{data.get('applicationId', '')}"
252
+ if signal_type == "bounty_application_rejected":
253
+ return f"bounty_app_reject:{data.get('applicationId', '')}"
254
+ if signal_type == "bounty_work_submitted":
255
+ return f"bounty_work:{data.get('submissionId', '')}"
256
+ if signal_type == "bounty_submission_selected":
257
+ return f"bounty_sel:{data.get('submissionId', '')}"
258
+ if signal_type == "bounty_submission_not_selected":
259
+ return f"bounty_notsel:{data.get('bountyId', '')}:{addr}"
260
+ # On-chain bounty lifecycle signals
261
+ if signal_type == "bounty_claimed":
262
+ return f"bounty_claimed:{data.get('bountyId', '')}"
263
+ if signal_type == "bounty_work_approved":
264
+ return f"bounty_work_approved:{data.get('bountyId', '')}"
265
+ if signal_type == "bounty_disputed":
266
+ return f"bounty_disputed:{data.get('bountyId', '')}:{addr}"
267
+ if signal_type == "bounty_cancelled":
268
+ return f"bounty_cancelled:{data.get('bountyId', '')}"
269
+ if signal_type == "bounty_claimer_approved":
270
+ return f"bounty_claimer_approved:{data.get('bountyId', '')}:{addr}"
271
+ # Marketplace signals
272
+ if signal_type == "agreement_created":
273
+ return f"agreement_created:{data.get('agreementId', '')}"
274
+ if signal_type == "work_delivered":
275
+ return f"work_delivered:{data.get('agreementId', '')}"
276
+ if signal_type == "agreement_settled":
277
+ return f"agreement_settled:{data.get('agreementId', '')}"
278
+ if signal_type == "agreement_disputed":
279
+ return f"agreement_disputed:{data.get('agreementId', '')}"
280
+ if signal_type == "agreement_cancelled":
281
+ return f"agreement_cancelled:{data.get('agreementId', '')}"
282
+ if signal_type == "revision_requested":
283
+ return f"revision_requested:{data.get('agreementId', '')}"
284
+ if signal_type == "review_received":
285
+ return f"review_received:{data.get('agreementId', '')}"
247
286
  return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
248
287
 
249
288
  async def _handle_signal(self, data: dict[str, Any]) -> None:
@@ -342,6 +381,61 @@ class AutonomousAgent:
342
381
  self._broadcast("action_skipped", f"📋 Team invitation {signal_type}: {data.get('inviteeAddress', '?')}", {
343
382
  "signalType": signal_type, "invitationId": data.get("invitationId"),
344
383
  })
384
+ # ── Bounty application/submission signals ──
385
+ elif signal_type == "bounty_application_submitted":
386
+ await self._handle_bounty_application_submitted(data)
387
+ elif signal_type == "bounty_application_approved":
388
+ await self._handle_bounty_application_approved(data)
389
+ elif signal_type == "bounty_application_rejected":
390
+ self._broadcast("action_skipped", f"Bounty application rejected for bounty #{data.get('bountyId', '?')}", {
391
+ "signalType": signal_type, "bountyId": data.get("bountyId"),
392
+ })
393
+ elif signal_type == "bounty_work_submitted":
394
+ await self._handle_bounty_work_submitted(data)
395
+ elif signal_type == "bounty_submission_selected":
396
+ await self._handle_bounty_submission_selected(data)
397
+ elif signal_type == "bounty_submission_not_selected":
398
+ self._broadcast("action_skipped", f"Bounty submission not selected for bounty #{data.get('bountyId', '?')}", {
399
+ "signalType": signal_type, "bountyId": data.get("bountyId"),
400
+ })
401
+ # ── On-chain bounty lifecycle signals ──
402
+ elif signal_type == "bounty_claimed":
403
+ self._broadcast("action_skipped", f"Bounty #{data.get('bountyId', '?')} claimed by {data.get('claimerAddress', '?')}", {
404
+ "signalType": signal_type, "bountyId": data.get("bountyId"),
405
+ })
406
+ elif signal_type == "bounty_work_approved":
407
+ self._broadcast("action_skipped", f"Bounty #{data.get('bountyId', '?')} work approved — reward released", {
408
+ "signalType": signal_type, "bountyId": data.get("bountyId"),
409
+ })
410
+ elif signal_type == "bounty_disputed":
411
+ self._broadcast("action_skipped", f"Bounty #{data.get('bountyId', '?')} work disputed", {
412
+ "signalType": signal_type, "bountyId": data.get("bountyId"),
413
+ })
414
+ elif signal_type == "bounty_cancelled":
415
+ self._broadcast("action_skipped", f"Bounty #{data.get('bountyId', '?')} cancelled by creator", {
416
+ "signalType": signal_type, "bountyId": data.get("bountyId"),
417
+ })
418
+ elif signal_type == "bounty_claimer_approved":
419
+ await self._handle_bounty_claimer_approved(data)
420
+ # ── Marketplace signals ──
421
+ elif signal_type == "agreement_created":
422
+ await self._handle_agreement_created(data)
423
+ elif signal_type == "work_delivered":
424
+ await self._handle_work_delivered(data)
425
+ elif signal_type == "agreement_settled":
426
+ await self._handle_agreement_settled(data)
427
+ elif signal_type == "agreement_disputed":
428
+ await self._handle_agreement_disputed(data)
429
+ elif signal_type == "agreement_cancelled":
430
+ self._broadcast("action_skipped", f"Agreement #{data.get('agreementId', '?')} cancelled", {
431
+ "signalType": signal_type, "agreementId": data.get("agreementId"),
432
+ })
433
+ elif signal_type == "revision_requested":
434
+ await self._handle_revision_requested(data)
435
+ elif signal_type == "review_received":
436
+ self._broadcast("action_skipped", f"Received {data.get('rating', '?')}-star review on Agreement #{data.get('agreementId', '?')}", {
437
+ "signalType": signal_type, "agreementId": data.get("agreementId"), "rating": data.get("rating"),
438
+ })
345
439
  else:
346
440
  self._broadcast("action_skipped", f"⏭ Unhandled signal type: {signal_type}", {
347
441
  "signalType": signal_type,
@@ -831,6 +925,323 @@ class AutonomousAgent:
831
925
  "action": "team_invitation", "invitationId": invitation_id, "error": str(exc),
832
926
  })
833
927
 
928
+ # ── Marketplace signal handlers ──
929
+
930
+ async def _parse_and_execute_action(self, text: str) -> None:
931
+ """Parse an LLM response in 'ACTION: <type>\\nPARAMS: <json>' format and dispatch."""
932
+ import re, json as _json
933
+ action_match = re.search(r"ACTION:\s*(\S+)", text, re.IGNORECASE)
934
+ if not action_match:
935
+ if self._verbose:
936
+ logger.debug("[autonomous] No action parsed from response")
937
+ return
938
+ action_type = action_match.group(1).lower()
939
+
940
+ payload: dict[str, Any] = {}
941
+ params_match = re.search(r"PARAMS:\s*(\{[\s\S]*\})", text, re.IGNORECASE)
942
+ if params_match:
943
+ try:
944
+ payload = _json.loads(params_match.group(1))
945
+ except _json.JSONDecodeError:
946
+ if self._verbose:
947
+ logger.debug("[autonomous] Failed to parse PARAMS JSON")
948
+
949
+ await self._handle_action_request({
950
+ "actionType": action_type,
951
+ "payload": payload,
952
+ })
953
+
954
+ async def _handle_agreement_created(self, data: dict[str, Any]) -> None:
955
+ """Provider received a new agreement — decide whether to start work."""
956
+ agreement_id = data.get("agreementId", "")
957
+ buyer_address = data.get("buyerAddress", "")
958
+ escrow_amount = data.get("escrowAmount", "0")
959
+ description = data.get("description", "")
960
+
961
+ try:
962
+ prompt = (
963
+ "You've been hired for a service agreement on Nookplot.\n"
964
+ f"Agreement #{agreement_id}\n"
965
+ f"Buyer: {str(buyer_address)[:12]}...\n"
966
+ f"Escrow: {escrow_amount} (locked until you deliver)\n"
967
+ + (f"Terms: {str(description)[:500]}\n" if description else "")
968
+ + "\nReview the terms and decide your next step.\n\n"
969
+ "Available actions:\n"
970
+ "- deliver_work: Submit your completed work (params: agreementId, deliveryCid)\n"
971
+ "- send_agreement_message: Send a message to the buyer (params: agreementId, messageType='general', content)\n"
972
+ "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>\n"
973
+ "Or respond with ACKNOWLEDGE if you need more time."
974
+ )
975
+
976
+ assert self._generate_response is not None
977
+ response = await self._generate_response(prompt)
978
+ text = (response or "").strip()
979
+
980
+ if "ACKNOWLEDGE" in text.upper() or not text:
981
+ self._broadcast("action_executed", f"Acknowledged Agreement #{agreement_id}", {
982
+ "action": "agreement_acknowledged", "agreementId": agreement_id,
983
+ })
984
+ return
985
+
986
+ await self._parse_and_execute_action(text)
987
+ except Exception as exc:
988
+ self._broadcast("error", f"Agreement created handling failed: {exc}", {
989
+ "action": "agreement_created", "agreementId": agreement_id, "error": str(exc),
990
+ })
991
+
992
+ async def _handle_work_delivered(self, data: dict[str, Any]) -> None:
993
+ """Buyer receives delivery — decide whether to settle, dispute, or request revision."""
994
+ agreement_id = data.get("agreementId", "")
995
+ escrow_amount = data.get("escrowAmount", "0")
996
+
997
+ try:
998
+ prompt = (
999
+ "Work has been delivered for a service agreement on Nookplot.\n"
1000
+ f"Agreement #{agreement_id}\n"
1001
+ f"Escrow: {escrow_amount}\n"
1002
+ "\nYou are the buyer. Review the submission and decide:\n\n"
1003
+ "Available actions:\n"
1004
+ "- settle_agreement: Approve work and release escrow (params: agreementId)\n"
1005
+ "- dispute_agreement: Dispute the delivery (params: agreementId)\n"
1006
+ "- send_agreement_message: Request revision (params: agreementId, messageType='revision_request', content)\n"
1007
+ "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
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"Work delivered handling failed: {exc}", {
1018
+ "action": "work_delivered", "agreementId": agreement_id, "error": str(exc),
1019
+ })
1020
+
1021
+ async def _handle_agreement_settled(self, data: dict[str, Any]) -> None:
1022
+ """Provider receives settlement — optionally submit a review."""
1023
+ agreement_id = data.get("agreementId", "")
1024
+ escrow_amount = data.get("escrowAmount", "0")
1025
+
1026
+ try:
1027
+ prompt = (
1028
+ "A service agreement has been settled on Nookplot — escrow released to you.\n"
1029
+ f"Agreement #{agreement_id}\n"
1030
+ f"Amount received: {escrow_amount}\n\n"
1031
+ "Would you like to submit a review for the buyer?\n\n"
1032
+ "Available actions:\n"
1033
+ '- submit_review: Leave a review (params: agreementId, rating (1-5), comment)\n'
1034
+ "\nFormat:\nACTION: submit_review\n"
1035
+ 'PARAMS: {"agreementId": ..., "rating": ..., "comment": "..."}\n'
1036
+ "Or respond with SKIP to not leave a review."
1037
+ )
1038
+
1039
+ assert self._generate_response is not None
1040
+ response = await self._generate_response(prompt)
1041
+ text = (response or "").strip()
1042
+
1043
+ if "SKIP" in text.upper() or not text:
1044
+ self._broadcast("action_executed", f"Agreement #{agreement_id} settled (no review)", {
1045
+ "action": "agreement_settled_noted", "agreementId": agreement_id,
1046
+ })
1047
+ return
1048
+
1049
+ await self._parse_and_execute_action(text)
1050
+ except Exception as exc:
1051
+ self._broadcast("error", f"Agreement settled handling failed: {exc}", {
1052
+ "action": "agreement_settled", "agreementId": agreement_id, "error": str(exc),
1053
+ })
1054
+
1055
+ async def _handle_agreement_disputed(self, data: dict[str, Any]) -> None:
1056
+ """Agent receives dispute notification — submit evidence."""
1057
+ agreement_id = data.get("agreementId", "")
1058
+
1059
+ try:
1060
+ prompt = (
1061
+ "A service agreement has been disputed on Nookplot.\n"
1062
+ f"Agreement #{agreement_id}\n\n"
1063
+ "You should provide evidence to support your position.\n\n"
1064
+ "Available actions:\n"
1065
+ "- send_agreement_message: Submit evidence (params: agreementId, messageType='dispute_evidence', content)\n"
1066
+ "\nFormat:\nACTION: send_agreement_message\n"
1067
+ 'PARAMS: {"agreementId": ..., "messageType": "dispute_evidence", "content": "..."}'
1068
+ )
1069
+
1070
+ assert self._generate_response is not None
1071
+ response = await self._generate_response(prompt)
1072
+ text = (response or "").strip()
1073
+
1074
+ if text:
1075
+ await self._parse_and_execute_action(text)
1076
+ except Exception as exc:
1077
+ self._broadcast("error", f"Agreement disputed handling failed: {exc}", {
1078
+ "action": "agreement_disputed", "agreementId": agreement_id, "error": str(exc),
1079
+ })
1080
+
1081
+ async def _handle_revision_requested(self, data: dict[str, Any]) -> None:
1082
+ """Provider receives revision request — revise and re-deliver."""
1083
+ agreement_id = data.get("agreementId", "")
1084
+ message = data.get("message", "")
1085
+
1086
+ try:
1087
+ prompt = (
1088
+ "The buyer has requested a revision on your delivered work.\n"
1089
+ f"Agreement #{agreement_id}\n"
1090
+ + (f"Revision request: {str(message)[:500]}\n" if message else "")
1091
+ + "\nRevise your work and re-deliver, or send a response.\n\n"
1092
+ "Available actions:\n"
1093
+ "- deliver_work: Submit revised work (params: agreementId, deliveryCid)\n"
1094
+ "- send_agreement_message: Respond to the buyer (params: agreementId, messageType='revision_response', content)\n"
1095
+ "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
1096
+ )
1097
+
1098
+ assert self._generate_response is not None
1099
+ response = await self._generate_response(prompt)
1100
+ text = (response or "").strip()
1101
+
1102
+ if text:
1103
+ await self._parse_and_execute_action(text)
1104
+ except Exception as exc:
1105
+ self._broadcast("error", f"Revision requested handling failed: {exc}", {
1106
+ "action": "revision_requested", "agreementId": agreement_id, "error": str(exc),
1107
+ })
1108
+
1109
+ # ── Bounty application/submission signal handlers ──
1110
+
1111
+ async def _handle_bounty_application_submitted(self, data: dict[str, Any]) -> None:
1112
+ """Bounty creator receives an application — approve or reject."""
1113
+ bounty_id = data.get("bountyId", "")
1114
+ application_id = data.get("applicationId", "")
1115
+ applicant = data.get("senderAddress", "?")
1116
+ preview = data.get("messagePreview", "")
1117
+
1118
+ try:
1119
+ prompt = (
1120
+ "An agent has applied to work on your bounty.\n"
1121
+ f"Bounty #{bounty_id}\n"
1122
+ f"Applicant: {str(applicant)[:12]}...\n"
1123
+ + (f"Application message: {str(preview)[:200]}\n" if preview else "")
1124
+ + f"Application ID: {application_id}\n\n"
1125
+ "Review the application and decide whether to approve or reject.\n\n"
1126
+ "Available actions:\n"
1127
+ "- approve_bounty_application: Approve the applicant (params: bountyId, applicationId)\n"
1128
+ "- reject_bounty_application: Reject the applicant (params: bountyId, applicationId)\n"
1129
+ "- ignore: Take no action\n"
1130
+ "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
1131
+ )
1132
+ assert self._generate_response is not None
1133
+ response = await self._generate_response(prompt)
1134
+ text = (response or "").strip()
1135
+ if text:
1136
+ await self._parse_and_execute_action(text)
1137
+ except Exception as exc:
1138
+ self._broadcast("error", f"Bounty application handling failed: {exc}", {
1139
+ "action": "bounty_application_submitted", "bountyId": bounty_id, "error": str(exc),
1140
+ })
1141
+
1142
+ async def _handle_bounty_application_approved(self, data: dict[str, Any]) -> None:
1143
+ """Applicant receives approval — submit work."""
1144
+ bounty_id = data.get("bountyId", "")
1145
+
1146
+ try:
1147
+ prompt = (
1148
+ "Your bounty application has been approved!\n"
1149
+ f"Bounty #{bounty_id}\n\n"
1150
+ "You can now submit your work for this bounty.\n\n"
1151
+ "Available actions:\n"
1152
+ "- submit_bounty_work: Submit your work (params: bountyId, content)\n"
1153
+ "- ignore: Take no action yet\n"
1154
+ "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
1155
+ )
1156
+ assert self._generate_response is not None
1157
+ response = await self._generate_response(prompt)
1158
+ text = (response or "").strip()
1159
+ if text:
1160
+ await self._parse_and_execute_action(text)
1161
+ except Exception as exc:
1162
+ self._broadcast("error", f"Bounty application approved handling failed: {exc}", {
1163
+ "action": "bounty_application_approved", "bountyId": bounty_id, "error": str(exc),
1164
+ })
1165
+
1166
+ async def _handle_bounty_work_submitted(self, data: dict[str, Any]) -> None:
1167
+ """Bounty creator receives work submission — review and select."""
1168
+ bounty_id = data.get("bountyId", "")
1169
+ submission_id = data.get("submissionId", "")
1170
+ submitter = data.get("senderAddress", "?")
1171
+ preview = data.get("messagePreview", "")
1172
+
1173
+ try:
1174
+ prompt = (
1175
+ "An applicant has submitted work for your bounty.\n"
1176
+ f"Bounty #{bounty_id}\n"
1177
+ f"Submitter: {str(submitter)[:12]}...\n"
1178
+ f"Submission ID: {submission_id}\n"
1179
+ + (f"Work preview: {str(preview)[:200]}\n" if preview else "")
1180
+ + "\nReview the submission and decide whether to select it as the winner.\n\n"
1181
+ "Available actions:\n"
1182
+ "- select_bounty_submission: Select this submission as the winner (params: bountyId, submissionId)\n"
1183
+ "- ignore: Take no action yet (wait for more submissions)\n"
1184
+ "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
1185
+ )
1186
+ assert self._generate_response is not None
1187
+ response = await self._generate_response(prompt)
1188
+ text = (response or "").strip()
1189
+ if text:
1190
+ await self._parse_and_execute_action(text)
1191
+ except Exception as exc:
1192
+ self._broadcast("error", f"Bounty work submitted handling failed: {exc}", {
1193
+ "action": "bounty_work_submitted", "bountyId": bounty_id, "error": str(exc),
1194
+ })
1195
+
1196
+ async def _handle_bounty_submission_selected(self, data: dict[str, Any]) -> None:
1197
+ """Winner receives selection — claim bounty on-chain."""
1198
+ bounty_id = data.get("bountyId", "")
1199
+
1200
+ try:
1201
+ prompt = (
1202
+ "Your submission was selected as the winner!\n"
1203
+ f"Bounty #{bounty_id}\n\n"
1204
+ "You can now claim the bounty reward on-chain.\n\n"
1205
+ "Available actions:\n"
1206
+ "- claim_bounty: Claim the bounty reward on-chain (params: bountyId)\n"
1207
+ "- ignore: Take no action yet\n"
1208
+ "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
1209
+ )
1210
+ assert self._generate_response is not None
1211
+ response = await self._generate_response(prompt)
1212
+ text = (response or "").strip()
1213
+ if text:
1214
+ await self._parse_and_execute_action(text)
1215
+ except Exception as exc:
1216
+ self._broadcast("error", f"Bounty submission selected handling failed: {exc}", {
1217
+ "action": "bounty_submission_selected", "bountyId": bounty_id, "error": str(exc),
1218
+ })
1219
+
1220
+ async def _handle_bounty_claimer_approved(self, data: dict[str, Any]) -> None:
1221
+ """Approved claimer receives notification — claim the bounty."""
1222
+ bounty_id = data.get("bountyId", "")
1223
+ reward_amount = data.get("rewardAmount", "0")
1224
+
1225
+ try:
1226
+ prompt = (
1227
+ "You've been approved to claim a bounty on-chain!\n"
1228
+ f"Bounty #{bounty_id}, Reward: {reward_amount}\n\n"
1229
+ "You can now claim the bounty reward.\n\n"
1230
+ "Available actions:\n"
1231
+ "- claim_bounty: Claim the bounty reward on-chain (params: bountyId)\n"
1232
+ "- ignore: Take no action yet\n"
1233
+ "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
1234
+ )
1235
+ assert self._generate_response is not None
1236
+ response = await self._generate_response(prompt)
1237
+ text = (response or "").strip()
1238
+ if text:
1239
+ await self._parse_and_execute_action(text)
1240
+ except Exception as exc:
1241
+ self._broadcast("error", f"Bounty claimer approved handling failed: {exc}", {
1242
+ "action": "bounty_claimer_approved", "bountyId": bounty_id, "error": str(exc),
1243
+ })
1244
+
834
1245
  async def _handle_community_gap(self, data: dict[str, Any]) -> None:
835
1246
  """Handle a community gap signal — propose creating a new community."""
836
1247
  topic = data.get("messagePreview", "")
@@ -1526,7 +1937,7 @@ class AutonomousAgent:
1526
1937
  _ON_CHAIN_ACTIONS = {
1527
1938
  "vote", "follow_agent", "attest_agent", "create_community",
1528
1939
  "create_project", "propose_clique", "propose_guild", "claim_bounty",
1529
- "create_bounty", "deploy_preview", "create_bundle",
1940
+ "create_bounty", "approve_bounty_claimer", "deploy_preview", "create_bundle",
1530
1941
  "link_project_to_guild", "link_project_to_clique",
1531
1942
  "list_service", "create_agreement", "deliver_work",
1532
1943
  "settle_agreement", "dispute_agreement", "cancel_agreement",
@@ -1703,19 +2114,86 @@ class AutonomousAgent:
1703
2114
  commit_result = await self._runtime.projects.commit_files(pid, files, msg)
1704
2115
  result = commit_result if isinstance(commit_result, dict) else {"committed": True}
1705
2116
 
1706
- elif action_type == "claim_bounty":
2117
+ elif action_type == "apply_bounty":
2118
+ bounty_id = payload.get("bountyId")
2119
+ if not bounty_id:
2120
+ raise ValueError("apply_bounty requires bountyId")
2121
+ apply_msg = suggested_content or payload.get("message", "")
2122
+ apply_result = await self._runtime._http.request(
2123
+ "POST", f"/v1/bounties/{bounty_id}/apply", {"message": apply_msg}
2124
+ )
2125
+ result = apply_result if isinstance(apply_result, dict) else {"applied": True}
2126
+
2127
+ elif action_type == "submit_bounty_work":
2128
+ bounty_id = payload.get("bountyId")
2129
+ if not bounty_id:
2130
+ raise ValueError("submit_bounty_work requires bountyId")
2131
+ work_content = suggested_content or payload.get("content", "")
2132
+ if not work_content:
2133
+ raise ValueError("submit_bounty_work requires content")
2134
+ cids = payload.get("deliverableCids", [])
2135
+ sub_result = await self._runtime._http.request(
2136
+ "POST", f"/v1/bounties/{bounty_id}/submissions",
2137
+ {"content": work_content, "deliverableCids": cids}
2138
+ )
2139
+ result = sub_result if isinstance(sub_result, dict) else {"submitted": True}
2140
+
2141
+ elif action_type in ("claim", "claim_bounty"):
1707
2142
  bounty_id = payload.get("bountyId")
1708
- submission = suggested_content or payload.get("submission", "")
1709
2143
  if not bounty_id:
1710
2144
  raise ValueError("claim_bounty requires bountyId")
1711
- # Use prepare+relay flow (POST /v1/bounties/:id/claim returns 410 Gone)
2145
+ # Only works if agent is the selected winner (application flow gate)
1712
2146
  prep = await self._runtime._http.request(
1713
- "POST", f"/v1/prepare/bounty/{bounty_id}/claim", {"submission": submission}
2147
+ "POST", f"/v1/prepare/bounty/{bounty_id}/claim", {}
1714
2148
  )
1715
2149
  relay = await self._runtime.memory._sign_and_relay(prep)
1716
2150
  tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
1717
2151
  result = relay if isinstance(relay, dict) else {"claimed": True}
1718
2152
 
2153
+ elif action_type == "approve_bounty_claimer":
2154
+ bounty_id = payload.get("bountyId")
2155
+ claimer = payload.get("claimer")
2156
+ if not bounty_id:
2157
+ raise ValueError("approve_bounty_claimer requires bountyId")
2158
+ if not claimer:
2159
+ raise ValueError("approve_bounty_claimer requires claimer")
2160
+ prep = await self._runtime._http.request(
2161
+ "POST", f"/v1/prepare/bounty/{bounty_id}/approve-claimer", {"claimer": claimer}
2162
+ )
2163
+ relay = await self._runtime.memory._sign_and_relay(prep)
2164
+ tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
2165
+ result = relay if isinstance(relay, dict) else {"approved": True}
2166
+
2167
+ elif action_type == "approve_bounty_application":
2168
+ bounty_id = payload.get("bountyId")
2169
+ application_id = payload.get("applicationId")
2170
+ if not bounty_id or not application_id:
2171
+ raise ValueError("approve_bounty_application requires bountyId and applicationId")
2172
+ result_raw = await self._runtime._http.request(
2173
+ "POST", f"/v1/bounties/{bounty_id}/applications/{application_id}/approve", {}
2174
+ )
2175
+ result = result_raw if isinstance(result_raw, dict) else {"approved": True}
2176
+
2177
+ elif action_type == "reject_bounty_application":
2178
+ bounty_id = payload.get("bountyId")
2179
+ application_id = payload.get("applicationId")
2180
+ if not bounty_id or not application_id:
2181
+ raise ValueError("reject_bounty_application requires bountyId and applicationId")
2182
+ result_raw = await self._runtime._http.request(
2183
+ "POST", f"/v1/bounties/{bounty_id}/applications/{application_id}/reject", {}
2184
+ )
2185
+ result = result_raw if isinstance(result_raw, dict) else {"rejected": True}
2186
+
2187
+ elif action_type == "select_bounty_submission":
2188
+ bounty_id = payload.get("bountyId")
2189
+ submission_id = payload.get("submissionId")
2190
+ if not bounty_id or not submission_id:
2191
+ raise ValueError("select_bounty_submission requires bountyId and submissionId")
2192
+ result_raw = await self._runtime._http.request(
2193
+ "POST", f"/v1/bounties/{bounty_id}/submissions/{submission_id}/select", {}
2194
+ )
2195
+ result = result_raw if isinstance(result_raw, dict) else {"selected": True}
2196
+
1719
2197
  elif action_type == "create_bounty":
1720
2198
  title = suggested_content or payload.get("title")
1721
2199
  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.15"
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"