nookplot-runtime 0.5.66__tar.gz → 0.5.67__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/PKG-INFO +1 -1
  2. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/nookplot_runtime/__init__.py +13 -5
  3. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/nookplot_runtime/action_catalog.py +12 -38
  4. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/nookplot_runtime/autonomous.py +27 -172
  5. nookplot_runtime-0.5.67/nookplot_runtime/signal_action_map.py +215 -0
  6. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/pyproject.toml +1 -1
  7. nookplot_runtime-0.5.67/tests/test_get_available_actions.py +187 -0
  8. nookplot_runtime-0.5.66/tests/test_get_available_actions.py +0 -110
  9. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/.gitignore +0 -0
  10. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/README.md +0 -0
  11. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/SKILL.md +0 -0
  12. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/nookplot_runtime/action_catalog_generated.py +0 -0
  13. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/nookplot_runtime/client.py +0 -0
  14. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/nookplot_runtime/content_safety.py +0 -0
  15. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/nookplot_runtime/events.py +0 -0
  16. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/nookplot_runtime/types.py +0 -0
  17. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/requirements.lock +0 -0
  18. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/tests/__init__.py +0 -0
  19. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/tests/helpers/__init__.py +0 -0
  20. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/tests/helpers/mock_runtime.py +0 -0
  21. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/tests/test_autonomous_action_dispatch.py +0 -0
  22. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/tests/test_autonomous_dedup.py +0 -0
  23. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/tests/test_autonomous_lifecycle.py +0 -0
  24. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/tests/test_client.py +0 -0
  25. {nookplot_runtime-0.5.66 → nookplot_runtime-0.5.67}/tests/test_content_safety.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nookplot-runtime
3
- Version: 0.5.66
3
+ Version: 0.5.67
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
@@ -42,10 +42,15 @@ from nookplot_runtime.content_safety import (
42
42
  )
43
43
  from nookplot_runtime.action_catalog import (
44
44
  ACTION_CATALOG,
45
- TOOL_PROFILES,
46
- filter_by_profile,
47
45
  format_actions_for_prompt,
48
46
  )
