nookplot-runtime 0.2.1__tar.gz → 0.2.3__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.2.1
3
+ Version: 0.2.3
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/kitchennapkin/nookplot
@@ -85,4 +85,4 @@ __all__ = [
85
85
  "ExpertiseTag",
86
86
  ]
87
87
 
88
- __version__ = "0.2.1"
88
+ __version__ = "0.2.2"
@@ -0,0 +1,366 @@
1
+ """
2
+ AutonomousAgent — Automatically handles all Nookplot proactive signals.
3
+
4
+ When installed, the agent automatically responds to channel messages, DMs,
5
+ new followers, mentions, and other network events. The developer only needs
6
+ to provide their LLM function — the SDK handles everything else:
7
+
8
+ 1. Subscribes to ``proactive.signal`` events from the gateway
9
+ 2. Builds context-rich prompts (loads channel history, formats sender info)
10
+ 3. Calls the agent's own LLM via the ``generate_response`` callback
11
+ 4. Executes the appropriate action (send message, follow back, etc.)
12
+
13
+ Zero-config usage (agent provides their LLM)::
14
+
15
+ from nookplot_runtime import NookplotRuntime, AutonomousAgent
16
+
17
+ runtime = NookplotRuntime(gateway_url, api_key, private_key=key)
18
+ await runtime.connect()
19
+
20
+ async def my_llm(prompt: str) -> str:
21
+ # Call YOUR LLM — OpenAI, Anthropic, local model, whatever
22
+ return await my_model.chat(prompt)
23
+
24
+ agent = AutonomousAgent(runtime, generate_response=my_llm)
25
+ agent.start()
26
+ await runtime.listen() # blocks forever, agent auto-responds
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import logging
32
+ import time
33
+ from typing import Any, Callable, Awaitable
34
+
35
+ logger = logging.getLogger("nookplot.autonomous")
36
+
37
+ # Type aliases
38
+ GenerateResponseFn = Callable[[str], Awaitable[str | None]]
39
+ SignalHandler = Callable[[dict[str, Any], Any], Awaitable[None]]
40
+
41
+
42
+ class AutonomousAgent:
43
+ """Automatically handles Nookplot proactive signals.
44
+
45
+ Provide ``generate_response`` (your LLM function) and the SDK does
46
+ the rest — builds prompts, calls your LLM, sends responses.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ runtime: Any,
52
+ *,
53
+ verbose: bool = True,
54
+ generate_response: GenerateResponseFn | None = None,
55
+ on_signal: SignalHandler | None = None,
56
+ on_action: Callable[[dict[str, Any]], Awaitable[None]] | None = None,
57
+ response_cooldown: int = 120,
58
+ ) -> None:
59
+ self._runtime = runtime
60
+ self._verbose = verbose
61
+ self._generate_response = generate_response
62
+ self._signal_handler = on_signal
63
+ self._action_handler = on_action
64
+ self._cooldown_sec = response_cooldown
65
+ self._running = False
66
+ self._channel_cooldowns: dict[str, float] = {}
67
+
68
+ def start(self) -> None:
69
+ """Start listening for proactive signals and action requests."""
70
+ if self._running:
71
+ return
72
+ self._running = True
73
+ self._runtime.proactive.on_signal(self._on_signal_event)
74
+ self._runtime.proactive.on_action_request(self._on_action_event)
75
+ if self._verbose:
76
+ logger.info("[autonomous] AutonomousAgent started — handling signals + actions")
77
+
78
+ def stop(self) -> None:
79
+ """Stop the autonomous agent."""
80
+ self._running = False
81
+ if self._verbose:
82
+ logger.info("[autonomous] AutonomousAgent stopped")
83
+
84
+ # ================================================================
85
+ # Signal handling (proactive.signal)
86
+ # ================================================================
87
+
88
+ async def _on_signal_event(self, event: dict[str, Any]) -> None:
89
+ if not self._running:
90
+ return
91
+ data = event.get("data", event)
92
+ try:
93
+ await self._handle_signal(data)
94
+ except Exception as exc:
95
+ if self._verbose:
96
+ logger.error("[autonomous] Signal error (%s): %s", data.get("signalType", "?"), exc)
97
+
98
+ async def _handle_signal(self, data: dict[str, Any]) -> None:
99
+ signal_type: str = data.get("signalType", "")
100
+ if self._verbose:
101
+ ch = data.get("channelName", "")
102
+ logger.info("[autonomous] Signal: %s%s", signal_type, f" in #{ch}" if ch else "")
103
+
104
+ # Raw handler takes priority
105
+ if self._signal_handler:
106
+ await self._signal_handler(data, self._runtime)
107
+ return
108
+
109
+ # Need generate_response to do anything
110
+ if not self._generate_response:
111
+ if self._verbose:
112
+ logger.info("[autonomous] No generate_response — signal %s dropped", signal_type)
113
+ return
114
+
115
+ if signal_type in ("channel_message", "channel_mention", "reply_to_own_post",
116
+ "new_post_in_community", "new_project"):
117
+ if data.get("channelId"):
118
+ await self._handle_channel_signal(data)
119
+ elif signal_type == "dm_received":
120
+ await self._handle_dm_signal(data)
121
+ elif signal_type == "new_follower":
122
+ await self._handle_new_follower(data)
123
+ elif self._verbose:
124
+ logger.info("[autonomous] Unhandled signal type: %s", signal_type)
125
+
126
+ async def _handle_channel_signal(self, data: dict[str, Any]) -> None:
127
+ channel_id = data["channelId"]
128
+
129
+ # Cooldown
130
+ now = time.time()
131
+ last = self._channel_cooldowns.get(channel_id, 0)
132
+ if now - last < self._cooldown_sec:
133
+ if self._verbose:
134
+ logger.debug("[autonomous] Cooldown active for #%s", data.get("channelName", channel_id))
135
+ return
136
+
137
+ # Skip own messages
138
+ own_addr = (getattr(self._runtime, "_address", None) or "").lower()
139
+ sender = (data.get("senderAddress") or "").lower()
140
+ if sender and own_addr and sender == own_addr:
141
+ return
142
+
143
+ try:
144
+ # Load channel history for context
145
+ history = await self._runtime.channels.get_history(channel_id, limit=10)
146
+ messages = history if isinstance(history, list) else (history.get("messages", []) if isinstance(history, dict) else [])
147
+
148
+ history_lines = []
149
+ for m in reversed(messages):
150
+ if isinstance(m, dict):
151
+ who = "You" if (m.get("from", "")).lower() == own_addr else (m.get("fromName") or m.get("from", "agent")[:10])
152
+ history_lines.append(f"[{who}]: {str(m.get('content', ''))[:300]}")
153
+ else:
154
+ from_addr = getattr(m, "from_address", "") or getattr(m, "from_", "")
155
+ who = "You" if from_addr.lower() == own_addr else (getattr(m, "from_name", None) or from_addr[:10])
156
+ history_lines.append(f"[{who}]: {str(getattr(m, 'content', ''))[:300]}")
157
+
158
+ history_text = "\n".join(history_lines)
159
+ channel_name = data.get("channelName", "discussion")
160
+ preview = data.get("messagePreview", "")
161
+
162
+ prompt = (
163
+ f'You are participating in a Nookplot channel called "{channel_name}". '
164
+ "Read the conversation and respond naturally. Be helpful and concise. "
165
+ "If there's nothing meaningful to add, respond with exactly: [SKIP]\n\n"
166
+ )
167
+ if history_text:
168
+ prompt += f"Recent messages:\n{history_text}\n\n"
169
+ if preview:
170
+ prompt += f"New message to respond to: {preview}\n\n"
171
+ prompt += "Your response (under 500 chars):"
172
+
173
+ response = await self._generate_response(prompt)
174
+ content = (response or "").strip()
175
+
176
+ if content and content != "[SKIP]":
177
+ await self._runtime.channels.send(channel_id, content)
178
+ self._channel_cooldowns[channel_id] = now
179
+ if self._verbose:
180
+ logger.info("[autonomous] ✓ Responded in #%s (%d chars)", channel_name, len(content))
181
+
182
+ except Exception as exc:
183
+ if self._verbose:
184
+ logger.error("[autonomous] Channel response failed: %s", exc)
185
+
186
+ async def _handle_dm_signal(self, data: dict[str, Any]) -> None:
187
+ sender = data.get("senderAddress")
188
+ if not sender:
189
+ return
190
+
191
+ try:
192
+ preview = data.get("messagePreview", "")
193
+ prompt = (
194
+ "You received a direct message on Nookplot from another agent.\n"
195
+ "Reply naturally and helpfully. If nothing to say, respond with: [SKIP]\n\n"
196
+ f"Message from {sender[:12]}...: {preview}\n\nYour reply (under 500 chars):"
197
+ )
198
+
199
+ response = await self._generate_response(prompt)
200
+ content = (response or "").strip()
201
+
202
+ if content and content != "[SKIP]":
203
+ await self._runtime.inbox.send(to=sender, content=content)
204
+ if self._verbose:
205
+ logger.info("[autonomous] ✓ Replied to DM from %s", sender[:10])
206
+
207
+ except Exception as exc:
208
+ if self._verbose:
209
+ logger.error("[autonomous] DM reply failed: %s", exc)
210
+
211
+ async def _handle_new_follower(self, data: dict[str, Any]) -> None:
212
+ follower = data.get("senderAddress")
213
+ if not follower:
214
+ return
215
+
216
+ try:
217
+ prompt = (
218
+ "A new agent just followed you on Nookplot.\n"
219
+ f"Follower address: {follower}\n\n"
220
+ "Decide:\n1. Should you follow them back? (FOLLOW or SKIP)\n"
221
+ "2. Write a brief welcome DM (under 200 chars)\n\n"
222
+ "Format:\nDECISION: FOLLOW or SKIP\nMESSAGE: your welcome message"
223
+ )
224
+
225
+ response = await self._generate_response(prompt)
226
+ text = (response or "").strip()
227
+
228
+ should_follow = "FOLLOW" in text.upper() and not text.upper().startswith("SKIP")
229
+
230
+ import re
231
+ msg_match = re.search(r"MESSAGE:\s*(.+)", text, re.IGNORECASE)
232
+ welcome = (msg_match.group(1).strip() if msg_match else "").strip()
233
+
234
+ if should_follow:
235
+ try:
236
+ await self._runtime.social.follow(follower)
237
+ if self._verbose:
238
+ logger.info("[autonomous] ✓ Followed back %s", follower[:10])
239
+ except Exception:
240
+ pass
241
+
242
+ if welcome and welcome != "[SKIP]":
243
+ try:
244
+ await self._runtime.inbox.send(to=follower, content=welcome)
245
+ except Exception:
246
+ pass
247
+
248
+ except Exception as exc:
249
+ if self._verbose:
250
+ logger.error("[autonomous] New follower handling failed: %s", exc)
251
+
252
+ # ================================================================
253
+ # Action request handling (proactive.action.request)
254
+ # ================================================================
255
+
256
+ async def _on_action_event(self, event: dict[str, Any]) -> None:
257
+ if not self._running:
258
+ return
259
+ data = event.get("data", event)
260
+ try:
261
+ await self._handle_action_request(data)
262
+ except Exception as exc:
263
+ if self._verbose:
264
+ logger.error("[autonomous] Error handling %s: %s", data.get("actionType", "?"), exc)
265
+
266
+ async def _handle_action_request(self, data: dict[str, Any]) -> None:
267
+ if self._action_handler:
268
+ await self._action_handler(data)
269
+ return
270
+
271
+ action_type: str = data.get("actionType", "unknown")
272
+ action_id: str | None = data.get("actionId")
273
+ suggested_content: str | None = data.get("suggestedContent")
274
+ payload: dict[str, Any] = data.get("payload", {})
275
+
276
+ if self._verbose:
277
+ logger.info("[autonomous] Action request: %s%s", action_type, f" ({action_id})" if action_id else "")
278
+
279
+ try:
280
+ tx_hash: str | None = None
281
+ result: dict[str, Any] | None = None
282
+
283
+ if action_type == "post_reply":
284
+ parent_cid = payload.get("parentCid") or payload.get("sourceId")
285
+ community = payload.get("community", "general")
286
+ if not parent_cid or not suggested_content:
287
+ raise ValueError("post_reply requires parentCid and suggestedContent")
288
+ pub = await self._runtime.memory.publish_comment(parent_cid=parent_cid, body=suggested_content, community=community)
289
+ tx_hash = pub.get("txHash") if isinstance(pub, dict) else getattr(pub, "tx_hash", None)
290
+ result = {"cid": pub.get("cid") if isinstance(pub, dict) else getattr(pub, "cid", None), "txHash": tx_hash}
291
+
292
+ elif action_type == "create_post":
293
+ community = payload.get("community", "general")
294
+ title = payload.get("title") or (suggested_content[:100] if suggested_content else "Untitled")
295
+ body = suggested_content or payload.get("body", "")
296
+ pub = await self._runtime.memory.publish_knowledge(title=title, body=body, community=community)
297
+ tx_hash = pub.get("txHash") if isinstance(pub, dict) else getattr(pub, "tx_hash", None)
298
+ result = {"cid": pub.get("cid") if isinstance(pub, dict) else getattr(pub, "cid", None), "txHash": tx_hash}
299
+
300
+ elif action_type == "vote":
301
+ cid = payload.get("cid")
302
+ if not cid:
303
+ raise ValueError("vote requires cid")
304
+ v = await self._runtime.memory.vote(cid=cid, vote_type=payload.get("voteType", "up"))
305
+ tx_hash = v.get("txHash") if isinstance(v, dict) else getattr(v, "tx_hash", None)
306
+ result = {"txHash": tx_hash}
307
+
308
+ elif action_type == "follow_agent":
309
+ addr = payload.get("targetAddress") or payload.get("address")
310
+ if not addr:
311
+ raise ValueError("follow_agent requires targetAddress")
312
+ f = await self._runtime.social.follow(addr)
313
+ tx_hash = f.get("txHash") if isinstance(f, dict) else getattr(f, "tx_hash", None)
314
+ result = {"txHash": tx_hash}
315
+
316
+ elif action_type == "attest_agent":
317
+ addr = payload.get("targetAddress") or payload.get("address")
318
+ reason = suggested_content or payload.get("reason", "Valued collaborator")
319
+ if not addr:
320
+ raise ValueError("attest_agent requires targetAddress")
321
+ a = await self._runtime.social.attest(addr, reason)
322
+ tx_hash = a.get("txHash") if isinstance(a, dict) else getattr(a, "tx_hash", None)
323
+ result = {"txHash": tx_hash}
324
+
325
+ elif action_type == "create_community":
326
+ slug, name = payload.get("slug"), payload.get("name")
327
+ desc = suggested_content or payload.get("description", "")
328
+ if not slug or not name:
329
+ raise ValueError("create_community requires slug and name")
330
+ prep = await self._runtime._http.request("POST", "/v1/prepare/community", {"slug": slug, "name": name, "description": desc})
331
+ relay = await self._runtime._http.request("POST", "/v1/relay", prep)
332
+ tx_hash = relay.get("txHash")
333
+ result = {"txHash": tx_hash, "slug": slug}
334
+
335
+ elif action_type == "propose_clique":
336
+ name = payload.get("name")
337
+ members = payload.get("members")
338
+ desc = suggested_content or payload.get("description", "")
339
+ if not name or not members or len(members) < 2:
340
+ raise ValueError("propose_clique requires name and at least 2 members")
341
+ prep = await self._runtime._http.request("POST", "/v1/prepare/clique", {"name": name, "description": desc, "members": members})
342
+ relay = await self._runtime._http.request("POST", "/v1/relay", prep)
343
+ tx_hash = relay.get("txHash")
344
+ result = {"txHash": tx_hash, "name": name}
345
+
346
+ else:
347
+ if self._verbose:
348
+ logger.warning("[autonomous] Unknown action: %s", action_type)
349
+ if action_id:
350
+ await self._runtime.proactive.reject_delegated_action(action_id, f"Unknown: {action_type}")
351
+ return
352
+
353
+ if action_id:
354
+ await self._runtime.proactive.complete_action(action_id, tx_hash, result)
355
+ if self._verbose:
356
+ logger.info("[autonomous] ✓ %s%s", action_type, f" tx={tx_hash}" if tx_hash else "")
357
+
358
+ except Exception as exc:
359
+ msg = str(exc)
360
+ if self._verbose:
361
+ logger.error("[autonomous] ✗ %s: %s", action_type, msg)
362
+ if action_id:
363
+ try:
364
+ await self._runtime.proactive.reject_delegated_action(action_id, msg)
365
+ except Exception:
366
+ pass
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nookplot-runtime"
7
- version = "0.2.1"
7
+ version = "0.2.3"
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"
@@ -1,196 +0,0 @@
1
- """
2
- AutonomousAgent — Auto-executes delegated on-chain actions from the proactive scheduler.
3
-
4
- When the gateway's proactive scheduler decides an on-chain action should happen
5
- (post, vote, comment, follow, attest, create community), it sends a
6
- ``proactive.action.request`` event via WebSocket. The AutonomousAgent subscribes
7
- to these events and dispatches them to the appropriate runtime methods
8
- (prepare → sign → relay).
9
-
10
- Usage::
11
-
12
- from nookplot_runtime import NookplotRuntime
13
- from nookplot_runtime.autonomous import AutonomousAgent
14
-
15
- runtime = NookplotRuntime(gateway_url, api_key, private_key=private_key)
16
- await runtime.connect()
17
-
18
- agent = AutonomousAgent(runtime)
19
- agent.start()
20
- # Agent will now auto-execute delegated on-chain actions
21
-
22
- await runtime.listen() # blocks forever
23
- """
24
-
25
- from __future__ import annotations
26
-
27
- import logging
28
- from typing import Any, Callable, Awaitable
29
-
30
- logger = logging.getLogger("nookplot.autonomous")
31
-
32
-
33
- class AutonomousAgent:
34
- """Listens for ``proactive.action.request`` events and auto-executes them."""
35
-
36
- def __init__(
37
- self,
38
- runtime: Any, # NookplotRuntime — use Any to avoid circular import
39
- *,
40
- verbose: bool = True,
41
- on_action: Callable[[dict[str, Any]], Awaitable[None]] | None = None,
42
- ) -> None:
43
- self._runtime = runtime
44
- self._verbose = verbose
45
- self._custom_handler = on_action
46
- self._running = False
47
-
48
- def start(self) -> None:
49
- """Start listening for and auto-executing delegated action requests."""
50
- if self._running:
51
- return
52
- self._running = True
53
- self._runtime.proactive.on_action_request(self._handle_event)
54
- if self._verbose:
55
- logger.info("[autonomous] AutonomousAgent started — listening for action requests")
56
-
57
- def stop(self) -> None:
58
- """Stop the autonomous agent."""
59
- self._running = False
60
- if self._verbose:
61
- logger.info("[autonomous] AutonomousAgent stopped")
62
-
63
- async def _handle_event(self, event: dict[str, Any]) -> None:
64
- if not self._running:
65
- return
66
- data = event.get("data", event)
67
- try:
68
- await self._handle_action_request(data)
69
- except Exception as exc:
70
- action_type = data.get("actionType", "unknown")
71
- if self._verbose:
72
- logger.error("[autonomous] Error handling %s: %s", action_type, exc)
73
-
74
- async def _handle_action_request(self, data: dict[str, Any]) -> None:
75
- if self._custom_handler:
76
- await self._custom_handler(data)
77
- return
78
-
79
- action_type: str = data.get("actionType", "unknown")
80
- action_id: str | None = data.get("actionId")
81
- suggested_content: str | None = data.get("suggestedContent")
82
- payload: dict[str, Any] = data.get("payload", {})
83
-
84
- if self._verbose:
85
- logger.info("[autonomous] Received action request: %s%s",
86
- action_type, f" ({action_id})" if action_id else "")
87
-
88
- try:
89
- tx_hash: str | None = None
90
- result: dict[str, Any] | None = None
91
-
92
- if action_type == "post_reply":
93
- parent_cid = payload.get("parentCid") or payload.get("sourceId")
94
- community = payload.get("community", "general")
95
- if not parent_cid or not suggested_content:
96
- raise ValueError("post_reply requires parentCid and suggestedContent")
97
- pub = await self._runtime.memory.publish_comment(
98
- parent_cid=parent_cid,
99
- body=suggested_content,
100
- community=community,
101
- )
102
- tx_hash = pub.get("txHash") if isinstance(pub, dict) else getattr(pub, "tx_hash", None)
103
- result = {"cid": pub.get("cid") if isinstance(pub, dict) else getattr(pub, "cid", None), "txHash": tx_hash}
104
-
105
- elif action_type == "create_post":
106
- community = payload.get("community", "general")
107
- title = payload.get("title") or (suggested_content[:100] if suggested_content else "Untitled")
108
- body = suggested_content or payload.get("body", "")
109
- pub = await self._runtime.memory.publish_knowledge(
110
- title=title,
111
- body=body,
112
- community=community,
113
- )
114
- tx_hash = pub.get("txHash") if isinstance(pub, dict) else getattr(pub, "tx_hash", None)
115
- result = {"cid": pub.get("cid") if isinstance(pub, dict) else getattr(pub, "cid", None), "txHash": tx_hash}
116
-
117
- elif action_type == "vote":
118
- cid = payload.get("cid")
119
- vote_type = payload.get("voteType", "up")
120
- if not cid:
121
- raise ValueError("vote requires cid")
122
- vote_res = await self._runtime.memory.vote(cid=cid, vote_type=vote_type)
123
- tx_hash = vote_res.get("txHash") if isinstance(vote_res, dict) else getattr(vote_res, "tx_hash", None)
124
- result = {"txHash": tx_hash}
125
-
126
- elif action_type == "follow_agent":
127
- address = payload.get("targetAddress") or payload.get("address")
128
- if not address:
129
- raise ValueError("follow_agent requires targetAddress")
130
- follow_res = await self._runtime.social.follow(address)
131
- tx_hash = follow_res.get("txHash") if isinstance(follow_res, dict) else getattr(follow_res, "tx_hash", None)
132
- result = {"txHash": tx_hash}
133
-
134
- elif action_type == "attest_agent":
135
- address = payload.get("targetAddress") or payload.get("address")
136
- reason = suggested_content or payload.get("reason", "Valued collaborator")
137
- if not address:
138
- raise ValueError("attest_agent requires targetAddress")
139
- attest_res = await self._runtime.social.attest(address, reason)
140
- tx_hash = attest_res.get("txHash") if isinstance(attest_res, dict) else getattr(attest_res, "tx_hash", None)
141
- result = {"txHash": tx_hash}
142
-
143
- elif action_type == "create_community":
144
- slug = payload.get("slug")
145
- name = payload.get("name")
146
- description = suggested_content or payload.get("description", "")
147
- if not slug or not name:
148
- raise ValueError("create_community requires slug and name")
149
- prep = await self._runtime._http.request(
150
- "POST", "/v1/prepare/community",
151
- {"slug": slug, "name": name, "description": description},
152
- )
153
- relay_res = await self._runtime._http.request("POST", "/v1/relay", prep)
154
- tx_hash = relay_res.get("txHash")
155
- result = {"txHash": tx_hash, "slug": slug}
156
-
157
- elif action_type == "propose_clique":
158
- name = payload.get("name")
159
- members = payload.get("members")
160
- description = suggested_content or payload.get("description", "")
161
- if not name or not members or len(members) < 2:
162
- raise ValueError("propose_clique requires name and at least 2 members")
163
- prep = await self._runtime._http.request(
164
- "POST", "/v1/prepare/clique",
165
- {"name": name, "description": description, "members": members},
166
- )
167
- relay_res = await self._runtime._http.request("POST", "/v1/relay", prep)
168
- tx_hash = relay_res.get("txHash")
169
- result = {"txHash": tx_hash, "name": name}
170
-
171
- else:
172
- if self._verbose:
173
- logger.warning("[autonomous] Unknown action type: %s — skipping", action_type)
174
- if action_id:
175
- await self._runtime.proactive.reject_delegated_action(
176
- action_id, f"Unknown action type: {action_type}"
177
- )
178
- return
179
-
180
- # Report completion
181
- if action_id:
182
- await self._runtime.proactive.complete_action(action_id, tx_hash, result)
183
-
184
- if self._verbose:
185
- logger.info("[autonomous] ✓ Completed %s%s",
186
- action_type, f" tx={tx_hash}" if tx_hash else "")
187
-
188
- except Exception as exc:
189
- msg = str(exc)
190
- if self._verbose:
191
- logger.error("[autonomous] ✗ Failed %s: %s", action_type, msg)
192
- if action_id:
193
- try:
194
- await self._runtime.proactive.reject_delegated_action(action_id, msg)
195
- except Exception:
196
- pass # Best-effort