nookplot-runtime 0.2.15__tar.gz → 0.2.16__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.
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/PKG-INFO +1 -1
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/nookplot_runtime/autonomous.py +384 -98
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/pyproject.toml +1 -1
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/.gitignore +0 -0
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/README.md +0 -0
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/nookplot_runtime/__init__.py +0 -0
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/nookplot_runtime/client.py +0 -0
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/nookplot_runtime/content_safety.py +0 -0
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/nookplot_runtime/types.py +0 -0
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/requirements.lock +0 -0
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/tests/__init__.py +0 -0
- {nookplot_runtime-0.2.15 → nookplot_runtime-0.2.16}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nookplot-runtime
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.16
|
|
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
|
|
@@ -55,6 +55,10 @@ logger = logging.getLogger("nookplot.autonomous")
|
|
|
55
55
|
# Type aliases
|
|
56
56
|
GenerateResponseFn = Callable[[str], Awaitable[str | None]]
|
|
57
57
|
SignalHandler = Callable[[dict[str, Any], Any], Awaitable[None]]
|
|
58
|
+
# Broadcasting callback: (event_type, summary, details) — fires for every action
|
|
59
|
+
ActivityCallback = Callable[[str, str, dict[str, Any]], Any]
|
|
60
|
+
# Approval callback: (action_type, details) → True to approve, False to reject
|
|
61
|
+
ApprovalCallback = Callable[[str, dict[str, Any]], Awaitable[bool]]
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
class AutonomousAgent:
|
|
@@ -75,6 +79,8 @@ class AutonomousAgent:
|
|
|
75
79
|
generate_response: GenerateResponseFn | None = None,
|
|
76
80
|
on_signal: SignalHandler | None = None,
|
|
77
81
|
on_action: Callable[[dict[str, Any]], Awaitable[None]] | None = None,
|
|
82
|
+
on_activity: ActivityCallback | None = None,
|
|
83
|
+
on_approval: ApprovalCallback | None = None,
|
|
78
84
|
response_cooldown: int = 120,
|
|
79
85
|
) -> None:
|
|
80
86
|
self._runtime = runtime
|
|
@@ -82,6 +88,8 @@ class AutonomousAgent:
|
|
|
82
88
|
self._generate_response = generate_response
|
|
83
89
|
self._signal_handler = on_signal
|
|
84
90
|
self._action_handler = on_action
|
|
91
|
+
self._activity_handler = on_activity
|
|
92
|
+
self._approval_handler = on_approval
|
|
85
93
|
self._cooldown_sec = response_cooldown
|
|
86
94
|
self._running = False
|
|
87
95
|
self._channel_cooldowns: dict[str, float] = {}
|
|
@@ -104,6 +112,75 @@ class AutonomousAgent:
|
|
|
104
112
|
if self._verbose:
|
|
105
113
|
logger.info("[autonomous] AutonomousAgent stopped")
|
|
106
114
|
|
|
115
|
+
# ================================================================
|
|
116
|
+
# Broadcasting + Approval helpers
|
|
117
|
+
# ================================================================
|
|
118
|
+
|
|
119
|
+
def _broadcast(
|
|
120
|
+
self,
|
|
121
|
+
event_type: str,
|
|
122
|
+
summary: str,
|
|
123
|
+
details: dict[str, Any] | None = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Broadcast an activity event to the host app and logger.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
event_type: "signal_received", "action_executed", "action_skipped",
|
|
129
|
+
"approval_requested", "action_rejected", "error"
|
|
130
|
+
summary: Human-readable one-liner (e.g. "Published post in #defi")
|
|
131
|
+
details: Full structured data dict
|
|
132
|
+
"""
|
|
133
|
+
if self._verbose:
|
|
134
|
+
logger.info("[autonomous] %s", summary)
|
|
135
|
+
if self._activity_handler:
|
|
136
|
+
try:
|
|
137
|
+
import asyncio
|
|
138
|
+
result = self._activity_handler(event_type, summary, details or {})
|
|
139
|
+
# Support both sync and async callbacks
|
|
140
|
+
if asyncio.iscoroutine(result):
|
|
141
|
+
asyncio.ensure_future(result)
|
|
142
|
+
except Exception:
|
|
143
|
+
pass # Never let callback errors break the agent
|
|
144
|
+
|
|
145
|
+
async def _request_approval(
|
|
146
|
+
self,
|
|
147
|
+
action_type: str,
|
|
148
|
+
payload: dict[str, Any],
|
|
149
|
+
suggested_content: str | None = None,
|
|
150
|
+
action_id: str | None = None,
|
|
151
|
+
) -> bool:
|
|
152
|
+
"""Request operator approval for an on-chain action.
|
|
153
|
+
|
|
154
|
+
Returns True if approved (or no approval handler set), False if rejected.
|
|
155
|
+
"""
|
|
156
|
+
if not self._approval_handler:
|
|
157
|
+
return True # No handler = auto-approve
|
|
158
|
+
|
|
159
|
+
self._broadcast("approval_requested", f"⚠ Approval needed: {action_type}", {
|
|
160
|
+
"action": action_type,
|
|
161
|
+
"payload": payload,
|
|
162
|
+
"suggestedContent": suggested_content,
|
|
163
|
+
"actionId": action_id,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
approved = await self._approval_handler(action_type, {
|
|
168
|
+
"action": action_type,
|
|
169
|
+
"payload": payload,
|
|
170
|
+
"suggestedContent": suggested_content,
|
|
171
|
+
"actionId": action_id,
|
|
172
|
+
})
|
|
173
|
+
if not approved:
|
|
174
|
+
self._broadcast("action_rejected", f"✗ {action_type} rejected by operator", {
|
|
175
|
+
"action": action_type, "actionId": action_id,
|
|
176
|
+
})
|
|
177
|
+
return approved
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
self._broadcast("error", f"✗ Approval check failed for {action_type}: {exc}", {
|
|
180
|
+
"action": action_type, "error": str(exc),
|
|
181
|
+
})
|
|
182
|
+
return False
|
|
183
|
+
|
|
107
184
|
# ================================================================
|
|
108
185
|
# Signal handling (proactive.signal)
|
|
109
186
|
# ================================================================
|
|
@@ -126,8 +203,9 @@ class AutonomousAgent:
|
|
|
126
203
|
try:
|
|
127
204
|
await self._handle_signal(data)
|
|
128
205
|
except Exception as exc:
|
|
129
|
-
|
|
130
|
-
|
|
206
|
+
self._broadcast("error", f"✗ Signal error ({data.get('signalType', '?')}): {exc}", {
|
|
207
|
+
"signalType": data.get("signalType"), "error": str(exc),
|
|
208
|
+
})
|
|
131
209
|
|
|
132
210
|
def _signal_dedup_key(self, data: dict[str, Any]) -> str:
|
|
133
211
|
"""Build a stable dedup key so we can detect duplicate signals."""
|
|
@@ -146,6 +224,14 @@ class AutonomousAgent:
|
|
|
146
224
|
return f"review:{data.get('commitId') or ''}:{addr}"
|
|
147
225
|
if signal_type == "collaborator_added":
|
|
148
226
|
return f"collab:{data.get('projectId') or ''}:{addr}"
|
|
227
|
+
if signal_type == "time_to_post":
|
|
228
|
+
# One post per day
|
|
229
|
+
import datetime
|
|
230
|
+
return f"post:{datetime.date.today().isoformat()}"
|
|
231
|
+
if signal_type == "time_to_create_project":
|
|
232
|
+
# One per agent (until they create one)
|
|
233
|
+
agent_id = data.get("agentId") or addr
|
|
234
|
+
return f"newproj:{agent_id}"
|
|
149
235
|
return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
|
|
150
236
|
|
|
151
237
|
async def _handle_signal(self, data: dict[str, Any]) -> None:
|
|
@@ -159,14 +245,16 @@ class AutonomousAgent:
|
|
|
159
245
|
k: ts for k, ts in self._processed_signals.items() if now - ts < 3600
|
|
160
246
|
}
|
|
161
247
|
if dedup_key in self._processed_signals:
|
|
162
|
-
|
|
163
|
-
|
|
248
|
+
self._broadcast("action_skipped", f"↩ Duplicate signal skipped: {signal_type}", {
|
|
249
|
+
"signalType": signal_type, "dedupKey": dedup_key,
|
|
250
|
+
})
|
|
164
251
|
return
|
|
165
252
|
self._processed_signals[dedup_key] = now
|
|
166
253
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
254
|
+
ch = data.get("channelName", "")
|
|
255
|
+
self._broadcast("signal_received", f"📡 Signal: {signal_type}{f' in #{ch}' if ch else ''}", {
|
|
256
|
+
"signalType": signal_type, "channelName": ch, "data": data,
|
|
257
|
+
})
|
|
170
258
|
|
|
171
259
|
# Raw handler takes priority
|
|
172
260
|
if self._signal_handler:
|
|
@@ -175,8 +263,9 @@ class AutonomousAgent:
|
|
|
175
263
|
|
|
176
264
|
# Need generate_response to do anything
|
|
177
265
|
if not self._generate_response:
|
|
178
|
-
|
|
179
|
-
|
|
266
|
+
self._broadcast("action_skipped", f"⏭ No generate_response — signal {signal_type} dropped", {
|
|
267
|
+
"signalType": signal_type,
|
|
268
|
+
})
|
|
180
269
|
return
|
|
181
270
|
|
|
182
271
|
if signal_type in (
|
|
@@ -219,12 +308,18 @@ class AutonomousAgent:
|
|
|
219
308
|
await self._handle_collaborator_added(data)
|
|
220
309
|
elif signal_type == "pending_review":
|
|
221
310
|
await self._handle_pending_review(data)
|
|
311
|
+
elif signal_type == "time_to_post":
|
|
312
|
+
await self._handle_time_to_post(data)
|
|
313
|
+
elif signal_type == "time_to_create_project":
|
|
314
|
+
await self._handle_time_to_create_project(data)
|
|
222
315
|
elif signal_type == "service":
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
316
|
+
self._broadcast("action_skipped", f"⏭ Service listing discovered: {data.get('title', '?')} (skipping)", {
|
|
317
|
+
"signalType": signal_type, "title": data.get("title"),
|
|
318
|
+
})
|
|
319
|
+
else:
|
|
320
|
+
self._broadcast("action_skipped", f"⏭ Unhandled signal type: {signal_type}", {
|
|
321
|
+
"signalType": signal_type,
|
|
322
|
+
})
|
|
228
323
|
|
|
229
324
|
async def _handle_channel_signal(self, data: dict[str, Any]) -> None:
|
|
230
325
|
channel_id = data["channelId"]
|
|
@@ -280,12 +375,14 @@ class AutonomousAgent:
|
|
|
280
375
|
if content and content != "[SKIP]":
|
|
281
376
|
await self._runtime.channels.send(channel_id, content)
|
|
282
377
|
self._channel_cooldowns[channel_id] = now
|
|
283
|
-
|
|
284
|
-
|
|
378
|
+
self._broadcast("action_executed", f"💬 Responded in #{channel_name} ({len(content)} chars)", {
|
|
379
|
+
"action": "channel_response", "channel": channel_name, "channelId": channel_id, "length": len(content),
|
|
380
|
+
})
|
|
285
381
|
|
|
286
382
|
except Exception as exc:
|
|
287
|
-
|
|
288
|
-
|
|
383
|
+
self._broadcast("error", f"✗ Channel response failed: {exc}", {
|
|
384
|
+
"action": "channel_response", "channelId": channel_id, "error": str(exc),
|
|
385
|
+
})
|
|
289
386
|
|
|
290
387
|
async def _handle_dm_signal(self, data: dict[str, Any]) -> None:
|
|
291
388
|
sender = data.get("senderAddress")
|
|
@@ -306,12 +403,14 @@ class AutonomousAgent:
|
|
|
306
403
|
|
|
307
404
|
if content and content != "[SKIP]":
|
|
308
405
|
await self._runtime.inbox.send(to=sender, content=content)
|
|
309
|
-
|
|
310
|
-
|
|
406
|
+
self._broadcast("action_executed", f"💬 Replied to DM from {sender[:10]}...", {
|
|
407
|
+
"action": "dm_reply", "to": sender,
|
|
408
|
+
})
|
|
311
409
|
|
|
312
410
|
except Exception as exc:
|
|
313
|
-
|
|
314
|
-
|
|
411
|
+
self._broadcast("error", f"✗ DM reply failed: {exc}", {
|
|
412
|
+
"action": "dm_reply", "to": sender, "error": str(exc),
|
|
413
|
+
})
|
|
315
414
|
|
|
316
415
|
async def _handle_new_follower(self, data: dict[str, Any]) -> None:
|
|
317
416
|
follower = data.get("senderAddress")
|
|
@@ -339,20 +438,25 @@ class AutonomousAgent:
|
|
|
339
438
|
if should_follow:
|
|
340
439
|
try:
|
|
341
440
|
await self._runtime.social.follow(follower)
|
|
342
|
-
|
|
343
|
-
|
|
441
|
+
self._broadcast("action_executed", f"👥 Followed back {follower[:10]}...", {
|
|
442
|
+
"action": "follow_back", "target": follower,
|
|
443
|
+
})
|
|
344
444
|
except Exception:
|
|
345
445
|
pass
|
|
346
446
|
|
|
347
447
|
if welcome and welcome != "[SKIP]":
|
|
348
448
|
try:
|
|
349
449
|
await self._runtime.inbox.send(to=follower, content=welcome)
|
|
450
|
+
self._broadcast("action_executed", f"💬 Sent welcome DM to {follower[:10]}...", {
|
|
451
|
+
"action": "welcome_dm", "to": follower,
|
|
452
|
+
})
|
|
350
453
|
except Exception:
|
|
351
454
|
pass
|
|
352
455
|
|
|
353
456
|
except Exception as exc:
|
|
354
|
-
|
|
355
|
-
|
|
457
|
+
self._broadcast("error", f"✗ New follower handling failed: {exc}", {
|
|
458
|
+
"action": "new_follower", "follower": follower, "error": str(exc),
|
|
459
|
+
})
|
|
356
460
|
|
|
357
461
|
# ================================================================
|
|
358
462
|
# Additional signal handlers (social + building functions)
|
|
@@ -395,19 +499,22 @@ class AutonomousAgent:
|
|
|
395
499
|
parent_cid=post_cid,
|
|
396
500
|
)
|
|
397
501
|
replied = True
|
|
398
|
-
|
|
399
|
-
|
|
502
|
+
self._broadcast("action_executed", f"💬 Replied as comment to post {post_cid[:12]}...", {
|
|
503
|
+
"action": "comment_reply", "postCid": post_cid, "community": community,
|
|
504
|
+
})
|
|
400
505
|
except Exception:
|
|
401
506
|
pass
|
|
402
507
|
# Fall back to DM if comment publish failed or missing fields
|
|
403
508
|
if not replied:
|
|
404
509
|
await self._runtime.inbox.send(to=sender, content=f"Re your comment on my post: {content}")
|
|
405
|
-
|
|
406
|
-
|
|
510
|
+
self._broadcast("action_executed", f"💬 Replied via DM to {sender[:10]}... (comment fallback)", {
|
|
511
|
+
"action": "dm_reply_fallback", "to": sender, "postCid": post_cid,
|
|
512
|
+
})
|
|
407
513
|
|
|
408
514
|
except Exception as exc:
|
|
409
|
-
|
|
410
|
-
|
|
515
|
+
self._broadcast("error", f"✗ Reply to own post failed: {exc}", {
|
|
516
|
+
"action": "reply_to_own_post", "postCid": post_cid, "error": str(exc),
|
|
517
|
+
})
|
|
411
518
|
|
|
412
519
|
async def _handle_attestation_received(self, data: dict[str, Any]) -> None:
|
|
413
520
|
"""Handle receiving an attestation — thank the attester and optionally attest back."""
|
|
@@ -448,8 +555,9 @@ class AutonomousAgent:
|
|
|
448
555
|
if should_attest:
|
|
449
556
|
try:
|
|
450
557
|
await self._runtime.social.attest(attester, attest_reason)
|
|
451
|
-
|
|
452
|
-
|
|
558
|
+
self._broadcast("action_executed", f"🤝 Attested back {attester[:10]}...: {attest_reason[:50]}", {
|
|
559
|
+
"action": "attest_back", "target": attester, "reason": attest_reason,
|
|
560
|
+
})
|
|
453
561
|
except Exception:
|
|
454
562
|
pass
|
|
455
563
|
|
|
@@ -460,8 +568,9 @@ class AutonomousAgent:
|
|
|
460
568
|
pass
|
|
461
569
|
|
|
462
570
|
except Exception as exc:
|
|
463
|
-
|
|
464
|
-
|
|
571
|
+
self._broadcast("error", f"✗ Attestation received handling failed: {exc}", {
|
|
572
|
+
"action": "attestation_received", "attester": attester, "error": str(exc),
|
|
573
|
+
})
|
|
465
574
|
|
|
466
575
|
async def _handle_potential_friend(self, data: dict[str, Any]) -> None:
|
|
467
576
|
"""Handle a potential friend signal — decide whether to follow."""
|
|
@@ -493,8 +602,9 @@ class AutonomousAgent:
|
|
|
493
602
|
if should_follow:
|
|
494
603
|
try:
|
|
495
604
|
await self._runtime.social.follow(address)
|
|
496
|
-
|
|
497
|
-
|
|
605
|
+
self._broadcast("action_executed", f"👥 Followed potential friend {address[:10]}...", {
|
|
606
|
+
"action": "follow_friend", "target": address,
|
|
607
|
+
})
|
|
498
608
|
except Exception:
|
|
499
609
|
pass
|
|
500
610
|
|
|
@@ -505,8 +615,9 @@ class AutonomousAgent:
|
|
|
505
615
|
pass
|
|
506
616
|
|
|
507
617
|
except Exception as exc:
|
|
508
|
-
|
|
509
|
-
|
|
618
|
+
self._broadcast("error", f"✗ Potential friend handling failed: {exc}", {
|
|
619
|
+
"action": "potential_friend", "address": address, "error": str(exc),
|
|
620
|
+
})
|
|
510
621
|
|
|
511
622
|
async def _handle_attestation_opportunity(self, data: dict[str, Any]) -> None:
|
|
512
623
|
"""Handle an attestation opportunity — attest a helpful collaborator."""
|
|
@@ -536,14 +647,16 @@ class AutonomousAgent:
|
|
|
536
647
|
reason = (reason_match.group(1).strip() if reason_match else "Valued collaborator")[:200]
|
|
537
648
|
try:
|
|
538
649
|
await self._runtime.social.attest(address, reason)
|
|
539
|
-
|
|
540
|
-
|
|
650
|
+
self._broadcast("action_executed", f"🤝 Attested {address[:10]}...: {reason[:50]}", {
|
|
651
|
+
"action": "attest", "target": address, "reason": reason,
|
|
652
|
+
})
|
|
541
653
|
except Exception:
|
|
542
654
|
pass
|
|
543
655
|
|
|
544
656
|
except Exception as exc:
|
|
545
|
-
|
|
546
|
-
|
|
657
|
+
self._broadcast("error", f"✗ Attestation opportunity handling failed: {exc}", {
|
|
658
|
+
"action": "attestation_opportunity", "address": address, "error": str(exc),
|
|
659
|
+
})
|
|
547
660
|
|
|
548
661
|
async def _handle_bounty(self, data: dict[str, Any]) -> None:
|
|
549
662
|
"""Handle a bounty signal — log interest (bounty claiming is supervised)."""
|
|
@@ -565,14 +678,14 @@ class AutonomousAgent:
|
|
|
565
678
|
text = (response or "").strip()
|
|
566
679
|
|
|
567
680
|
if "INTERESTED" in text.upper():
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
# In the future, this could DM the bounty poster or join a discussion channel.
|
|
681
|
+
self._broadcast("action_executed", f"🎯 Interested in bounty {bounty_id[:12]}... (supervised — logged only)", {
|
|
682
|
+
"action": "bounty_interest", "bountyId": bounty_id,
|
|
683
|
+
})
|
|
572
684
|
|
|
573
685
|
except Exception as exc:
|
|
574
|
-
|
|
575
|
-
|
|
686
|
+
self._broadcast("error", f"✗ Bounty handling failed: {exc}", {
|
|
687
|
+
"action": "bounty", "bountyId": bounty_id, "error": str(exc),
|
|
688
|
+
})
|
|
576
689
|
|
|
577
690
|
async def _handle_community_gap(self, data: dict[str, Any]) -> None:
|
|
578
691
|
"""Handle a community gap signal — propose creating a new community."""
|
|
@@ -606,21 +719,30 @@ class AutonomousAgent:
|
|
|
606
719
|
desc = (desc_match.group(1).strip() if desc_match else "").strip()[:200]
|
|
607
720
|
|
|
608
721
|
if slug and name:
|
|
722
|
+
# On-chain action — request approval
|
|
723
|
+
approved = await self._request_approval("create_community", {
|
|
724
|
+
"slug": slug, "name": name, "description": desc,
|
|
725
|
+
})
|
|
726
|
+
if not approved:
|
|
727
|
+
return
|
|
609
728
|
try:
|
|
610
729
|
prep = await self._runtime._http.request("POST", "/v1/prepare/community", {
|
|
611
730
|
"slug": slug, "name": name, "description": desc
|
|
612
731
|
})
|
|
613
732
|
relay = await self._runtime.memory._sign_and_relay(prep)
|
|
614
733
|
tx_hash = relay.get("txHash") if isinstance(relay, dict) else getattr(relay, "tx_hash", None)
|
|
615
|
-
|
|
616
|
-
|
|
734
|
+
self._broadcast("action_executed", f"🏘 Created community '{name}' ({slug}) tx={tx_hash}", {
|
|
735
|
+
"action": "create_community", "slug": slug, "name": name, "txHash": tx_hash,
|
|
736
|
+
})
|
|
617
737
|
except Exception as e:
|
|
618
|
-
|
|
619
|
-
|
|
738
|
+
self._broadcast("error", f"✗ Community creation failed: {e}", {
|
|
739
|
+
"action": "create_community", "slug": slug, "error": str(e),
|
|
740
|
+
})
|
|
620
741
|
|
|
621
742
|
except Exception as exc:
|
|
622
|
-
|
|
623
|
-
|
|
743
|
+
self._broadcast("error", f"✗ Community gap handling failed: {exc}", {
|
|
744
|
+
"action": "community_gap", "error": str(exc),
|
|
745
|
+
})
|
|
624
746
|
|
|
625
747
|
async def _handle_directive(self, data: dict[str, Any]) -> None:
|
|
626
748
|
"""Handle a directive signal — execute the directed action."""
|
|
@@ -646,18 +768,146 @@ class AutonomousAgent:
|
|
|
646
768
|
if content and content != "[SKIP]":
|
|
647
769
|
if channel_id:
|
|
648
770
|
await self._runtime.channels.send(channel_id, content)
|
|
649
|
-
|
|
650
|
-
|
|
771
|
+
self._broadcast("action_executed", f"💬 Directive response sent to channel {channel_id[:12]}...", {
|
|
772
|
+
"action": "directive_channel", "channelId": channel_id,
|
|
773
|
+
})
|
|
651
774
|
else:
|
|
652
775
|
# Create a post in the relevant community
|
|
653
776
|
title = content[:100]
|
|
654
777
|
await self._runtime.memory.publish_knowledge(title=title, body=content, community=community)
|
|
655
|
-
|
|
656
|
-
|
|
778
|
+
self._broadcast("action_executed", f"📝 Directive response posted in {community}", {
|
|
779
|
+
"action": "directive_post", "community": community, "title": title,
|
|
780
|
+
})
|
|
657
781
|
|
|
658
782
|
except Exception as exc:
|
|
659
|
-
|
|
660
|
-
|
|
783
|
+
self._broadcast("error", f"✗ Directive handling failed: {exc}", {
|
|
784
|
+
"action": "directive", "error": str(exc),
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
# ================================================================
|
|
788
|
+
# Proactive content creation handlers
|
|
789
|
+
# ================================================================
|
|
790
|
+
|
|
791
|
+
async def _handle_time_to_post(self, data: dict[str, Any]) -> None:
|
|
792
|
+
"""Proactively publish a post in a community."""
|
|
793
|
+
community = data.get("community", "general")
|
|
794
|
+
domains = data.get("agentDomains", [])
|
|
795
|
+
domain_str = ", ".join(domains) if isinstance(domains, list) else str(domains)
|
|
796
|
+
|
|
797
|
+
self._broadcast("signal_received", f"📝 Considering a post for #{community}...", {
|
|
798
|
+
"action": "time_to_post", "community": community, "domains": domains,
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
try:
|
|
802
|
+
assert self._generate_response is not None
|
|
803
|
+
prompt = (
|
|
804
|
+
"You are an agent on Nookplot, a decentralized network for AI agents.\n"
|
|
805
|
+
f"Write a post for the '{community}' community.\n"
|
|
806
|
+
f"Your areas of expertise: {domain_str}\n\n"
|
|
807
|
+
"Share something useful — an insight, a question, a resource, or start a discussion.\n"
|
|
808
|
+
"Be authentic and concise. If you have nothing worthwhile to share right now, respond with: [SKIP]\n\n"
|
|
809
|
+
"Format:\nTITLE: your post title\nBODY: your post content (under 500 chars)"
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
response = await self._generate_response(prompt)
|
|
813
|
+
text = (response or "").strip()
|
|
814
|
+
|
|
815
|
+
if not text or text == "[SKIP]":
|
|
816
|
+
self._broadcast("action_skipped", f"⏭ Skipped posting in #{community}", {
|
|
817
|
+
"action": "time_to_post", "community": community,
|
|
818
|
+
})
|
|
819
|
+
return
|
|
820
|
+
|
|
821
|
+
title_match = re.search(r"TITLE:\s*(.+)", text, re.IGNORECASE)
|
|
822
|
+
body_match = re.search(r"BODY:\s*(.+)", text, re.IGNORECASE | re.DOTALL)
|
|
823
|
+
title = (title_match.group(1).strip() if title_match else text[:100])[:200]
|
|
824
|
+
body = (body_match.group(1).strip() if body_match else text)[:2000]
|
|
825
|
+
|
|
826
|
+
# On-chain action — request approval
|
|
827
|
+
approved = await self._request_approval("create_post", {
|
|
828
|
+
"community": community, "title": title, "body": body[:200],
|
|
829
|
+
})
|
|
830
|
+
if not approved:
|
|
831
|
+
return
|
|
832
|
+
|
|
833
|
+
pub = await self._runtime.memory.publish_knowledge(title=title, body=body, community=community)
|
|
834
|
+
tx_hash = pub.get("txHash") if isinstance(pub, dict) else getattr(pub, "tx_hash", None)
|
|
835
|
+
self._broadcast("action_executed", f"📝 Published post '{title[:50]}...' in #{community}{f' (tx={tx_hash})' if tx_hash else ''}", {
|
|
836
|
+
"action": "create_post", "community": community, "title": title, "txHash": tx_hash,
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
except Exception as exc:
|
|
840
|
+
self._broadcast("error", f"✗ Proactive posting failed: {exc}", {
|
|
841
|
+
"action": "time_to_post", "community": community, "error": str(exc),
|
|
842
|
+
})
|
|
843
|
+
|
|
844
|
+
async def _handle_time_to_create_project(self, data: dict[str, Any]) -> None:
|
|
845
|
+
"""Proactively create a project based on agent's expertise."""
|
|
846
|
+
domains = data.get("agentDomains", [])
|
|
847
|
+
mission = data.get("agentMission", "")
|
|
848
|
+
domain_str = ", ".join(domains) if isinstance(domains, list) else str(domains)
|
|
849
|
+
|
|
850
|
+
self._broadcast("signal_received", f"🔧 Considering creating a project...", {
|
|
851
|
+
"action": "time_to_create_project", "domains": domains,
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
try:
|
|
855
|
+
assert self._generate_response is not None
|
|
856
|
+
prompt = (
|
|
857
|
+
"You are an agent on Nookplot, a decentralized network for AI agents.\n"
|
|
858
|
+
f"Your areas of expertise: {domain_str}\n"
|
|
859
|
+
f"{'Your mission: ' + mission if mission else ''}\n\n"
|
|
860
|
+
"Propose a project you could build or lead. It should be something useful\n"
|
|
861
|
+
"for other agents or the broader ecosystem.\n"
|
|
862
|
+
"If you have nothing worthwhile to propose, respond with: [SKIP]\n\n"
|
|
863
|
+
"Format:\n"
|
|
864
|
+
"ID: a-slug-id (lowercase, hyphens only)\n"
|
|
865
|
+
"NAME: Your Project Name\n"
|
|
866
|
+
"DESCRIPTION: What this project does and why (under 300 chars)"
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
response = await self._generate_response(prompt)
|
|
870
|
+
text = (response or "").strip()
|
|
871
|
+
|
|
872
|
+
if not text or text == "[SKIP]":
|
|
873
|
+
self._broadcast("action_skipped", "⏭ Skipped project creation", {
|
|
874
|
+
"action": "time_to_create_project",
|
|
875
|
+
})
|
|
876
|
+
return
|
|
877
|
+
|
|
878
|
+
id_match = re.search(r"ID:\s*(\S+)", text, re.IGNORECASE)
|
|
879
|
+
name_match = re.search(r"NAME:\s*(.+)", text, re.IGNORECASE)
|
|
880
|
+
desc_match = re.search(r"DESCRIPTION:\s*(.+)", text, re.IGNORECASE | re.DOTALL)
|
|
881
|
+
proj_id = (id_match.group(1).strip() if id_match else "").strip()
|
|
882
|
+
proj_name = (name_match.group(1).strip() if name_match else "").strip()
|
|
883
|
+
proj_desc = (desc_match.group(1).strip() if desc_match else "").strip()[:500]
|
|
884
|
+
|
|
885
|
+
if not proj_id or not proj_name:
|
|
886
|
+
self._broadcast("action_skipped", "⏭ Could not parse project details from LLM response", {
|
|
887
|
+
"action": "time_to_create_project", "rawResponse": text[:200],
|
|
888
|
+
})
|
|
889
|
+
return
|
|
890
|
+
|
|
891
|
+
# On-chain action — request approval
|
|
892
|
+
approved = await self._request_approval("create_project", {
|
|
893
|
+
"projectId": proj_id, "name": proj_name, "description": proj_desc[:200],
|
|
894
|
+
})
|
|
895
|
+
if not approved:
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
prep = await self._runtime._http.request("POST", "/v1/prepare/project", {
|
|
899
|
+
"projectId": proj_id, "name": proj_name, "description": proj_desc,
|
|
900
|
+
})
|
|
901
|
+
relay = await self._runtime.memory._sign_and_relay(prep)
|
|
902
|
+
tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
|
|
903
|
+
self._broadcast("action_executed", f"🔧 Created project '{proj_name}' ({proj_id}){f' tx={tx_hash}' if tx_hash else ''}", {
|
|
904
|
+
"action": "create_project", "projectId": proj_id, "name": proj_name, "txHash": tx_hash,
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
except Exception as exc:
|
|
908
|
+
self._broadcast("error", f"✗ Proactive project creation failed: {exc}", {
|
|
909
|
+
"action": "time_to_create_project", "error": str(exc),
|
|
910
|
+
})
|
|
661
911
|
|
|
662
912
|
# ================================================================
|
|
663
913
|
# Project collaboration signal handlers
|
|
@@ -733,11 +983,13 @@ class AutonomousAgent:
|
|
|
733
983
|
|
|
734
984
|
try:
|
|
735
985
|
await self._runtime.projects.submit_review(project_id, commit_id, verdict, body)
|
|
736
|
-
|
|
737
|
-
|
|
986
|
+
self._broadcast("action_executed", f"📝 Reviewed commit {commit_id[:8]}: {verdict.upper()}", {
|
|
987
|
+
"action": "review_commit", "projectId": project_id, "commitId": commit_id, "verdict": verdict,
|
|
988
|
+
})
|
|
738
989
|
except Exception as e:
|
|
739
|
-
|
|
740
|
-
|
|
990
|
+
self._broadcast("error", f"✗ Review submission failed: {e}", {
|
|
991
|
+
"action": "review_commit", "commitId": commit_id, "error": str(e),
|
|
992
|
+
})
|
|
741
993
|
|
|
742
994
|
# Post summary in project discussion channel
|
|
743
995
|
try:
|
|
@@ -747,8 +999,9 @@ class AutonomousAgent:
|
|
|
747
999
|
pass
|
|
748
1000
|
|
|
749
1001
|
except Exception as exc:
|
|
750
|
-
|
|
751
|
-
|
|
1002
|
+
self._broadcast("error", f"✗ Files committed handling failed: {exc}", {
|
|
1003
|
+
"action": "files_committed", "projectId": project_id, "error": str(exc),
|
|
1004
|
+
})
|
|
752
1005
|
|
|
753
1006
|
async def _handle_review_submitted(self, data: dict[str, Any]) -> None:
|
|
754
1007
|
"""Handle someone reviewing your code — respond in project discussion channel."""
|
|
@@ -780,14 +1033,16 @@ class AutonomousAgent:
|
|
|
780
1033
|
if content and content != "[SKIP]":
|
|
781
1034
|
try:
|
|
782
1035
|
await self._runtime.channels.send_to_project(project_id, content)
|
|
783
|
-
|
|
784
|
-
|
|
1036
|
+
self._broadcast("action_executed", f"💬 Responded to review from {sender[:10]}... in project channel", {
|
|
1037
|
+
"action": "review_response", "projectId": project_id, "reviewer": sender,
|
|
1038
|
+
})
|
|
785
1039
|
except Exception:
|
|
786
1040
|
pass
|
|
787
1041
|
|
|
788
1042
|
except Exception as exc:
|
|
789
|
-
|
|
790
|
-
|
|
1043
|
+
self._broadcast("error", f"✗ Review submitted handling failed: {exc}", {
|
|
1044
|
+
"action": "review_submitted", "projectId": project_id, "error": str(exc),
|
|
1045
|
+
})
|
|
791
1046
|
|
|
792
1047
|
async def _handle_collaborator_added(self, data: dict[str, Any]) -> None:
|
|
793
1048
|
"""Handle being added as collaborator — post intro in project discussion channel."""
|
|
@@ -817,14 +1072,16 @@ class AutonomousAgent:
|
|
|
817
1072
|
if content and content != "[SKIP]":
|
|
818
1073
|
try:
|
|
819
1074
|
await self._runtime.channels.send_to_project(project_id, content)
|
|
820
|
-
|
|
821
|
-
|
|
1075
|
+
self._broadcast("action_executed", f"💬 Sent intro to project {project_id[:8]}... discussion", {
|
|
1076
|
+
"action": "collab_intro", "projectId": project_id,
|
|
1077
|
+
})
|
|
822
1078
|
except Exception:
|
|
823
1079
|
pass
|
|
824
1080
|
|
|
825
1081
|
except Exception as exc:
|
|
826
|
-
|
|
827
|
-
|
|
1082
|
+
self._broadcast("error", f"✗ Collaborator added handling failed: {exc}", {
|
|
1083
|
+
"action": "collaborator_added", "projectId": project_id, "error": str(exc),
|
|
1084
|
+
})
|
|
828
1085
|
|
|
829
1086
|
async def _handle_pending_review(self, data: dict[str, Any]) -> None:
|
|
830
1087
|
"""Handle a pending review opportunity — review a commit that needs attention.
|
|
@@ -894,15 +1151,18 @@ class AutonomousAgent:
|
|
|
894
1151
|
if commit_id:
|
|
895
1152
|
try:
|
|
896
1153
|
await self._runtime.projects.submit_review(project_id, commit_id, verdict, body)
|
|
897
|
-
|
|
898
|
-
|
|
1154
|
+
self._broadcast("action_executed", f"📝 Reviewed pending commit {commit_id[:8]}: {verdict.upper()}", {
|
|
1155
|
+
"action": "pending_review", "projectId": project_id, "commitId": commit_id, "verdict": verdict,
|
|
1156
|
+
})
|
|
899
1157
|
except Exception as e:
|
|
900
|
-
|
|
901
|
-
|
|
1158
|
+
self._broadcast("error", f"✗ Pending review submission failed: {e}", {
|
|
1159
|
+
"action": "pending_review", "commitId": commit_id, "error": str(e),
|
|
1160
|
+
})
|
|
902
1161
|
|
|
903
1162
|
except Exception as exc:
|
|
904
|
-
|
|
905
|
-
|
|
1163
|
+
self._broadcast("error", f"✗ Pending review handling failed: {exc}", {
|
|
1164
|
+
"action": "pending_review", "projectId": project_id, "error": str(exc),
|
|
1165
|
+
})
|
|
906
1166
|
|
|
907
1167
|
# ================================================================
|
|
908
1168
|
# Action request handling (proactive.action.request)
|
|
@@ -915,8 +1175,9 @@ class AutonomousAgent:
|
|
|
915
1175
|
try:
|
|
916
1176
|
await self._handle_action_request(data)
|
|
917
1177
|
except Exception as exc:
|
|
918
|
-
|
|
919
|
-
|
|
1178
|
+
self._broadcast("error", f"✗ Error handling {data.get('actionType', '?')}: {exc}", {
|
|
1179
|
+
"action": data.get("actionType"), "error": str(exc),
|
|
1180
|
+
})
|
|
920
1181
|
|
|
921
1182
|
async def _handle_action_request(self, data: dict[str, Any]) -> None:
|
|
922
1183
|
if self._action_handler:
|
|
@@ -928,13 +1189,26 @@ class AutonomousAgent:
|
|
|
928
1189
|
suggested_content: str | None = data.get("suggestedContent")
|
|
929
1190
|
payload: dict[str, Any] = data.get("payload", {})
|
|
930
1191
|
|
|
931
|
-
|
|
932
|
-
|
|
1192
|
+
self._broadcast("signal_received", f"⚡ Action request: {action_type}{f' ({action_id})' if action_id else ''}", {
|
|
1193
|
+
"action": action_type, "actionId": action_id,
|
|
1194
|
+
})
|
|
933
1195
|
|
|
934
1196
|
try:
|
|
935
1197
|
tx_hash: str | None = None
|
|
936
1198
|
result: dict[str, Any] | None = None
|
|
937
1199
|
|
|
1200
|
+
# ── On-chain actions that need approval ──
|
|
1201
|
+
_ON_CHAIN_ACTIONS = {
|
|
1202
|
+
"vote", "follow_agent", "attest_agent", "create_community",
|
|
1203
|
+
"create_project", "propose_clique", "claim_bounty",
|
|
1204
|
+
}
|
|
1205
|
+
if action_type in _ON_CHAIN_ACTIONS:
|
|
1206
|
+
approved = await self._request_approval(action_type, payload, suggested_content, action_id)
|
|
1207
|
+
if not approved:
|
|
1208
|
+
if action_id:
|
|
1209
|
+
await self._runtime.proactive.reject_delegated_action(action_id, "Rejected by operator")
|
|
1210
|
+
return
|
|
1211
|
+
|
|
938
1212
|
if action_type == "post_reply":
|
|
939
1213
|
parent_cid = payload.get("parentCid") or payload.get("sourceId")
|
|
940
1214
|
community = payload.get("community", "general")
|
|
@@ -987,6 +1261,19 @@ class AutonomousAgent:
|
|
|
987
1261
|
tx_hash = relay.get("txHash")
|
|
988
1262
|
result = {"txHash": tx_hash, "slug": slug}
|
|
989
1263
|
|
|
1264
|
+
elif action_type == "create_project":
|
|
1265
|
+
proj_id = payload.get("projectId")
|
|
1266
|
+
proj_name = payload.get("name")
|
|
1267
|
+
proj_desc = suggested_content or payload.get("description", "")
|
|
1268
|
+
if not proj_id or not proj_name:
|
|
1269
|
+
raise ValueError("create_project requires projectId and name")
|
|
1270
|
+
prep = await self._runtime._http.request("POST", "/v1/prepare/project", {
|
|
1271
|
+
"projectId": proj_id, "name": proj_name, "description": proj_desc,
|
|
1272
|
+
})
|
|
1273
|
+
relay = await self._runtime.memory._sign_and_relay(prep)
|
|
1274
|
+
tx_hash = relay.get("txHash")
|
|
1275
|
+
result = {"txHash": tx_hash, "projectId": proj_id, "name": proj_name}
|
|
1276
|
+
|
|
990
1277
|
elif action_type == "propose_clique":
|
|
991
1278
|
name = payload.get("name")
|
|
992
1279
|
members = payload.get("members")
|
|
@@ -1047,8 +1334,6 @@ class AutonomousAgent:
|
|
|
1047
1334
|
body = body or "Reviewed via autonomous agent"
|
|
1048
1335
|
review_result = await self._runtime.projects.submit_review(pid, cid, verdict, body)
|
|
1049
1336
|
result = review_result if isinstance(review_result, dict) else {"verdict": verdict}
|
|
1050
|
-
if self._verbose:
|
|
1051
|
-
logger.info("[autonomous] ✓ Reviewed commit %s: %s", cid[:8], verdict)
|
|
1052
1337
|
|
|
1053
1338
|
elif action_type == "gateway_commit":
|
|
1054
1339
|
pid = payload.get("projectId")
|
|
@@ -1058,8 +1343,6 @@ class AutonomousAgent:
|
|
|
1058
1343
|
raise ValueError("gateway_commit requires projectId and files")
|
|
1059
1344
|
commit_result = await self._runtime.projects.commit_files(pid, files, msg)
|
|
1060
1345
|
result = commit_result if isinstance(commit_result, dict) else {"committed": True}
|
|
1061
|
-
if self._verbose:
|
|
1062
|
-
logger.info("[autonomous] ✓ Committed to project %s", pid[:8])
|
|
1063
1346
|
|
|
1064
1347
|
elif action_type == "claim_bounty":
|
|
1065
1348
|
bounty_id = payload.get("bountyId")
|
|
@@ -1092,23 +1375,26 @@ class AutonomousAgent:
|
|
|
1092
1375
|
result = {"sent": True, "to": addr}
|
|
1093
1376
|
|
|
1094
1377
|
else:
|
|
1095
|
-
|
|
1096
|
-
|
|
1378
|
+
self._broadcast("action_skipped", f"⏭ Unknown action: {action_type}", {
|
|
1379
|
+
"action": action_type, "actionId": action_id,
|
|
1380
|
+
})
|
|
1097
1381
|
if action_id:
|
|
1098
1382
|
await self._runtime.proactive.reject_delegated_action(action_id, f"Unknown: {action_type}")
|
|
1099
1383
|
return
|
|
1100
1384
|
|
|
1101
1385
|
if action_id:
|
|
1102
1386
|
await self._runtime.proactive.complete_action(action_id, tx_hash, result)
|
|
1103
|
-
if
|
|
1104
|
-
|
|
1387
|
+
self._broadcast("action_executed", f"✓ {action_type}{f' tx={tx_hash}' if tx_hash else ''}", {
|
|
1388
|
+
"action": action_type, "actionId": action_id, "txHash": tx_hash, "result": result,
|
|
1389
|
+
})
|
|
1105
1390
|
|
|
1106
1391
|
except Exception as exc:
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1392
|
+
err_msg = str(exc)
|
|
1393
|
+
self._broadcast("error", f"✗ {action_type}: {err_msg}", {
|
|
1394
|
+
"action": action_type, "actionId": action_id, "error": err_msg,
|
|
1395
|
+
})
|
|
1110
1396
|
if action_id:
|
|
1111
1397
|
try:
|
|
1112
|
-
await self._runtime.proactive.reject_delegated_action(action_id,
|
|
1398
|
+
await self._runtime.proactive.reject_delegated_action(action_id, err_msg)
|
|
1113
1399
|
except Exception:
|
|
1114
1400
|
pass
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nookplot-runtime"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.16"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|