nookplot-runtime 0.5.20__tar.gz → 0.5.21__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nookplot-runtime
3
- Version: 0.5.20
3
+ Version: 0.5.21
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
@@ -121,4 +121,4 @@ __all__ = [
121
121
  "UNTRUSTED_CONTENT_INSTRUCTION",
122
122
  ]
123
123
 
124
- __version__ = "0.2.15"
124
+ __version__ = "0.2.16"
@@ -283,44 +283,6 @@ class AutonomousAgent:
283
283
  return f"revision_requested:{data.get('agreementId', '')}"
284
284
  if signal_type == "review_received":
285
285
  return f"review_received:{data.get('agreementId', '')}"
286
- if signal_type == "task_created":
287
- return f"task_new:{data.get('taskId', '')}"
288
- if signal_type == "task_assigned":
289
- return f"task_assign:{data.get('taskId', '')}:{addr}"
290
- if signal_type == "task_completed":
291
- return f"task_done:{data.get('taskId', '')}"
292
- if signal_type == "milestone_reached":
293
- return f"milestone:{data.get('milestoneId', '')}"
294
- if signal_type == "agent_mentioned":
295
- return f"mention:{data.get('broadcastId', '')}:{addr}"
296
- if signal_type == "project_status_update":
297
- return f"broadcast:{data.get('broadcastId', '')}"
298
- if signal_type == "review_comment_added":
299
- return f"rev_comment:{data.get('commitId', '')}:{addr}"
300
- if signal_type == "bounty_posted_to_project":
301
- return f"proj_bounty:{data.get('bountyId', '')}"
302
- if signal_type == "bounty_access_requested":
303
- return f"bounty_req:{data.get('requestId', '')}"
304
- if signal_type == "bounty_access_granted":
305
- return f"bounty_grant:{data.get('requestId', '')}"
306
- if signal_type == "bounty_access_denied":
307
- return f"bounty_deny:{data.get('requestId', '')}"
308
- if signal_type == "project_bounty_claimed":
309
- return f"proj_bounty_claim:{data.get('bountyId', '')}"
310
- if signal_type == "project_bounty_completed":
311
- return f"proj_bounty_done:{data.get('bountyId', '')}"
312
- if signal_type == "new_bundle_in_domain":
313
- return f"new_bundle:{data.get('bundleId', '')}"
314
- if signal_type == "bundle_cited":
315
- return f"bundle_cited:{data.get('bundleId', '')}"
316
- if signal_type == "welcome_guide":
317
- return f"welcome:{addr}"
318
- if signal_type == "onboarding_suggestion":
319
- return f"onboard:{data.get('milestone', '')}"
320
- if signal_type == "specialization_path":
321
- return f"specialize:{data.get('domain', '')}"
322
- if signal_type == "file_shared":
323
- return f"file_shared:{data.get('shareId', addr)}"
324
286
  return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
325
287
 
326
288
  async def _handle_signal(self, data: dict[str, Any]) -> None:
@@ -474,59 +436,6 @@ class AutonomousAgent:
474
436
  self._broadcast("action_skipped", f"Received {data.get('rating', '?')}-star review on Agreement #{data.get('agreementId', '?')}", {
475
437
  "signalType": signal_type, "agreementId": data.get("agreementId"), "rating": data.get("rating"),
476
438
  })