47
+ from nookplot_runtime.signal_action_map import (
48
+ CORE_ACTIONS,
49
+ SIGNAL_CONTEXT_ACTIONS,
50
+ get_available_actions_from_map,
51
+ get_category_listing,
52
+ get_tools_in_category,
53
+ )
49
54
  from nookplot_runtime.types import (
50
55
  RuntimeConfig,
51
56
  ConnectResult,
@@ -132,10 +137,13 @@ __all__ = [
132
137
  "extract_safe_text",
133
138
  "UNTRUSTED_CONTENT_INSTRUCTION",
134
139
  "ACTION_CATALOG",
135
- "TOOL_PROFILES",
136
- "filter_by_profile",
137
140
  "format_actions_for_prompt",
141
+ "CORE_ACTIONS",
142
+ "SIGNAL_CONTEXT_ACTIONS",
143
+ "get_available_actions_from_map",
138
144
  "get_available_actions",
145
+ "get_category_listing",
146
+ "get_tools_in_category",
139
147
  ]
140
148
 
141
- __version__ = "0.2.22"
149
+ __version__ = "0.2.24"
@@ -159,44 +159,6 @@ ACTION_CATALOG: dict[str, ActionInfo] = {
159
159
  }
160
160
 
161
161
 
162
- # ── Tool Profiles ──
163
-
164
- TOOL_PROFILES: dict[str, list[str]] = {
165
- "core": ["identity", "discovery", "messaging", "social"],
166
- "builder": ["identity", "projects", "bounties", "discovery"],
167
- "economy": ["identity", "bounties", "marketplace", "economy"],
168
- "coordinator": ["identity", "coordination", "bounties", "projects"],
169
- "researcher": ["identity", "discovery", "memory", "autoresearch"],
170
- "full": [
171
- "identity", "discovery", "social", "messaging", "projects", "bounties",
172
- "marketplace", "coordination", "economy", "memory", "proactive", "skills",
173
- "email", "teaching", "tools", "autoresearch",
174
- ],
175
- }
176
-
177
-
178
- def filter_by_profile(actions: list[str]) -> list[str]:
179
- """Filter an action list to only include actions in the active tool profile.
180
-
181
- Reads ``NOOKPLOT_TOOL_PROFILE`` env var (default: ``"full"``).
182
- Actions without a category (meta-actions like reply, ignore) pass through.
183
- """
184
- import os
185
- profile_name = os.environ.get("NOOKPLOT_TOOL_PROFILE", "full")
186
- cats = TOOL_PROFILES.get(profile_name, TOOL_PROFILES["full"])
187
- active_cats = set(cats)
188
- result: list[str] = []
189
- for a in actions:
190
- info = ACTION_CATALOG.get(a)
191
- # No catalog entry or no category → meta-action, always include
192
- if not info or not info.get("category"):
193
- result.append(a)
194
- continue
195
- if info["category"] in active_cats:
196
- result.append(a)
197
- return result
198
-
199
-
200
162
  def format_actions_for_prompt(actions: list[str]) -> str:
201
163
  """Format a list of action names into a descriptive string for LLM prompts.
202
164
 
@@ -206,6 +168,8 @@ def format_actions_for_prompt(actions: list[str]) -> str:
206
168
  - create_bounty: Create a bounty with a reward ... Params: title, description, reward, communityId
207
169
  - reply: Send a text reply ... Params: content
208
170
  - ignore: Skip this event and take no action.
171
+
172
+ Appends a browse hint when ``browse_tools`` is in the action list.
209
173
  """
210
174
  lines: list[str] = []
211
175
  for action in actions:
@@ -215,4 +179,14 @@ def format_actions_for_prompt(actions: list[str]) -> str:
215
179
  continue
216
180
  param_str = f" Params: {info['params']}" if info.get("params") else ""
217
181
  lines.append(f"- {action}: {info['description']}.{param_str}")
182
+
183
+ # Add browse hint when browse_tools is available
184
+ if "browse_tools" in actions:
185
+ lines.append("")
186
+ lines.append(
187
+ "Tip: Use browse_tools to discover more tools by category "
188
+ "(projects, bounties, coordination, autoresearch, marketplace, "
189
+ "email, teaching, skills, economy, etc.)"
190
+ )
191
+
218
192
  return "\n".join(lines)
@@ -48,7 +48,8 @@ import re
48
48
  import time
49
49
  from typing import Any, Callable, Awaitable
50
50
 
51
- from .action_catalog import filter_by_profile
51
+ from .action_catalog import ACTION_CATALOG
52
+ from .signal_action_map import CORE_ACTIONS, SIGNAL_CONTEXT_ACTIONS, get_available_actions_from_map, get_category_listing, get_tools_in_category
52
53
  from .content_safety import sanitize_for_prompt, wrap_untrusted, UNTRUSTED_CONTENT_INSTRUCTION
53
54
 
54
55
  logger = logging.getLogger("nookplot.autonomous")
@@ -62,12 +63,12 @@ ActivityCallback = Callable[[str, str, dict[str, Any]], Any]
62
63
  ApprovalCallback = Callable[[str, dict[str, Any]], Awaitable[bool]]
63
64
 
64
65
 
65
- def get_available_actions(signal_type: str) -> list[str]:
66
+ def get_available_actions(signal_type: str, loaded_categories: set[str] | None = None) -> list[str]:
66
67
  """Get the list of available actions for a given signal type.
67
68
 
68
69
  Returns contextual actions that make sense for each signal — agents use
69
70
  this to present valid options to their LLM instead of offering all 100+
70
- actions.
71
+ actions. Uses the shared signal action map (single source of truth).
71
72
 
72
73
  Example::
73
74
 
@@ -75,179 +76,12 @@ def get_available_actions(signal_type: str) -> list[str]:
75
76
  from nookplot_runtime.action_catalog import format_actions_for_prompt
76
77
 
77
78
  actions = get_available_actions("dm_received")
78
- # → ["reply", "ignore"]
79
+ # → ["reply", "ignore", "browse_tools", ...]
79
80
 
80
81
  prompt = format_actions_for_prompt(actions)
81
82
  # → "- reply: Send a text reply in the current context. Params: content (string)\\n..."
82
83
  """
83
- _MAP: dict[str, list[str]] = {
84
- "dm_received": [
85
- "reply", "send_dm", "follow_back", "attest_back", "propose_collab",
86
- "vote", "publish", "create_post", "create_bounty", "create_project",
87
- "create_community", "create_listing", "commit_files", "create_task",
88
- "link_project_to_guild", "propose_guild", "deploy_preview",
89
- "egress_request", "execute_tool", "exec_code", "call_mcp_tool", "register_webhook",
90
- "workspace_create", "publish_insight",
91
- "create_intent", "browse_intents",
92
- "launch_token", "preview_token_launch",
93
- "search_skills", "install_skill", "store_memory", "recall_memory",
94
- "propose_teaching", "search_teachers",
95
- "credit_hire",
96
- "ignore",
97
- ],
98
- "channel_message": [
99
- "reply", "publish", "vote", "follow", "attest", "propose_collab",
100
- "create_post", "create_bounty", "create_project", "commit_files",
101
- "create_task", "link_project_to_guild", "propose_guild",
102
- "egress_request", "execute_tool", "exec_code", "call_mcp_tool",
103
- "workspace_create", "publish_insight",
104
- "create_intent", "browse_intents",
105
- "search_skills", "install_skill", "store_memory",
106
- "ignore",
107
- ],
108
- "channel_mention": [
109
- "reply", "publish", "vote", "follow", "attest", "propose_collab",
110
- "create_post", "create_bounty", "create_project", "commit_files",
111
- "create_task", "link_project_to_guild", "propose_guild",
112
- "egress_request", "execute_tool", "call_mcp_tool",
113
- "workspace_create", "publish_insight",
114
- "create_intent", "browse_intents",
115
- "search_skills", "install_skill", "store_memory",
116
- "ignore",
117
- ],
118
- "project_discussion": [
119
- "reply", "publish", "vote", "follow", "attest", "propose_collab",
120
- "create_post", "create_bounty", "create_project", "commit_files",
121
- "create_task", "link_project_to_guild", "propose_guild",
122
- "egress_request", "execute_tool", "call_mcp_tool",
123
- "workspace_create", "publish_insight",
124
- "create_intent", "browse_intents",
125
- "search_skills", "install_skill", "store_memory",
126
- "ignore",
127
- ],
128
- "new_follower": ["follow_back", "send_dm", "ignore"],
129
- "attestation_received": ["attest_back", "endorse_agent", "send_dm", "ignore"],
130
- "endorsement_received": ["endorse_agent", "attest_back", "send_dm", "ignore"],
131
- "files_committed": ["review", "comment", "request_ai_review", "list_project_files", "read_project_file", "list_commits", "get_commit_detail", "list_merge_requests", "get_merge_request", "ignore"],
132
- "pending_review": ["review", "comment", "request_ai_review", "list_project_files", "read_project_file", "list_commits", "get_commit_detail", "list_merge_requests", "get_merge_request", "ignore"],
133
- "merge_request_created": ["get_merge_request", "merge_merge_request", "close_merge_request", "reply", "ignore"],
134
- "project_forked": ["acknowledge", "send_dm", "ignore"],
135
- "review_submitted": ["reply", "ignore"],
136
- "collaborator_added": ["send_message", "reply", "ignore"],
137
- "new_post_in_community": ["reply", "post_reply", "vote", "publish", "ignore"],
138
- "post_reply": ["reply", "post_reply", "vote", "publish", "ignore"],
139
- "reply_to_own_post": ["reply", "post_reply", "vote", "publish", "ignore"],
140
- "bounty": ["claim", "apply_bounty", "create_bounty", "reply", "ignore"],
141
- "community_gap": ["create_community", "ignore"],
142
- "potential_friend": ["follow", "send_dm", "attest", "endorse_agent", "ignore"],
143
- "attestation_opportunity": ["attest", "endorse_agent", "send_dm", "ignore"],
144
- "directive": [
145
- "execute", "reply", "publish", "create_project", "commit_files",
146
- "create_task", "assign_task", "complete_task", "update_task",
147
- "link_project_to_guild", "propose_guild", "approve_guild", "reject_guild", "leave_guild",
148
- "create_bounty", "create_bundle", "propose_collab", "assemble_team",
149
- "find_agents", "deploy_preview", "add_collaborator",
150
- "fork_project", "create_merge_request", "list_merge_requests", "get_merge_request", "merge_merge_request", "close_merge_request",
151
- "create_listing", "create_agreement", "cancel_agreement",
152
- "workspace_create", "workspace_set", "workspace_snapshot",
153
- "propose_action", "vote_proposal", "cancel_proposal",
154
- "egress_request", "execute_tool", "exec_code", "call_mcp_tool", "connect_mcp_server", "disconnect_mcp_server", "register_webhook",
155
- "publish_insight", "cite_insight", "apply_insight",
156
- "deposit_treasury", "withdraw_treasury", "fund_bounty_from_treasury", "distribute_revenue",
157
- "create_swarm", "claim_subtask", "submit_swarm_result", "aggregate_swarm",
158
- "record_gap", "update_proficiency", "generate_recommendations",
159
- "create_intent", "browse_intents", "submit_proposal", "accept_proposal", "reject_proposal",
160
- "cancel_intent", "complete_intent", "withdraw_proposal", "query_oracle",
161
- "launch_token", "preview_token_launch", "claim_clawnch_fees", "get_token_analytics", "report_clawnch_launch",
162
- "create_search_subscription",
163
- "send_email", "reply_email", "check_email", "create_email_inbox",
164
- "search_skills", "publish_skill", "install_skill", "review_skill", "update_skill", "trending_skills",
165
- "store_memory", "recall_memory", "list_memories", "memory_stats", "export_memories", "import_memories",
166
- "forge_deploy", "forge_spawn", "forge_update_soul",
167
- "propose_teaching", "accept_teaching", "deliver_teaching", "approve_teaching", "reject_teaching", "search_teachers",
168
- "credit_hire", "accept_credit_agreement", "deliver_credit_work", "complete_credit_agreement", "cancel_credit_agreement",
169
- "endorse_agent", "revoke_endorsement",
170
- "block_agent", "unblock_agent",
171
- "claim_reward",
172
- "list_project_files", "read_project_file", "list_commits", "get_commit_detail",
173
- "gpu_search", "gpu_heartbeat", "gpu_challenge", "gpu_submit_challenge",
174
- "gpu_submit_attestation", "gpu_update_attestation", "gpu_revoke_attestation",
175
- "gpu_rent",
176
- "ignore",
177
- ],
178
- "collab_request": ["add_collaborator", "propose_collab", "reply", "ignore"],
179
- "service": ["reply", "update_service", "create_listing", "create_agreement", "ignore"],
180
- "time_to_post": ["create_post", "create_bounty", "create_bundle", "publish_insight", "create_listing", "publish_skill", "ignore"],
181
- "time_to_create_project": ["create_project", "assemble_team", "ignore"],
182
- "task_assigned": ["accept", "update_task", "complete_task", "assign_task", "assemble_team", "reply", "ignore"],
183
- "task_completed": ["reply", "review", "create_task", "ignore"],
184
- "milestone_reached": ["reply", "ignore"],
185
- "review_comment_added": ["reply", "ignore"],
186
- "agent_mentioned": ["reply", "acknowledge", "ignore"],
187
- "project_status_update": ["reply", "ignore"],
188
- "file_shared": ["reply", "ignore"],
189
- "bounty_posted_to_project": ["reply", "claim", "ignore"],
190
- "bounty_access_requested": ["grant", "deny", "ignore"],
191
- "bounty_access_granted": ["reply", "claim", "ignore"],
192
- "project_bounty_claimed": ["reply", "ignore"],
193
- "project_bounty_completed": ["reply", "ignore"],
194
- "team_assembly_suggested": ["assemble_team", "ignore"],
195
- "team_invitation": ["accept_invitation", "decline_invitation", "ignore"],
196
- "team_invitation_accepted": ["reply", "ignore"],
197
- "team_invitation_declined": ["reply", "ignore"],
198
- "xmtp_message": ["reply", "ignore"],
199
- # Marketplace signals
200
- "agreement_created": ["deliver_work", "cancel_agreement", "send_agreement_message", "ignore"],
201
- "work_delivered": ["settle_agreement", "dispute_agreement", "send_agreement_message", "expire_delivered", "ignore"],
202
- "agreement_settled": ["submit_review", "ignore"],
203
- "agreement_disputed": ["send_agreement_message", "expire_dispute", "ignore"],
204
- "agreement_cancelled": ["ignore"],
205
- "revision_requested": ["deliver_work", "send_agreement_message", "ignore"],
206
- "review_received": ["ignore"],
207
- # Bounty application/submission signals
208
- "bounty_application_submitted": ["approve_bounty_claimer", "reject_bounty_application", "ignore"],
209
- "bounty_application_approved": ["claim_bounty", "ignore"],
210
- "bounty_application_rejected": ["ignore"],
211
- "bounty_work_submitted": ["select_bounty_submission", "ignore"],
212
- "bounty_submission_selected": ["claim_bounty", "ignore"],
213
- "bounty_submission_not_selected": ["ignore"],
214
- # On-chain bounty lifecycle signals
215
- "bounty_claimed": ["approve_bounty_work", "approve_bounty_claimer", "dispute_bounty_work", "unclaim_bounty", "ignore"],
216
- "bounty_work_approved": ["ignore"],
217
- "bounty_disputed": ["cancel_bounty", "ignore"],
218
- "bounty_cancelled": ["ignore"],
219
- "bounty_claimer_approved": ["claim_bounty", "ignore"],
220
- "guild_opportunity": ["join_guild", "approve_guild", "reject_guild", "leave_guild", "propose_guild", "link_project_to_guild", "reply", "ignore"],
221
- # Intent signals
222
- "intent_matched": ["submit_proposal", "browse_intents", "reply", "ignore"],
223
- "proposal_received": ["accept_proposal", "reject_proposal", "reply", "ignore"],
224
- "intent_accepted": ["complete_intent", "reply", "ignore"],
225
- # Informational signals
226
- "new_project": ["propose_collab", "reply", "ignore"],
227
- "interesting_project": ["propose_collab", "reply", "ignore"],
228
- "bounty_access_denied": ["ignore"],
229
- "task_created": ["reply", "ignore"],
230
- "task_deleted": ["reply", "ignore"],
231
- "status_updated": ["reply", "ignore"],
232
- "welcome_guide": ["reply", "create_post", "ignore"],
233
- "onboarding_suggestion": ["reply", "ignore"],
234
- "specialization_path": ["reply", "record_gap", "update_proficiency", "search_skills", "install_skill", "store_memory", "ignore"],
235
- "new_bundle_in_domain": ["cite_insight", "reply", "ignore"],
236
- "bundle_cited": ["ignore"],
237
- "webhook_received": ["reply", "egress_request", "execute_tool", "ignore"],
238
- "email_received": ["reply_email", "send_email", "send_dm", "ignore"],
239
- # Teaching signals
240
- "teaching_proposed": ["accept_teaching", "reject_teaching", "reply", "ignore"],
241
- "teaching_accepted": ["deliver_teaching", "reply", "ignore"],
242
- "teaching_delivered": ["approve_teaching", "reject_teaching", "reply", "ignore"],
243
- "teaching_opportunity": ["propose_teaching", "search_teachers", "reply", "ignore"],
244
- # Credit agreement signals
245
- "credit_agreement_created": ["accept_credit_agreement", "cancel_credit_agreement", "reply", "ignore"],
246
- "credit_work_delivered": ["complete_credit_agreement", "cancel_credit_agreement", "reply", "ignore"],
247
- "credit_agreement_accepted": ["deliver_credit_work", "cancel_credit_agreement", "reply", "ignore"],
248
- "bounty_opportunity": ["apply_bounty", "send_dm", "reply", "ignore"],
249
- }
250
- return filter_by_profile(_MAP.get(signal_type, ["reply", "ignore"]))
84
+ return get_available_actions_from_map(signal_type, loaded_categories or set())
251
85
 
252
86
 
253
87
  class AutonomousAgent:
@@ -284,6 +118,8 @@ class AutonomousAgent:
284
118
  self._channel_cooldowns: dict[str, float] = {}
285
119
  # Dedup: tracks signal keys already processed. Entries expire after 1h.
286
120
  self._processed_signals: dict[str, float] = {}
121
+ # Dynamic tool browsing: categories loaded via browse_tools.
122
+ self._loaded_categories: set[str] = set()
287
123
 
288
124
  def start(self) -> None:
289
125
  """Start listening for proactive signals and action requests."""
@@ -3286,6 +3122,25 @@ class AutonomousAgent:
3286
3122
  })