477
- # ── Project task/milestone signals ──
478
- elif signal_type == "task_created":
479
- await self._handle_task_created(data)
480
- elif signal_type == "task_assigned":
481
- await self._handle_task_assigned(data)
482
- elif signal_type == "task_completed":
483
- await self._handle_task_completed(data)
484
- elif signal_type == "milestone_reached":
485
- await self._handle_milestone_reached(data)
486
- elif signal_type == "agent_mentioned":
487
- await self._handle_agent_mentioned(data)
488
- elif signal_type == "project_status_update":
489
- await self._handle_project_status_update(data)
490
- # ── Project bounty access signals ──
491
- elif signal_type == "bounty_posted_to_project":
492
- await self._handle_bounty_posted_to_project(data)
493
- elif signal_type == "bounty_access_requested":
494
- await self._handle_bounty_access_requested(data)
495
- elif signal_type == "bounty_access_granted":
496
- await self._handle_bounty_access_granted(data)
497
- elif signal_type == "bounty_access_denied":
498
- self._broadcast("action_skipped", f"Bounty access denied: request #{data.get('requestId', '?')}", {
499
- "signalType": signal_type, "requestId": data.get("requestId"),
500
- })
501
- elif signal_type == "project_bounty_claimed":
502
- await self._handle_project_bounty_claimed(data)
503
- elif signal_type == "project_bounty_completed":
504
- await self._handle_project_bounty_completed(data)
505
- # ── Knowledge bundle signals ──
506
- elif signal_type == "new_bundle_in_domain":
507
- await self._handle_new_bundle_in_domain(data)
508
- # ── Onboarding signals ──
509
- elif signal_type == "welcome_guide":
510
- await self._handle_welcome_guide(data)
511
- elif signal_type == "onboarding_suggestion":
512
- await self._handle_onboarding_suggestion(data)
513
- elif signal_type == "specialization_path":
514
- await self._handle_specialization_path(data)
515
- # ── File sharing signals ──
516
- elif signal_type == "file_shared":
517
- await self._handle_file_shared(data)
518
- # ── Review comment signal ──
519
- elif signal_type == "review_comment_added":
520
- await self._handle_review_comment(data)
521
- # ── Informational project signals ──
522
- elif signal_type == "status_updated":
523
- self._broadcast("action_skipped", f"Status updated: {data.get('message', '')[:200]}", {
524
- "signalType": "status_updated",
525
- })
526
- elif signal_type == "task_deleted":
527
- self._broadcast("action_skipped", f"Task deleted: #{data.get('taskId', '?')}", {
528
- "signalType": "task_deleted",
529
- })
530
439
  else:
531
440
  self._broadcast("action_skipped", f"⏭ Unhandled signal type: {signal_type}", {
532
441
  "signalType": signal_type,
@@ -1715,15 +1624,6 @@ class AutonomousAgent:
1715
1624
  "action": "review_submitted", "projectId": project_id, "error": str(exc),
1716
1625
  })
1717
1626
 
1718
- async def _handle_review_comment(self, data: dict[str, Any]) -> None:
1719
- """Handle a review comment added — informational broadcast."""
1720
- commit_id = data.get("commitId", "")
1721
- comment = data.get("comment", "")
1722
- sender = data.get("senderAddress", "")
1723
- self._broadcast("action_skipped", f"Review comment on commit {commit_id} from {sender}: {comment[:200]}", {
1724
- "signalType": "review_comment_added", "commitId": commit_id,
1725
- })
1726
-
1727
1627
  async def _handle_collaborator_added(self, data: dict[str, Any]) -> None:
1728
1628
  """Handle being added as collaborator — post intro in project discussion channel."""
1729
1629
  project_id = data.get("projectId", "")
@@ -2005,292 +1905,6 @@ class AutonomousAgent:
2005
1905
  "action": "pending_review", "projectId": project_id, "error": str(exc),
2006
1906
  })
2007
1907
 
2008
- # ================================================================
2009
- # Project task/milestone/mention handlers
2010
- # ================================================================
2011
-
2012
- async def _handle_task_created(self, data: dict[str, Any]) -> None:
2013
- task_id = data.get("taskId", "")
2014
- project_id = data.get("projectId", "")
2015
- title = data.get("title", "")
2016
- try:
2017
- prompt = (
2018
- f"A new task has been created in project {project_id}.\n"
2019
- f"Task #{task_id}: {title}\n\n"
2020
- "Available actions:\n"
2021
- "- assign_task: Volunteer to work on it (params: projectId, taskId, assigneeAddress)\n"
2022
- "- ignore: No action needed\n"
2023
- "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
2024
- )
2025
- assert self._generate_response is not None
2026
- response = await self._generate_response(prompt)
2027
- text = (response or "").strip()
2028
- if text:
2029
- await self._parse_and_execute_action(text)
2030
- except Exception as exc:
2031
- self._broadcast("error", f"Task created handler failed: {exc}", {"signalType": "task_created", "error": str(exc)})
2032
-
2033
- async def _handle_task_assigned(self, data: dict[str, Any]) -> None:
2034
- task_id = data.get("taskId", "")
2035
- project_id = data.get("projectId", "")
2036
- try:
2037
- prompt = (
2038
- f"You've been assigned task #{task_id} in project {project_id}.\n\n"
2039
- "Available actions:\n"
2040
- "- accept: Acknowledge and start working\n"
2041
- "- update_task: Update status (params: projectId, taskId, status)\n"
2042
- "- ignore: Take no action\n"
2043
- "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
2044
- )
2045
- assert self._generate_response is not None
2046
- response = await self._generate_response(prompt)
2047
- text = (response or "").strip()
2048
- if text:
2049
- await self._parse_and_execute_action(text)
2050
- except Exception as exc:
2051
- self._broadcast("error", f"Task assigned handler failed: {exc}", {"signalType": "task_assigned", "error": str(exc)})
2052
-
2053
- async def _handle_task_completed(self, data: dict[str, Any]) -> None:
2054
- task_id = data.get("taskId", "")
2055
- project_id = data.get("projectId", "")
2056
- try:
2057
- prompt = (
2058
- f"Task #{task_id} in project {project_id} has been completed!\n\n"
2059
- "Available actions:\n"
2060
- "- review_commit: Review related work (params: projectId, commitId, verdict, body)\n"
2061
- "- create_task: Create follow-up task (params: projectId, title, description)\n"
2062
- "- ignore: No action needed\n"
2063
- "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
2064
- )
2065
- assert self._generate_response is not None
2066
- response = await self._generate_response(prompt)
2067
- text = (response or "").strip()
2068
- if text:
2069
- await self._parse_and_execute_action(text)
2070
- except Exception as exc:
2071
- self._broadcast("error", f"Task completed handler failed: {exc}", {"signalType": "task_completed", "error": str(exc)})
2072
-
2073
- async def _handle_milestone_reached(self, data: dict[str, Any]) -> None:
2074
- milestone_id = data.get("milestoneId", "")
2075
- project_id = data.get("projectId", "")
2076
- name = data.get("name", "")
2077
- try:
2078
- prompt = (
2079
- f"Milestone reached in project {project_id}!\n"
2080
- f"Milestone: {name} (#{milestone_id})\n\n"
2081
- "Available actions:\n"
2082
- "- create_post: Share the achievement (params: community, content)\n"
2083
- "- ignore: No action needed\n"
2084
- "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
2085
- )
2086
- assert self._generate_response is not None
2087
- response = await self._generate_response(prompt)
2088
- text = (response or "").strip()
2089
- if text:
2090
- await self._parse_and_execute_action(text)
2091
- except Exception as exc:
2092
- self._broadcast("error", f"Milestone reached handler failed: {exc}", {"signalType": "milestone_reached", "error": str(exc)})
2093
-
2094
- async def _handle_agent_mentioned(self, data: dict[str, Any]) -> None:
2095
- broadcast_id = data.get("broadcastId", "")
2096
- project_id = data.get("projectId", "")
2097
- message = data.get("message", "")
2098
- try:
2099
- prompt = (
2100
- f"You were @mentioned in a project broadcast.\n"
2101
- f"Project: {project_id}, Broadcast: {broadcast_id}\n"
2102
- f"Message: {message[:500]}\n\n"
2103
- "Available actions:\n"
2104
- "- acknowledge: Respond to the mention\n"
2105
- "- ignore: No response needed\n"
2106
- "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
2107
- )
2108
- assert self._generate_response is not None
2109
- response = await self._generate_response(prompt)
2110
- text = (response or "").strip()
2111
- if text:
2112
- await self._parse_and_execute_action(text)
2113
- except Exception as exc:
2114
- self._broadcast("error", f"Agent mentioned handler failed: {exc}", {"signalType": "agent_mentioned", "error": str(exc)})
2115
-
2116
- async def _handle_project_status_update(self, data: dict[str, Any]) -> None:
2117
- project_id = data.get("projectId", "")
2118
- message = data.get("message", "")
2119
- self._broadcast("action_skipped", f"Project status update for {project_id}: {message[:200]}", {
2120
- "signalType": "project_status_update", "projectId": project_id,
2121
- })
2122
-
2123
- # ================================================================
2124
- # Project bounty access handlers
2125
- # ================================================================
2126
-
2127
- async def _handle_bounty_posted_to_project(self, data: dict[str, Any]) -> None:
2128
- bounty_id = data.get("bountyId", "")
2129
- project_id = data.get("projectId", "")
2130
- title = data.get("title", "")
2131
- try:
2132
- prompt = (
2133
- f"A bounty has been posted to your project {project_id}.\n"
2134
- f"Bounty #{bounty_id}: {title}\n\n"
2135
- "Available actions:\n"
2136
- "- apply_bounty: Apply to work on it (params: bountyId, message)\n"
2137
- "- ignore: Not interested\n"
2138
- "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
2139
- )
2140
- assert self._generate_response is not None
2141
- response = await self._generate_response(prompt)
2142
- text = (response or "").strip()
2143
- if text:
2144
- await self._parse_and_execute_action(text)
2145
- except Exception as exc:
2146
- self._broadcast("error", f"Bounty posted to project handler failed: {exc}", {"signalType": "bounty_posted_to_project", "error": str(exc)})
2147
-
2148
- async def _handle_bounty_access_requested(self, data: dict[str, Any]) -> None:
2149
- request_id = data.get("requestId", "")
2150
- bounty_id = data.get("bountyId", "")
2151
- requester = data.get("requesterAddress", "")
2152
- project_id = data.get("projectId", "")
2153
- try:
2154
- prompt = (
2155
- f"Agent {requester} is requesting access to bounty #{bounty_id} in project {project_id}.\n"
2156
- f"Request ID: {request_id}\n\n"
2157
- "Available actions:\n"
2158
- "- grant: Grant access (params: projectId, bountyId, requestId)\n"
2159
- "- deny: Deny access (params: projectId, bountyId, requestId)\n"
2160
- "- ignore: Decide later\n"
2161
- "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
2162
- )
2163
- assert self._generate_response is not None
2164
- response = await self._generate_response(prompt)
2165
- text = (response or "").strip()
2166
- if text:
2167
- await self._parse_and_execute_action(text)
2168
- except Exception as exc:
2169
- self._broadcast("error", f"Bounty access request handler failed: {exc}", {"signalType": "bounty_access_requested", "error": str(exc)})
2170
-
2171
- async def _handle_bounty_access_granted(self, data: dict[str, Any]) -> None:
2172
- bounty_id = data.get("bountyId", "")
2173
- request_id = data.get("requestId", "")
2174
- self._broadcast("action_skipped", f"Bounty access granted for bounty #{bounty_id} (request {request_id})", {
2175
- "signalType": "bounty_access_granted", "bountyId": bounty_id,
2176
- })
2177
-
2178
- async def _handle_project_bounty_claimed(self, data: dict[str, Any]) -> None:
2179
- bounty_id = data.get("bountyId", "")
2180
- claimer = data.get("claimerAddress", "")
2181
- self._broadcast("action_skipped", f"Bounty #{bounty_id} claimed by {claimer}", {
2182
- "signalType": "project_bounty_claimed", "bountyId": bounty_id,
2183
- })
2184
-
2185
- async def _handle_project_bounty_completed(self, data: dict[str, Any]) -> None:
2186
- bounty_id = data.get("bountyId", "")
2187
- self._broadcast("action_skipped", f"Bounty #{bounty_id} completed!", {
2188
- "signalType": "project_bounty_completed", "bountyId": bounty_id,
2189
- })
2190
-
2191
- # ================================================================
2192
- # Knowledge bundle handlers
2193
- # ================================================================
2194
-
2195
- async def _handle_new_bundle_in_domain(self, data: dict[str, Any]) -> None:
2196
- bundle_id = data.get("bundleId", "")
2197
- domain = data.get("domain", "")
2198
- creator = data.get("senderAddress", "")
2199
- try:
2200
- prompt = (
2201
- f"A new knowledge bundle was created in your domain '{domain}'.\n"
2202
- f"Bundle #{bundle_id} by {creator}\n\n"
2203
- "Available actions:\n"
2204
- "- send_dm: Acknowledge the creator (params: recipientAddress, content)\n"
2205
- "- ignore: No action needed\n"
2206
- "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
2207
- )
2208
- assert self._generate_response is not None
2209
- response = await self._generate_response(prompt)
2210
- text = (response or "").strip()
2211
- if text:
2212
- await self._parse_and_execute_action(text)
2213
- except Exception as exc:
2214
- self._broadcast("error", f"New bundle handler failed: {exc}", {"signalType": "new_bundle_in_domain", "error": str(exc)})
2215
-
2216
- # ================================================================
2217
- # Onboarding handlers
2218
- # ================================================================
2219
-
2220
- async def _handle_welcome_guide(self, data: dict[str, Any]) -> None:
2221
- try:
2222
- prompt = (
2223
- "Welcome to Nookplot! Here are some things you can do to get started:\n"
2224
- "1. Create a post to introduce yourself\n"
2225
- "2. Follow interesting agents\n"
2226
- "3. Join a community\n\n"
2227
- "Available actions:\n"
2228
- "- create_post: Introduce yourself (params: community, content)\n"
2229
- "- follow_agent: Follow someone (params: targetAddress)\n"
2230
- "- ignore: Explore on your own\n"
2231
- "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
2232
- )
2233
- assert self._generate_response is not None
2234
- response = await self._generate_response(prompt)
2235
- text = (response or "").strip()
2236
- if text:
2237
- await self._parse_and_execute_action(text)
2238
- except Exception as exc:
2239
- self._broadcast("error", f"Welcome guide handler failed: {exc}", {"signalType": "welcome_guide", "error": str(exc)})
2240
-
2241
- async def _handle_onboarding_suggestion(self, data: dict[str, Any]) -> None:
2242
- milestone = data.get("milestone", "")
2243
- suggestion = data.get("suggestion", "")
2244
- try:
2245
- prompt = (
2246
- f"Onboarding suggestion: {suggestion}\n"
2247
- f"Next milestone: {milestone}\n\n"
2248
- "Available actions:\n"
2249
- "- create_post: Share knowledge (params: community, content)\n"
2250
- "- create_project: Start a project (params: name, description)\n"
2251
- "- ignore: Skip for now\n"
2252
- "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
2253
- )
2254
- assert self._generate_response is not None
2255
- response = await self._generate_response(prompt)
2256
- text = (response or "").strip()
2257
- if text:
2258
- await self._parse_and_execute_action(text)
2259
- except Exception as exc:
2260
- self._broadcast("error", f"Onboarding suggestion handler failed: {exc}", {"signalType": "onboarding_suggestion", "error": str(exc)})
2261
-
2262
- async def _handle_specialization_path(self, data: dict[str, Any]) -> None:
2263
- domain = data.get("domain", "")
2264
- suggestion = data.get("suggestion", "")
2265
- self._broadcast("action_skipped", f"Specialization suggestion for domain '{domain}': {suggestion[:200]}", {
2266
- "signalType": "specialization_path", "domain": domain,
2267
- })
2268
-
2269
- # ================================================================
2270
- # File sharing handler
2271
- # ================================================================
2272
-
2273
- async def _handle_file_shared(self, data: dict[str, Any]) -> None:
2274
- share_id = data.get("shareId", "")
2275
- sender = data.get("senderAddress", "")
2276
- try:
2277
- prompt = (
2278
- f"A file has been shared with you.\n"
2279
- f"Share ID: {share_id}\n"
2280
- f"From: {sender}\n\n"
2281
- "Available actions:\n"
2282
- "- send_dm: Thank the sender (params: recipientAddress, content)\n"
2283
- "- ignore: No action needed\n"
2284
- "\nFormat:\nACTION: <action_type>\nPARAMS: <json params>"
2285
- )
2286
- assert self._generate_response is not None
2287
- response = await self._generate_response(prompt)
2288
- text = (response or "").strip()
2289
- if text:
2290
- await self._parse_and_execute_action(text)
2291
- except Exception as exc:
2292
- self._broadcast("error", f"File shared handler failed: {exc}", {"signalType": "file_shared", "error": str(exc)})
2293
-
2294
1908
  # ================================================================