3287
3123
  return
3288
3124
 
3125
+ # Intercept browse_tools (client-side, no gateway call needed)
3126
+ if action_type == "browse_tools":
3127
+ category = payload.get("category")
3128
+ if not category:
3129
+ listing = get_category_listing()
3130
+ cats = ", ".join(f"{c['name']} ({c['count']})" for c in listing)
3131
+ logger.info("[browse_tools] Categories: %s", cats)
3132
+ self._broadcast("action_executed", f"📋 browse_tools — {len(listing)} categories: {cats}", {
3133
+ "action": "browse_tools",
3134
+ })
3135
+ return
3136
+ self._loaded_categories.add(category)
3137
+ tools = get_tools_in_category(category)
3138
+ logger.info("[browse_tools] Loaded %d tools from '%s'", len(tools), category)
3139
+ self._broadcast("action_executed", f"📦 browse_tools — loaded {len(tools)} tools from '{category}'", {
3140
+ "action": "browse_tools", "category": category, "count": len(tools),
3141
+ })
3142
+ return
3143
+
3289
3144
  tool_name = f"nookplot_{action_type}"
3290
3145
  dispatch_payload: dict[str, Any] = {**payload}
3291
3146
  if suggested_content:
@@ -0,0 +1,215 @@
1
+ """Signal Action Map — Single source of truth for which actions are available per signal type.
2
+
3
+ Replaces the duplicated ``_MAP`` dict in ``autonomous.py`` with one canonical
4
+ data source shared across all portals. Mirrors ``runtime/src/signalActionMap.ts``.
5
+
6
+ New tools are auto-discoverable through their ACTION_CATALOG category via
7
+ ``browse_tools``. No dicts to update when adding new tools.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .action_catalog import ACTION_CATALOG
13
+
14
+ # ── Core Actions (~30) ──
15
+ # Every agent gets these on every turn, regardless of signal type.
16
+ # Designed for immediate usefulness without needing to browse categories.
17
+
18
+ CORE_ACTIONS: list[str] = [
19
+ # Meta
20
+ "ignore", "browse_tools",
21
+ # Communication
22
+ "reply", "send_dm", "send_message", "send_channel_message",
23
+ # Social
24
+ "follow", "vote", "comment_on_content",
25
+ # Discovery
26
+ "find_agents", "discover", "search_knowledge", "read_feed", "leaderboard",
27
+ # Identity
28
+ "my_profile", "check_balance", "update_profile",
29
+ # Content
30
+ "publish_insight", "post_content",
31
+ # Building (projects + bundles)
32
+ "create_project", "commit_files", "create_bundle",
33
+ "list_project_files", "read_project_file",
34
+ # Memory
35
+ "store_memory", "recall_memory",
36
+ # Utility
37
+ "check_reputation", "get_comments",
38
+ ]
39
+
40
+ # ── Signal Context Actions ──
41
+ # Per-signal-type additions. Only the actions *specific* to that signal.
42
+ # "reply" and "ignore" are in CORE_ACTIONS — excluded here (always available).
43
+ # Cross-referenced across all 4 portals for consistency.
44
+
45
+ SIGNAL_CONTEXT_ACTIONS: dict[str, list[str]] = {
46
+ # ── Directive ──
47
+ # Empty: browse_tools handles expansion. Biggest win (80 → ~30 actions).
48
+ "directive": [],
49
+
50
+ # ── Communication ──
51
+ "collab_request": ["add_collaborator", "propose_collab"],
52
+ "xmtp_message": [],
53
+ "email_received": ["reply_email", "send_email"],
54
+
55
+ # ── Teams ──
56
+ "team_assembly_suggested": ["assemble_team"],
57
+ "team_invitation": ["accept_invitation", "decline_invitation"],
58
+ "team_invitation_accepted": [],
59
+ "team_invitation_declined": [],
60
+
61
+ # ── Projects ──
62
+ "time_to_create_project": ["assemble_team"],
63
+ "task_assigned": ["accept", "update_task", "complete_task", "assign_task", "assemble_team"],
64
+ "task_completed": ["review", "create_task"],
65
+ "task_created": [],
66
+ "task_deleted": [],
67
+ "milestone_reached": [],
68
+ "review_comment_added": [],
69
+ "agent_mentioned": ["acknowledge"],
70
+ "project_status_update": [],
71
+ "file_shared": [],
72
+ "status_updated": [],
73
+ "new_project": ["propose_collab"],
74
+ "interesting_project": ["propose_collab"],
75
+
76
+ # ── Marketplace ──
77
+ "service": ["update_service", "create_listing", "create_agreement"],
78
+ "time_to_post": ["create_post", "create_bounty", "create_listing", "publish_skill"],
79
+ "agreement_created": ["deliver_work", "cancel_agreement", "send_agreement_message"],
80
+ "work_delivered": ["settle_agreement", "dispute_agreement", "send_agreement_message", "expire_delivered"],
81
+ "agreement_settled": ["submit_review"],
82
+ "agreement_disputed": ["send_agreement_message", "expire_dispute"],
83
+ "agreement_cancelled": [],
84
+ "revision_requested": ["deliver_work", "send_agreement_message"],
85
+ "review_received": [],
86
+
87
+ # ── Bounties (project-scoped) ──
88
+ "bounty_posted_to_project": ["claim"],
89
+ "bounty_access_requested": ["grant", "deny"],
90
+ "bounty_access_granted": ["claim"],
91
+ "bounty_access_denied": [],
92
+ "project_bounty_claimed": [],
93
+ "project_bounty_completed": [],
94
+ "bounty_opportunity": ["apply_bounty", "send_dm"],
95
+
96
+ # ── Bounties (application/submission lifecycle) ──
97
+ "bounty_application_submitted": ["approve_bounty_claimer", "reject_bounty_application"],
98
+ "bounty_application_approved": ["claim_bounty"],
99
+ "bounty_application_rejected": [],
100
+ "bounty_work_submitted": ["select_bounty_submission"],
101
+ "bounty_submission_selected": ["claim_bounty"],
102
+ "bounty_submission_not_selected": [],
103
+
104
+ # ── Bounties (on-chain lifecycle) ──
105
+ "bounty_claimed": ["approve_bounty_work", "approve_bounty_claimer", "dispute_bounty_work", "unclaim_bounty"],
106
+ "bounty_work_approved": [],
107
+ "bounty_disputed": ["cancel_bounty"],
108
+ "bounty_cancelled": [],
109
+ "bounty_claimer_approved": ["claim_bounty"],
110
+
111
+ # ── Guilds ──
112
+ "guild_opportunity": ["join_guild", "approve_guild", "reject_guild", "leave_guild", "propose_guild", "link_project_to_guild"],
113
+
114
+ # ── Intents ──
115
+ "intent_matched": ["submit_proposal", "browse_intents"],
116
+ "proposal_received": ["accept_proposal", "reject_proposal"],
117
+ "intent_accepted": ["complete_intent"],
118
+
119
+ # ── Specialization ──
120
+ "specialization_path": ["record_gap", "update_proficiency", "search_skills", "install_skill"],
121
+
122
+ # ── Teaching ──
123
+ "teaching_proposed": ["accept_teaching", "reject_teaching"],
124
+ "teaching_accepted": ["deliver_teaching"],
125
+ "teaching_delivered": ["approve_teaching", "reject_teaching"],
126
+ "teaching_opportunity": ["propose_teaching", "search_teachers"],
127
+
128
+ # ── Credit Agreements ──
129
+ "credit_agreement_created": ["accept_credit_agreement", "cancel_credit_agreement"],
130
+ "credit_work_delivered": ["complete_credit_agreement", "cancel_credit_agreement"],
131
+ "credit_agreement_accepted": ["deliver_credit_work", "cancel_credit_agreement"],
132
+
133
+ # ── Knowledge ──
134
+ "new_bundle_in_domain": ["cite_insight"],
135
+ "bundle_cited": [],
136
+
137
+ # ── Webhooks ──
138
+ "webhook_received": ["egress_request", "execute_tool"],
139
+
140
+ # ── Onboarding ──
141
+ "welcome_guide": ["create_post"],
142
+ "onboarding_suggestion": [],
143
+ }
144
+
145
+
146
+ def get_available_actions_from_map(
147
+ signal_type: str,
148
+ loaded_categories: set[str],
149
+ ) -> list[str]:
150
+ """Derive the full list of available actions for a given signal type.
151
+
152
+ Combines:
153
+ 1. CORE_ACTIONS (always available, ~30)
154
+ 2. SIGNAL_CONTEXT_ACTIONS for the signal type (contextual, 0-6)
155
+ 3. All actions from loaded categories (dynamically loaded via browse_tools)
156
+
157
+ Args:
158
+ signal_type: The signal type (e.g. "directive", "bounty_claimed")
159
+ loaded_categories: Set of category names loaded via browse_tools
160
+
161
+ Returns:
162
+ Deduplicated list of action names
163
+ """
164
+ actions: set[str] = set(CORE_ACTIONS)
165
+
166
+ # Add signal-specific context actions
167
+ context_actions = SIGNAL_CONTEXT_ACTIONS.get(signal_type)
168
+ if context_actions is not None:
169
+ actions.update(context_actions)
170
+ # Unknown signal type — no extra context (core is sufficient)
171
+
172
+ # Add all actions from loaded categories
173
+ if loaded_categories:
174
+ for name, info in ACTION_CATALOG.items():
175
+ cat = info.get("category")
176
+ if cat and cat in loaded_categories:
177
+ actions.add(name)
178
+
179
+ return list(actions)
180
+
181
+
182
+ # ── Category Helpers ──
183
+
184
+ def get_category_listing() -> list[dict[str, int | str]]:
185
+ """List all tool categories with their tool counts.
186
+
187
+ Returns:
188
+ List of dicts with 'name' and 'count' keys, sorted by count descending.
189
+ """
190
+ counts: dict[str, int] = {}
191
+ for info in ACTION_CATALOG.values():
192
+ cat = info.get("category")
193
+ if cat:
194
+ counts[cat] = counts.get(cat, 0) + 1
195
+ return sorted(
196
+ [{"name": name, "count": count} for name, count in counts.items()],
197
+ key=lambda x: x["count"],
198
+ reverse=True,
199
+ )
200
+
201
+
202
+ def get_tools_in_category(category: str) -> list[str]:
203
+ """Get all action names in a specific category.
204
+
205
+ Args:
206
+ category: Category name (e.g. "projects", "bounties")
207
+
208
+ Returns:
209
+ List of action names in that category.
210
+ """
211
+ return [
212
+ name
213
+ for name, info in ACTION_CATALOG.items()
214
+ if info.get("category") == category
215
+ ]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nookplot-runtime"
7
- version = "0.5.66"
7
+ version = "0.5.67"
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"
@@ -0,0 +1,187 @@
1
+ """Tests for get_available_actions() — verifies CORE_ACTIONS + signal context behaviour."""
2
+ from __future__ import annotations
3
+
4
+ from nookplot_runtime.autonomous import get_available_actions
5
+ from nookplot_runtime.signal_action_map import (
6
+ CORE_ACTIONS,
7
+ SIGNAL_CONTEXT_ACTIONS,
8
+ )
9
+
10
+
11
+ class TestGetAvailableActions:
12
+ # ── Core always present ──
13
+
14
+ def test_always_includes_core_actions(self):
15
+ signals = [
16
+ "directive", "collab_request", "agreement_created",
17
+ "bounty_claimed", "guild_opportunity", "totally_unknown_signal",
18
+ ]
19
+ for sig in signals:
20
+ actions = get_available_actions(sig)
21
+ for core in CORE_ACTIONS:
22
+ assert core in actions, f"'{core}' missing for signal '{sig}'"
23
+
24
+ def test_always_includes_ignore_and_reply(self):
25
+ actions = get_available_actions("directive")
26
+ assert "ignore" in actions
27
+ assert "reply" in actions
28
+
29
+ def test_always_includes_browse_tools(self):
30
+ assert "browse_tools" in get_available_actions("directive")
31
+ assert "browse_tools" in get_available_actions("bounty_claimed")
32
+ assert "browse_tools" in get_available_actions("unknown_signal")
33
+
34
+ def test_building_tools_always_available(self):
35
+ building = ["create_project", "commit_files", "create_bundle",
36
+ "list_project_files", "read_project_file"]
37
+ for tool in building:
38
+ assert tool in CORE_ACTIONS
39
+ assert tool in get_available_actions("directive")
40
+ assert tool in get_available_actions("unknown_signal")
41
+
42
+ # ── Directive ──
43
+
44
+ def test_directive_returns_only_core(self):
45
+ """directive has empty context — browse_tools handles expansion."""
46
+ actions = get_available_actions("directive")
47
+ assert len(actions) == len(CORE_ACTIONS)
48
+ assert set(actions) == set(CORE_ACTIONS)
49
+
50
+ # ── Signal context additions ──
51
+
52
+ def test_agreement_created(self):
53
+ actions = get_available_actions("agreement_created")
54
+ assert "deliver_work" in actions
55
+ assert "cancel_agreement" in actions
56
+ assert "send_agreement_message" in actions
57
+
58
+ def test_work_delivered(self):
59
+ actions = get_available_actions("work_delivered")
60
+ assert "settle_agreement" in actions
61
+ assert "dispute_agreement" in actions
62
+ assert "send_agreement_message" in actions
63
+ assert "expire_delivered" in actions
64
+
65
+ def test_agreement_settled(self):
66
+ assert "submit_review" in get_available_actions("agreement_settled")
67
+
68
+ def test_revision_requested(self):
69
+ actions = get_available_actions("revision_requested")
70
+ assert "deliver_work" in actions
71
+ assert "send_agreement_message" in actions
72
+
73
+ def test_bounty_application_submitted(self):
74
+ actions = get_available_actions("bounty_application_submitted")
75
+ assert "approve_bounty_claimer" in actions
76
+ assert "reject_bounty_application" in actions
77
+
78
+ def test_bounty_claimed(self):
79
+ actions = get_available_actions("bounty_claimed")
80
+ assert "approve_bounty_work" in actions
81
+ assert "approve_bounty_claimer" in actions
82
+ assert "dispute_bounty_work" in actions
83
+ assert "unclaim_bounty" in actions
84
+
85
+ def test_team_invitation(self):
86
+ actions = get_available_actions("team_invitation")
87
+ assert "accept_invitation" in actions
88
+ assert "decline_invitation" in actions
89
+
90
+ def test_guild_opportunity(self):
91
+ actions = get_available_actions("guild_opportunity")
92
+ assert "join_guild" in actions
93
+ assert "approve_guild" in actions
94
+ assert "reject_guild" in actions
95
+ assert "leave_guild" in actions
96
+ assert "propose_guild" in actions
97
+ assert "link_project_to_guild" in actions
98
+
99
+ def test_webhook_received(self):
100
+ actions = get_available_actions("webhook_received")
101
+ assert "egress_request" in actions
102
+ assert "execute_tool" in actions
103
+
104
+ def test_collab_request(self):
105
+ actions = get_available_actions("collab_request")
106
+ assert "add_collaborator" in actions
107
+ assert "propose_collab" in actions
108
+
109
+ def test_time_to_post(self):
110
+ actions = get_available_actions("time_to_post")
111
+ assert "create_post" in actions
112
+ assert "create_bounty" in actions
113
+ assert "create_listing" in actions
114
+ assert "publish_skill" in actions
115
+ # publish_insight is in CORE, should also be present
116
+ assert "publish_insight" in actions
117
+
118
+ def test_intent_matched(self):
119
+ actions = get_available_actions("intent_matched")
120
+ assert "submit_proposal" in actions
121
+ assert "browse_intents" in actions
122
+
123
+ def test_teaching_signals(self):
124
+ assert "accept_teaching" in get_available_actions("teaching_proposed")
125
+ assert "reject_teaching" in get_available_actions("teaching_proposed")
126
+ assert "deliver_teaching" in get_available_actions("teaching_accepted")
127
+ assert "approve_teaching" in get_available_actions("teaching_delivered")
128
+ assert "propose_teaching" in get_available_actions("teaching_opportunity")
129
+ assert "search_teachers" in get_available_actions("teaching_opportunity")
130
+
131
+ def test_specialization_path(self):
132
+ actions = get_available_actions("specialization_path")
133
+ assert "record_gap" in actions
134
+ assert "update_proficiency" in actions
135
+ assert "search_skills" in actions
136
+ assert "install_skill" in actions
137
+
138
+ def test_credit_agreement_signals(self):
139
+ assert "accept_credit_agreement" in get_available_actions("credit_agreement_created")
140
+ assert "deliver_credit_work" in get_available_actions("credit_agreement_accepted")
141
+ assert "complete_credit_agreement" in get_available_actions("credit_work_delivered")
142
+
143
+ # ── Structural properties ──
144
+
145
+ def test_unknown_signal_returns_only_core(self):
146
+ actions = get_available_actions("totally_unknown_signal")
147
+ assert len(actions) == len(CORE_ACTIONS)
148
+ assert set(actions) == set(CORE_ACTIONS)
149
+
150
+ def test_context_actions_are_additive(self):
151
+ """Signals with context always have >= CORE_ACTIONS."""
152
+ for signal, ctx in SIGNAL_CONTEXT_ACTIONS.items():
153
+ if ctx:
154
+ actions = get_available_actions(signal)
155
+ assert len(actions) >= len(CORE_ACTIONS)
156
+
157
+ def test_signal_with_context_has_correct_count(self):
158
+ """Actions = CORE + context (deduped)."""
159
+ for signal, ctx in SIGNAL_CONTEXT_ACTIONS.items():
160
+ if ctx:
161
+ actions = get_available_actions(signal)
162
+ expected = set(CORE_ACTIONS) | set(ctx)
163
+ assert len(actions) == len(expected), f"Mismatch for '{signal}'"
164
+
165
+ def test_actions_are_deduplicated(self):
166
+ for signal in ["directive", "agreement_created", "bounty_claimed", "guild_opportunity"]:
167
+ actions = get_available_actions(signal)
168
+ assert len(set(actions)) == len(actions), f"Duplicates in '{signal}'"
169
+
170
+ def test_all_lists_non_empty(self):
171
+ signals = [
172
+ "directive", "collab_request", "agreement_created",
173
+ "agreement_cancelled", "review_received",
174
+ "bounty_application_rejected", "totally_unknown_signal",
175
+ ]
176
+ for sig in signals:
177
+ assert len(get_available_actions(sig)) > 0
178
+
179
+ def test_loaded_categories_expand_actions(self):
180
+ base = get_available_actions("directive")
181
+ expanded = get_available_actions("directive", {"projects"})
182
+ assert len(expanded) >= len(base)
183
+
184
+ def test_all_signal_context_types_handled(self):
185
+ for signal in SIGNAL_CONTEXT_ACTIONS:
186
+ actions = get_available_actions(signal)
187
+ assert len(actions) >= len(CORE_ACTIONS)
@@ -1,110 +0,0 @@
1
- """Tests for get_available_actions() — verifies contextual action lists per signal type."""
2
- from __future__ import annotations
3
-
4
- from nookplot_runtime.autonomous import get_available_actions
5
-
6
-
7
- class TestGetAvailableActions:
8
- def test_dm_received_rich_action_set(self):
9
- actions = get_available_actions("dm_received")
10
- assert "reply" in actions
11
- assert "create_project" in actions
12
- assert "link_project_to_guild" in actions
13
- assert "propose_guild" in actions
14
- assert "create_bounty" in actions
15
- assert "execute_tool" in actions
16
- assert len(actions) > 15
17
- assert actions[-1] == "ignore"
18
-
19
- def test_channel_message_expanded(self):
20
- actions = get_available_actions("channel_message")
21
- assert "reply" in actions
22
- assert "publish" in actions
23
- assert "create_project" in actions
24
- assert "link_project_to_guild" in actions
25
- assert len(actions) > 10
26
-
27
- def test_new_follower(self):
28
- actions = get_available_actions("new_follower")
29
- assert "follow_back" in actions
30
- assert "send_dm" in actions
31
-
32
- def test_attestation_received(self):
33
- actions = get_available_actions("attestation_received")
34
- assert "attest_back" in actions
35
-
36
- def test_files_committed(self):
37
- actions = get_available_actions("files_committed")
38
- assert "review" in actions
39
- assert "request_ai_review" in actions
40
-
41
- def test_bounty(self):
42
- actions = get_available_actions("bounty")
43
- assert "claim" in actions
44
- assert "apply_bounty" in actions
45
-
46
- def test_directive_has_many_actions(self):
47
- actions = get_available_actions("directive")
48
- assert len(actions) >= 40
49
- assert "execute" in actions
50
- assert "create_project" in actions
51
- assert "egress_request" in actions
52
- assert "report_clawnch_launch" in actions
53
- assert "query_oracle" in actions
54
- assert actions[-1] == "ignore"
55
-
56
- def test_unknown_type_returns_fallback(self):
57
- actions = get_available_actions("totally_unknown_signal")
58
- assert actions == ["reply", "ignore"]
59
-
60
- def test_agreement_created(self):
61
- actions = get_available_actions("agreement_created")
62
- assert "deliver_work" in actions
63
- assert "cancel_agreement" in actions
64
-
65
- def test_work_delivered(self):
66
- actions = get_available_actions("work_delivered")
67
- assert "settle_agreement" in actions
68
- assert "dispute_agreement" in actions
69
-
70
- def test_time_to_post(self):
71
- actions = get_available_actions("time_to_post")
72
- assert "create_post" in actions
73
- assert "publish_insight" in actions
74
-
75
- def test_bounty_application_submitted(self):
76
- actions = get_available_actions("bounty_application_submitted")
77
- assert "approve_bounty_claimer" in actions
78
- assert "reject_bounty_application" in actions
79
-
80
- def test_bounty_claimed(self):
81
- actions = get_available_actions("bounty_claimed")
82
- assert "approve_bounty_work" in actions
83
- assert "dispute_bounty_work" in actions
84
-
85
- def test_guild_opportunity(self):
86
- actions = get_available_actions("guild_opportunity")
87
- assert "join_guild" in actions
88
- assert "propose_guild" in actions
89
-
90
- def test_intent_matched(self):
91
- actions = get_available_actions("intent_matched")
92
- assert "submit_proposal" in actions
93
- assert "browse_intents" in actions
94
-
95
- def test_all_lists_end_with_ignore(self):
96
- """Every signal type's action list should end with 'ignore'."""
97
- signal_types = [
98
- "dm_received", "channel_message", "new_follower",
99
- "files_committed", "bounty", "directive",
100
- "agreement_created", "work_delivered",
101
- "time_to_post", "guild_opportunity",
102
- ]
103
- for sig in signal_types:
104
- actions = get_available_actions(sig)
105
- assert actions[-1] == "ignore", f"{sig} does not end with 'ignore'"
106
-
107
- def test_webhook_received(self):
108
- actions = get_available_actions("webhook_received")
109
- assert "egress_request" in actions
110
- assert "execute_tool" in actions