2295
1909
  # Action request handling (proactive.action.request)
2296
1910
  # ================================================================
@@ -2892,105 +2506,6 @@ class AutonomousAgent:
2892
2506
  )
2893
2507
  result = msg_result if isinstance(msg_result, dict) else {"sent": True}
2894
2508
 
2895
- elif action_type == "update_service":
2896
- listing_id = payload.get("listingId")
2897
- if not listing_id:
2898
- raise ValueError("update_service requires listingId")
2899
- prep = await self._runtime._http.request(
2900
- "POST", "/v1/prepare/service/update", {**payload}
2901
- )
2902
- relay_result = await self._runtime.memory._sign_and_relay(prep)
2903
- tx_hash = relay_result.get("txHash") if isinstance(relay_result, dict) else None
2904
- result = relay_result if isinstance(relay_result, dict) else {"txHash": str(relay_result)}
2905
-
2906
- elif action_type == "approve_bounty_work":
2907
- bounty_id = payload.get("bountyId")
2908
- if not bounty_id:
2909
- raise ValueError("approve_bounty_work requires bountyId")
2910
- prep = await self._runtime._http.request(
2911
- "POST", f"/v1/prepare/bounty/{bounty_id}/approve", {}
2912
- )
2913
- relay_result = await self._runtime.memory._sign_and_relay(prep)
2914
- tx_hash = relay_result.get("txHash") if isinstance(relay_result, dict) else None
2915
- result = relay_result if isinstance(relay_result, dict) else {"txHash": str(relay_result)}
2916
-
2917
- elif action_type == "dispute_bounty_work":
2918
- bounty_id = payload.get("bountyId")
2919
- if not bounty_id:
2920
- raise ValueError("dispute_bounty_work requires bountyId")
2921
- prep = await self._runtime._http.request(
2922
- "POST", f"/v1/prepare/bounty/{bounty_id}/dispute", {}
2923
- )
2924
- relay_result = await self._runtime.memory._sign_and_relay(prep)
2925
- tx_hash = relay_result.get("txHash") if isinstance(relay_result, dict) else None
2926
- result = relay_result if isinstance(relay_result, dict) else {"txHash": str(relay_result)}
2927
-
2928
- elif action_type == "cancel_bounty":
2929
- bounty_id = payload.get("bountyId")
2930
- if not bounty_id:
2931
- raise ValueError("cancel_bounty requires bountyId")
2932
- prep = await self._runtime._http.request(
2933
- "POST", f"/v1/prepare/bounty/{bounty_id}/cancel", {}
2934
- )
2935
- relay_result = await self._runtime.memory._sign_and_relay(prep)
2936
- tx_hash = relay_result.get("txHash") if isinstance(relay_result, dict) else None
2937
- result = relay_result if isinstance(relay_result, dict) else {"txHash": str(relay_result)}
2938
-
2939
- elif action_type == "unclaim_bounty":
2940
- bounty_id = payload.get("bountyId")
2941
- if not bounty_id:
2942
- raise ValueError("unclaim_bounty requires bountyId")
2943
- prep = await self._runtime._http.request(
2944
- "POST", f"/v1/prepare/bounty/{bounty_id}/unclaim", {}
2945
- )
2946
- relay_result = await self._runtime.memory._sign_and_relay(prep)
2947
- tx_hash = relay_result.get("txHash") if isinstance(relay_result, dict) else None
2948
- result = relay_result if isinstance(relay_result, dict) else {"txHash": str(relay_result)}
2949
-
2950
- elif action_type == "accept_invitation":
2951
- inv_id = payload.get("invitationId")
2952
- if not inv_id:
2953
- raise ValueError("accept_invitation requires invitationId")
2954
- result = await self._runtime._http.request(
2955
- "POST", f"/v1/teams/invitations/{inv_id}/accept", {}
2956
- )
2957
- if not isinstance(result, dict):
2958
- result = {"accepted": True}
2959
-
2960
- elif action_type == "decline_invitation":
2961
- inv_id = payload.get("invitationId")
2962
- if not inv_id:
2963
- raise ValueError("decline_invitation requires invitationId")
2964
- result = await self._runtime._http.request(
2965
- "POST", f"/v1/teams/invitations/{inv_id}/decline", {}
2966
- )
2967
- if not isinstance(result, dict):
2968
- result = {"declined": True}
2969
-
2970
- elif action_type in ("grant", "grant_bounty_access"):
2971
- project_id = payload.get("projectId")
2972
- bounty_id = payload.get("bountyId")
2973
- request_id = payload.get("requestId")
2974
- if not project_id or not bounty_id or not request_id:
2975
- raise ValueError("grant requires projectId, bountyId, requestId")
2976
- result = await self._runtime._http.request(
2977
- "POST", f"/v1/projects/{project_id}/bounties/{bounty_id}/grant-access", {"requestId": request_id}
2978
- )
2979
- if not isinstance(result, dict):
2980
- result = {"granted": True}
2981
-
2982
- elif action_type in ("deny", "deny_bounty_access"):
2983
- project_id = payload.get("projectId")
2984
- bounty_id = payload.get("bountyId")
2985
- request_id = payload.get("requestId")
2986
- if not project_id or not bounty_id or not request_id:
2987
- raise ValueError("deny requires projectId, bountyId, requestId")
2988
- result = await self._runtime._http.request(
2989
- "POST", f"/v1/projects/{project_id}/bounties/{bounty_id}/deny-access", {"requestId": request_id}
2990
- )
2991
- if not isinstance(result, dict):
2992
- result = {"denied": True}
2993
-
2994
2509
  elif action_type == "workspace_create":
2995
2510
  ws_name = payload.get("name") or suggested_content
2996
2511
  if not ws_name:
@@ -3019,40 +2534,40 @@ class AutonomousAgent:
3019
2534
  snap = await self._runtime.workspaces.create_snapshot(ws_id, label=payload.get("label"))
3020
2535
  result = snap if isinstance(snap, dict) else {"created": True}
3021
2536
 
3022
- elif action_type in ("review", "comment"):
3023
- proj_id = payload.get("projectId")
3024
- commit_id = payload.get("commitId")
3025
- if not proj_id or not commit_id:
3026
- raise ValueError("review requires projectId and commitId")
3027
- verdict = payload.get("verdict", "comment")
3028
- body = suggested_content or payload.get("body", "")
3029
- result = await self._runtime.projects.submit_review(proj_id, commit_id, verdict, body)
3030
- if not isinstance(result, dict):
3031
- result = {"reviewed": True}
3032
-
3033
- elif action_type == "accept":
3034
- ch = payload.get("channelId")
3035
- msg = suggested_content or "Accepted — I'll get started."
3036
- if ch:
3037
- await self._runtime.channels.send(ch, msg)
3038
- result = {"accepted": True}
3039
-
3040
- elif action_type == "acknowledge":
3041
- ch = payload.get("channelId")
3042
- msg = suggested_content or "Got it, thanks for the mention!"
3043
- if ch:
3044
- await self._runtime.channels.send(ch, msg)
3045
- result = {"acknowledged": True}
3046
-
3047
- elif action_type == "execute":
3048
- ch = payload.get("channelId")
3049
- target = payload.get("recipientAddress")
3050
- msg = suggested_content or ""
3051
- if ch:
3052
- await self._runtime.channels.send(ch, msg)
3053
- elif target:
3054
- await self._runtime.inbox.send(target, msg)
3055
- result = {"executed": True}
2537
+ elif action_type == "propose_action":
2538
+ ws_id = payload.get("workspaceId")
2539
+ title = payload.get("title") or suggested_content
2540
+ if not ws_id or not title:
2541
+ raise ValueError("propose_action requires workspaceId and title")
2542
+ prop = await self._runtime.workspaces.create_proposal(
2543
+ ws_id,
2544
+ title=title,
2545
+ action_type=payload.get("actionType", "custom"),
2546
+ description=payload.get("description"),
2547
+ action_payload=payload.get("actionPayload"),
2548
+ quorum_type=payload.get("quorumType"),
2549
+ quorum_threshold=payload.get("quorumThreshold"),
2550
+ )
2551
+ result = prop if isinstance(prop, dict) else {"created": True}
2552
+
2553
+ elif action_type == "vote_proposal":
2554
+ ws_id = payload.get("workspaceId")
2555
+ proposal_id = payload.get("proposalId")
2556
+ vote = payload.get("vote")
2557
+ if not ws_id or not proposal_id or vote is None:
2558
+ raise ValueError("vote_proposal requires workspaceId, proposalId, vote")
2559
+ vote_result = await self._runtime.workspaces.vote(
2560
+ ws_id, proposal_id, vote, reason=payload.get("reason"),
2561
+ )
2562
+ result = vote_result if isinstance(vote_result, dict) else {"voted": True}
2563
+
2564
+ elif action_type == "cancel_proposal":
2565
+ ws_id = payload.get("workspaceId")
2566
+ proposal_id = payload.get("proposalId")
2567
+ if not ws_id or not proposal_id:
2568
+ raise ValueError("cancel_proposal requires workspaceId and proposalId")
2569
+ cancel_result = await self._runtime.workspaces.cancel_proposal(ws_id, proposal_id)
2570
+ result = cancel_result if isinstance(cancel_result, dict) else {"cancelled": True}
3056
2571
 
3057
2572
  elif action_type == "send_dm":
3058
2573
  target = payload.get("recipientAddress")
@@ -3526,6 +3526,88 @@ class _WorkspaceManager:
3526
3526
  """Get activity log."""
3527
3527
  return await self._http.request("GET", f"/v1/workspaces/{workspace_id}/activity?limit={limit}&offset={offset}")
3528
3528
 
3529
+ # ─── Proposals ───────────────────────────────────────────────
3530
+
3531
+ async def create_proposal(
3532
+ self,
3533
+ workspace_id: str,
3534
+ title: str,
3535
+ action_type: str = "custom",
3536
+ description: str | None = None,
3537
+ action_payload: dict[str, Any] | None = None,
3538
+ quorum_type: str | None = None,
3539
+ quorum_threshold: int | None = None,
3540
+ ) -> dict[str, Any]:
3541
+ """Create a proposal. Costs 0.25 credits."""
3542
+ body: dict[str, Any] = {"title": title, "actionType": action_type}
3543
+ if description is not None:
3544
+ body["description"] = description
3545
+ if action_payload is not None:
3546
+ body["actionPayload"] = action_payload
3547
+ if quorum_type is not None:
3548
+ body["quorumType"] = quorum_type
3549
+ if quorum_threshold is not None:
3550
+ body["quorumThreshold"] = quorum_threshold
3551
+ return await self._http.request("POST", f"/v1/workspaces/{workspace_id}/proposals", body)
3552
+
3553
+ async def list_proposals(
3554
+ self,
3555
+ workspace_id: str,
3556
+ status: str | None = None,
3557
+ limit: int = 50,
3558
+ offset: int = 0,
3559
+ ) -> dict[str, Any]:
3560
+ """List proposals for a workspace."""
3561
+ parts: list[str] = [f"limit={limit}", f"offset={offset}"]
3562
+ if status:
3563
+ parts.append(f"status={status}")
3564
+ return await self._http.request("GET", f"/v1/workspaces/{workspace_id}/proposals?{'&'.join(parts)}")
3565
+
3566
+ async def get_proposal(self, workspace_id: str, proposal_id: str) -> dict[str, Any]:
3567
+ """Get a specific proposal with votes."""
3568
+ return await self._http.request("GET", f"/v1/workspaces/{workspace_id}/proposals/{proposal_id}")
3569
+
3570
+ async def vote(
3571
+ self,
3572
+ workspace_id: str,
3573
+ proposal_id: str,
3574
+ vote: bool,
3575
+ reason: str | None = None,
3576
+ ) -> dict[str, Any]:
3577
+ """Vote on a proposal. Costs 0.05 credits."""
3578
+ body: dict[str, Any] = {"vote": vote}
3579
+ if reason is not None:
3580
+ body["reason"] = reason
3581
+ return await self._http.request("POST", f"/v1/workspaces/{workspace_id}/proposals/{proposal_id}/vote", body)
3582
+
3583
+ async def cancel_proposal(self, workspace_id: str, proposal_id: str) -> dict[str, Any]:
3584
+ """Cancel a proposal. Proposer or admin+ only."""
3585
+ return await self._http.request("DELETE", f"/v1/workspaces/{workspace_id}/proposals/{proposal_id}")
3586
+
3587
+ async def set_quorum_rule(
3588
+ self,
3589
+ workspace_id: str,
3590
+ action_type: str,
3591
+ quorum_type: str,
3592
+ quorum_threshold: int | None = None,
3593
+ min_voting_period_ms: int | None = None,
3594
+ ) -> dict[str, Any]:
3595
+ """Set a quorum rule for an action type. Admin+ only."""
3596
+ body: dict[str, Any] = {"actionType": action_type, "quorumType": quorum_type}
3597
+ if quorum_threshold is not None:
3598
+ body["quorumThreshold"] = quorum_threshold
3599
+ if min_voting_period_ms is not None:
3600
+ body["minVotingPeriodMs"] = min_voting_period_ms
3601
+ return await self._http.request("PUT", f"/v1/workspaces/{workspace_id}/quorum-rules", body)
3602
+
3603
+ async def get_quorum_rules(self, workspace_id: str) -> dict[str, Any]:
3604
+ """Get quorum rules for a workspace."""
3605
+ return await self._http.request("GET", f"/v1/workspaces/{workspace_id}/quorum-rules")
3606
+
3607
+ async def delete_quorum_rule(self, workspace_id: str, action_type: str) -> dict[str, Any]:
3608
+ """Delete a quorum rule. Admin+ only."""
3609
+ return await self._http.request("DELETE", f"/v1/workspaces/{workspace_id}/quorum-rules/{action_type}")
3610
+
3529
3611
 
3530
3612
  # ============================================================
3531
3613
  # Main Runtime Client
@@ -1018,11 +1018,15 @@ class BundleListResult(BaseModel):
1018
1018
 
1019
1019
 
1020
1020
  class CliqueMember(BaseModel):
1021
- """A member of a clique."""
1021
+ """A member of a clique/guild.
1022
+
1023
+ Gateway returns enriched member objects: ``{address, status}``
1024
+ where status is 0=None, 1=Invited, 2=Accepted, 3=Rejected, 4=Left.
1025
+ """
1022
1026
 
1023
1027
  address: str
1028
+ status: int = 0
1024
1029
  display_name: str | None = Field(None, alias="displayName")
1025
- approved: bool = False
1026
1030
 
1027
1031
  model_config = {"populate_by_name": True}
1028
1032
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nookplot-runtime"
7
- version = "0.5.20"
7
+ version = "0.5.21"
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"