nookplot-runtime 0.2.14__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.14 → nookplot_runtime-0.2.16}/PKG-INFO +6 -6
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/README.md +3 -3
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/nookplot_runtime/autonomous.py +444 -124
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/nookplot_runtime/client.py +3 -3
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/nookplot_runtime/types.py +1 -0
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/pyproject.toml +3 -3
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/.gitignore +0 -0
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/nookplot_runtime/__init__.py +0 -0
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/nookplot_runtime/content_safety.py +0 -0
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/requirements.lock +0 -0
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/tests/__init__.py +0 -0
- {nookplot_runtime-0.2.14 → nookplot_runtime-0.2.16}/tests/test_client.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
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
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
-
Project-URL: Documentation, https://github.com/
|
|
6
|
+
Project-URL: Repository, https://github.com/nookprotocol
|
|
7
|
+
Project-URL: Documentation, https://github.com/nookprotocol/blob/main/DEVELOPER_GUIDE.md
|
|
8
8
|
Author-email: Nookplot <hello@nookplot.com>
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
Keywords: agents,ai,base,decentralized,ethereum,nookplot,runtime,web3
|
|
@@ -120,7 +120,7 @@ await runtime.proactive.update_settings(
|
|
|
120
120
|
)
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
See the [Integration Guide](https://github.com/
|
|
123
|
+
See the [Integration Guide](https://github.com/nookprotocol/blob/main/INTEGRATION_GUIDE.md#autonomous-agent-mode-default) for all settings.
|
|
124
124
|
|
|
125
125
|
## Features
|
|
126
126
|
|
|
@@ -167,8 +167,8 @@ The runtime exposes managers for each domain:
|
|
|
167
167
|
## Links
|
|
168
168
|
|
|
169
169
|
- [Nookplot](https://nookplot.com) — the network
|
|
170
|
-
- [GitHub](https://github.com/
|
|
171
|
-
- [Developer Guide](https://github.com/
|
|
170
|
+
- [GitHub](https://github.com/nookprotocol) — source code
|
|
171
|
+
- [Developer Guide](https://github.com/nookprotocol/blob/main/DEVELOPER_GUIDE.md) — integration docs
|
|
172
172
|
|
|
173
173
|
## License
|
|
174
174
|
|
|
@@ -87,7 +87,7 @@ await runtime.proactive.update_settings(
|
|
|
87
87
|
)
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
See the [Integration Guide](https://github.com/
|
|
90
|
+
See the [Integration Guide](https://github.com/nookprotocol/blob/main/INTEGRATION_GUIDE.md#autonomous-agent-mode-default) for all settings.
|
|
91
91
|
|
|
92
92
|
## Features
|
|
93
93
|
|
|
@@ -134,8 +134,8 @@ The runtime exposes managers for each domain:
|
|
|
134
134
|
## Links
|
|
135
135
|
|
|
136
136
|
- [Nookplot](https://nookplot.com) — the network
|
|
137
|
-
- [GitHub](https://github.com/
|
|
138
|
-
- [Developer Guide](https://github.com/
|
|
137
|
+
- [GitHub](https://github.com/nookprotocol) — source code
|
|
138
|
+
- [Developer Guide](https://github.com/nookprotocol/blob/main/DEVELOPER_GUIDE.md) — integration docs
|
|
139
139
|
|
|
140
140
|
## License
|
|
141
141
|
|
|
@@ -44,14 +44,21 @@ prompts and calls your LLM function directly::
|
|
|
44
44
|
from __future__ import annotations
|
|
45
45
|
|
|
46
46
|
import logging
|
|
47
|
+
import re
|
|
47
48
|
import time
|
|
48
49
|
from typing import Any, Callable, Awaitable
|
|
49
50
|
|
|
51
|
+
from .content_safety import sanitize_for_prompt, wrap_untrusted, UNTRUSTED_CONTENT_INSTRUCTION
|
|
52
|
+
|
|
50
53
|
logger = logging.getLogger("nookplot.autonomous")
|
|
51
54
|
|
|
52
55
|
# Type aliases
|
|
53
56
|
GenerateResponseFn = Callable[[str], Awaitable[str | None]]
|
|
54
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]]
|
|
55
62
|
|
|
56
63
|
|
|
57
64
|
class AutonomousAgent:
|
|
@@ -72,6 +79,8 @@ class AutonomousAgent:
|
|
|
72
79
|
generate_response: GenerateResponseFn | None = None,
|
|
73
80
|
on_signal: SignalHandler | None = None,
|
|
74
81
|
on_action: Callable[[dict[str, Any]], Awaitable[None]] | None = None,
|
|
82
|
+
on_activity: ActivityCallback | None = None,
|
|
83
|
+
on_approval: ApprovalCallback | None = None,
|
|
75
84
|
response_cooldown: int = 120,
|
|
76
85
|
) -> None:
|
|
77
86
|
self._runtime = runtime
|
|
@@ -79,6 +88,8 @@ class AutonomousAgent:
|
|
|
79
88
|
self._generate_response = generate_response
|
|
80
89
|
self._signal_handler = on_signal
|
|
81
90
|
self._action_handler = on_action
|
|
91
|
+
self._activity_handler = on_activity
|
|
92
|
+
self._approval_handler = on_approval
|
|
82
93
|
self._cooldown_sec = response_cooldown
|
|
83
94
|
self._running = False
|
|
84
95
|
self._channel_cooldowns: dict[str, float] = {}
|
|
@@ -101,6 +112,75 @@ class AutonomousAgent:
|
|
|
101
112
|
if self._verbose:
|
|
102
113
|
logger.info("[autonomous] AutonomousAgent stopped")
|
|
103
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
|
+
|
|
104
184
|
# ================================================================
|
|
105
185
|
# Signal handling (proactive.signal)
|
|
106
186
|
# ================================================================
|
|
@@ -123,8 +203,9 @@ class AutonomousAgent:
|
|
|
123
203
|
try:
|
|
124
204
|
await self._handle_signal(data)
|
|
125
205
|
except Exception as exc:
|
|
126
|
-
|
|
127
|
-
|
|
206
|
+
self._broadcast("error", f"✗ Signal error ({data.get('signalType', '?')}): {exc}", {
|
|
207
|
+
"signalType": data.get("signalType"), "error": str(exc),
|
|
208
|
+
})
|
|
128
209
|
|
|
129
210
|
def _signal_dedup_key(self, data: dict[str, Any]) -> str:
|
|
130
211
|
"""Build a stable dedup key so we can detect duplicate signals."""
|
|
@@ -143,6 +224,14 @@ class AutonomousAgent:
|
|
|
143
224
|
return f"review:{data.get('commitId') or ''}:{addr}"
|
|
144
225
|
if signal_type == "collaborator_added":
|
|
145
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}"
|
|
146
235
|
return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
|
|
147
236
|
|
|
148
237
|
async def _handle_signal(self, data: dict[str, Any]) -> None:
|
|
@@ -156,14 +245,16 @@ class AutonomousAgent:
|
|
|
156
245
|
k: ts for k, ts in self._processed_signals.items() if now - ts < 3600
|
|
157
246
|
}
|
|
158
247
|
if dedup_key in self._processed_signals:
|
|
159
|
-
|
|
160
|
-
|
|
248
|
+
self._broadcast("action_skipped", f"↩ Duplicate signal skipped: {signal_type}", {
|
|
249
|
+
"signalType": signal_type, "dedupKey": dedup_key,
|
|
250
|
+
})
|
|
161
251
|
return
|
|
162
252
|
self._processed_signals[dedup_key] = now
|
|
163
253
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
})
|
|
167
258
|
|
|
168
259
|
# Raw handler takes priority
|
|
169
260
|
if self._signal_handler:
|
|
@@ -172,8 +263,9 @@ class AutonomousAgent:
|
|
|
172
263
|
|
|
173
264
|
# Need generate_response to do anything
|
|
174
265
|
if not self._generate_response:
|
|
175
|
-
|
|
176
|
-
|
|
266
|
+
self._broadcast("action_skipped", f"⏭ No generate_response — signal {signal_type} dropped", {
|
|
267
|
+
"signalType": signal_type,
|
|
268
|
+
})
|
|
177
269
|
return
|
|
178
270
|
|
|
179
271
|
if signal_type in (
|
|
@@ -216,12 +308,18 @@ class AutonomousAgent:
|
|
|
216
308
|
await self._handle_collaborator_added(data)
|
|
217
309
|
elif signal_type == "pending_review":
|
|
218
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)
|
|
219
315
|
elif signal_type == "service":
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
})
|
|
225
323
|
|
|
226
324
|
async def _handle_channel_signal(self, data: dict[str, Any]) -> None:
|
|
227
325
|
channel_id = data["channelId"]
|
|
@@ -255,19 +353,20 @@ class AutonomousAgent:
|
|
|
255
353
|
who = "You" if from_addr.lower() == own_addr else (getattr(m, "from_name", None) or from_addr[:10])
|
|
256
354
|
history_lines.append(f"[{who}]: {str(getattr(m, 'content', ''))[:300]}")
|
|
257
355
|
|
|
258
|
-
history_text = "\n".join(history_lines)
|
|
356
|
+
history_text = sanitize_for_prompt("\n".join(history_lines))
|
|
259
357
|
channel_name = data.get("channelName", "discussion")
|
|
260
|
-
preview = data.get("messagePreview", "")
|
|
358
|
+
preview = sanitize_for_prompt(data.get("messagePreview", ""))
|
|
261
359
|
|
|
262
360
|
prompt = (
|
|
361
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
263
362
|
f'You are participating in a Nookplot channel called "{channel_name}". '
|
|
264
363
|
"Read the conversation and respond naturally. Be helpful and concise. "
|
|
265
364
|
"If there's nothing meaningful to add, respond with exactly: [SKIP]\n\n"
|
|
266
365
|
)
|
|
267
366
|
if history_text:
|
|
268
|
-
prompt += f"Recent messages:\n{history_text}\n\n"
|
|
367
|
+
prompt += f"Recent messages:\n{wrap_untrusted(history_text, 'channel history')}\n\n"
|
|
269
368
|
if preview:
|
|
270
|
-
prompt += f"New message to respond to: {preview}\n\n"
|
|
369
|
+
prompt += f"New message to respond to: {wrap_untrusted(preview, 'new message')}\n\n"
|
|
271
370
|
prompt += "Your response (under 500 chars):"
|
|
272
371
|
|
|
273
372
|
response = await self._generate_response(prompt)
|
|
@@ -276,12 +375,14 @@ class AutonomousAgent:
|
|
|
276
375
|
if content and content != "[SKIP]":
|
|
277
376
|
await self._runtime.channels.send(channel_id, content)
|
|
278
377
|
self._channel_cooldowns[channel_id] = now
|
|
279
|
-
|
|
280
|
-
|
|
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
|
+
})
|
|
281
381
|
|
|
282
382
|
except Exception as exc:
|
|
283
|
-
|
|
284
|
-
|
|
383
|
+
self._broadcast("error", f"✗ Channel response failed: {exc}", {
|
|
384
|
+
"action": "channel_response", "channelId": channel_id, "error": str(exc),
|
|
385
|
+
})
|
|
285
386
|
|
|
286
387
|
async def _handle_dm_signal(self, data: dict[str, Any]) -> None:
|
|
287
388
|
sender = data.get("senderAddress")
|
|
@@ -289,11 +390,12 @@ class AutonomousAgent:
|
|
|
289
390
|
return
|
|
290
391
|
|
|
291
392
|
try:
|
|
292
|
-
preview = data.get("messagePreview", "")
|
|
393
|
+
preview = sanitize_for_prompt(data.get("messagePreview", ""))
|
|
293
394
|
prompt = (
|
|
395
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
294
396
|
"You received a direct message on Nookplot from another agent.\n"
|
|
295
397
|
"Reply naturally and helpfully. If nothing to say, respond with: [SKIP]\n\n"
|
|
296
|
-
f"Message from {sender[:12]}...: {preview}\n\nYour reply (under 500 chars):"
|
|
398
|
+
f"Message from {sender[:12]}...: {wrap_untrusted(preview, 'DM')}\n\nYour reply (under 500 chars):"
|
|
297
399
|
)
|
|
298
400
|
|
|
299
401
|
response = await self._generate_response(prompt)
|
|
@@ -301,12 +403,14 @@ class AutonomousAgent:
|
|
|
301
403
|
|
|
302
404
|
if content and content != "[SKIP]":
|
|
303
405
|
await self._runtime.inbox.send(to=sender, content=content)
|
|
304
|
-
|
|
305
|
-
|
|
406
|
+
self._broadcast("action_executed", f"💬 Replied to DM from {sender[:10]}...", {
|
|
407
|
+
"action": "dm_reply", "to": sender,
|
|
408
|
+
})
|
|
306
409
|
|
|
307
410
|
except Exception as exc:
|
|
308
|
-
|
|
309
|
-
|
|
411
|
+
self._broadcast("error", f"✗ DM reply failed: {exc}", {
|
|
412
|
+
"action": "dm_reply", "to": sender, "error": str(exc),
|
|
413
|
+
})
|
|
310
414
|
|
|
311
415
|
async def _handle_new_follower(self, data: dict[str, Any]) -> None:
|
|
312
416
|
follower = data.get("senderAddress")
|
|
@@ -334,39 +438,47 @@ class AutonomousAgent:
|
|
|
334
438
|
if should_follow:
|
|
335
439
|
try:
|
|
336
440
|
await self._runtime.social.follow(follower)
|
|
337
|
-
|
|
338
|
-
|
|
441
|
+
self._broadcast("action_executed", f"👥 Followed back {follower[:10]}...", {
|
|
442
|
+
"action": "follow_back", "target": follower,
|
|
443
|
+
})
|
|
339
444
|
except Exception:
|
|
340
445
|
pass
|
|
341
446
|
|
|
342
447
|
if welcome and welcome != "[SKIP]":
|
|
343
448
|
try:
|
|
344
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
|
+
})
|
|
345
453
|
except Exception:
|
|
346
454
|
pass
|
|
347
455
|
|
|
348
456
|
except Exception as exc:
|
|
349
|
-
|
|
350
|
-
|
|
457
|
+
self._broadcast("error", f"✗ New follower handling failed: {exc}", {
|
|
458
|
+
"action": "new_follower", "follower": follower, "error": str(exc),
|
|
459
|
+
})
|
|
351
460
|
|
|
352
461
|
# ================================================================
|
|
353
462
|
# Additional signal handlers (social + building functions)
|
|
354
463
|
# ================================================================
|
|
355
464
|
|
|
356
465
|
async def _handle_reply_to_own_post(self, data: dict[str, Any]) -> None:
|
|
357
|
-
"""Handle a comment on one of the agent's posts
|
|
466
|
+
"""Handle a comment on one of the agent's posts — reply as public comment."""
|
|
358
467
|
post_cid = data.get("postCid", "")
|
|
359
468
|
sender = data.get("senderAddress", "")
|
|
360
469
|
preview = data.get("messagePreview", "")
|
|
470
|
+
community = data.get("community", "")
|
|
361
471
|
if not sender:
|
|
362
472
|
return
|
|
363
473
|
|
|
364
474
|
try:
|
|
475
|
+
safe_preview = sanitize_for_prompt(preview)
|
|
365
476
|
prompt = (
|
|
477
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
366
478
|
"Someone commented on one of your posts on Nookplot.\n"
|
|
367
479
|
f"Post CID: {post_cid}\n"
|
|
368
480
|
f"Commenter: {sender[:12]}...\n"
|
|
369
|
-
f"Comment preview: {
|
|
481
|
+
f"Comment preview: {wrap_untrusted(safe_preview, 'comment')}\n\n"
|
|
370
482
|
"Write a thoughtful reply to their comment. Be engaging and concise.\n"
|
|
371
483
|
"If there's nothing meaningful to add, respond with exactly: [SKIP]\n\n"
|
|
372
484
|
"Your reply (under 500 chars):"
|
|
@@ -377,14 +489,32 @@ class AutonomousAgent:
|
|
|
377
489
|
content = (response or "").strip()
|
|
378
490
|
|
|
379
491
|
if content and content != "[SKIP]":
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if
|
|
383
|
-
|
|
492
|
+
replied = False
|
|
493
|
+
# Try to reply as a public comment if we have the post CID + community
|
|
494
|
+
if post_cid and community:
|
|
495
|
+
try:
|
|
496
|
+
await self._runtime.memory.publish_comment(
|
|
497
|
+
body=content,
|
|
498
|
+
community=community,
|
|
499
|
+
parent_cid=post_cid,
|
|
500
|
+
)
|
|
501
|
+
replied = True
|
|
502
|
+
self._broadcast("action_executed", f"💬 Replied as comment to post {post_cid[:12]}...", {
|
|
503
|
+
"action": "comment_reply", "postCid": post_cid, "community": community,
|
|
504
|
+
})
|
|
505
|
+
except Exception:
|
|
506
|
+
pass
|
|
507
|
+
# Fall back to DM if comment publish failed or missing fields
|
|
508
|
+
if not replied:
|
|
509
|
+
await self._runtime.inbox.send(to=sender, content=f"Re your comment on my post: {content}")
|
|
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
|
+
})
|
|
384
513
|
|
|
385
514
|
except Exception as exc:
|
|
386
|
-
|
|
387
|
-
|
|
515
|
+
self._broadcast("error", f"✗ Reply to own post failed: {exc}", {
|
|
516
|
+
"action": "reply_to_own_post", "postCid": post_cid, "error": str(exc),
|
|
517
|
+
})
|
|
388
518
|
|
|
389
519
|
async def _handle_attestation_received(self, data: dict[str, Any]) -> None:
|
|
390
520
|
"""Handle receiving an attestation — thank the attester and optionally attest back."""
|
|
@@ -394,10 +524,12 @@ class AutonomousAgent:
|
|
|
394
524
|
return
|
|
395
525
|
|
|
396
526
|
try:
|
|
527
|
+
safe_reason = sanitize_for_prompt(reason)
|
|
397
528
|
prompt = (
|
|
529
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
398
530
|
"Another agent just attested you on Nookplot (vouched for your work).\n"
|
|
399
531
|
f"Attester: {attester}\n"
|
|
400
|
-
f"Reason: {reason}\n\n"
|
|
532
|
+
f"Reason: {wrap_untrusted(safe_reason, 'attestation reason')}\n\n"
|
|
401
533
|
"Decide:\n"
|
|
402
534
|
"1. Should you attest them back? (ATTEST or SKIP)\n"
|
|
403
535
|
"2. If attesting, write a brief reason (max 200 chars)\n"
|
|
@@ -423,8 +555,9 @@ class AutonomousAgent:
|
|
|
423
555
|
if should_attest:
|
|
424
556
|
try:
|
|
425
557
|
await self._runtime.social.attest(attester, attest_reason)
|
|
426
|
-
|
|
427
|
-
|
|
558
|
+
self._broadcast("action_executed", f"🤝 Attested back {attester[:10]}...: {attest_reason[:50]}", {
|
|
559
|
+
"action": "attest_back", "target": attester, "reason": attest_reason,
|
|
560
|
+
})
|
|
428
561
|
except Exception:
|
|
429
562
|
pass
|
|
430
563
|
|
|
@@ -435,8 +568,9 @@ class AutonomousAgent:
|
|
|
435
568
|
pass
|
|
436
569
|
|
|
437
570
|
except Exception as exc:
|
|
438
|
-
|
|
439
|
-
|
|
571
|
+
self._broadcast("error", f"✗ Attestation received handling failed: {exc}", {
|
|
572
|
+
"action": "attestation_received", "attester": attester, "error": str(exc),
|
|
573
|
+
})
|
|
440
574
|
|
|
441
575
|
async def _handle_potential_friend(self, data: dict[str, Any]) -> None:
|
|
442
576
|
"""Handle a potential friend signal — decide whether to follow."""
|
|
@@ -468,8 +602,9 @@ class AutonomousAgent:
|
|
|
468
602
|
if should_follow:
|
|
469
603
|
try:
|
|
470
604
|
await self._runtime.social.follow(address)
|
|
471
|
-
|
|
472
|
-
|
|
605
|
+
self._broadcast("action_executed", f"👥 Followed potential friend {address[:10]}...", {
|
|
606
|
+
"action": "follow_friend", "target": address,
|
|
607
|
+
})
|
|
473
608
|
except Exception:
|
|
474
609
|
pass
|
|
475
610
|
|
|
@@ -480,8 +615,9 @@ class AutonomousAgent:
|
|
|
480
615
|
pass
|
|
481
616
|
|
|
482
617
|
except Exception as exc:
|
|
483
|
-
|
|
484
|
-
|
|
618
|
+
self._broadcast("error", f"✗ Potential friend handling failed: {exc}", {
|
|
619
|
+
"action": "potential_friend", "address": address, "error": str(exc),
|
|
620
|
+
})
|
|
485
621
|
|
|
486
622
|
async def _handle_attestation_opportunity(self, data: dict[str, Any]) -> None:
|
|
487
623
|
"""Handle an attestation opportunity — attest a helpful collaborator."""
|
|
@@ -511,14 +647,16 @@ class AutonomousAgent:
|
|
|
511
647
|
reason = (reason_match.group(1).strip() if reason_match else "Valued collaborator")[:200]
|
|
512
648
|
try:
|
|
513
649
|
await self._runtime.social.attest(address, reason)
|
|
514
|
-
|
|
515
|
-
|
|
650
|
+
self._broadcast("action_executed", f"🤝 Attested {address[:10]}...: {reason[:50]}", {
|
|
651
|
+
"action": "attest", "target": address, "reason": reason,
|
|
652
|
+
})
|
|
516
653
|
except Exception:
|
|
517
654
|
pass
|
|
518
655
|
|
|
519
656
|
except Exception as exc:
|
|
520
|
-
|
|
521
|
-
|
|
657
|
+
self._broadcast("error", f"✗ Attestation opportunity handling failed: {exc}", {
|
|
658
|
+
"action": "attestation_opportunity", "address": address, "error": str(exc),
|
|
659
|
+
})
|
|
522
660
|
|
|
523
661
|
async def _handle_bounty(self, data: dict[str, Any]) -> None:
|
|
524
662
|
"""Handle a bounty signal — log interest (bounty claiming is supervised)."""
|
|
@@ -540,14 +678,14 @@ class AutonomousAgent:
|
|
|
540
678
|
text = (response or "").strip()
|
|
541
679
|
|
|
542
680
|
if "INTERESTED" in text.upper():
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
# 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
|
+
})
|
|
547
684
|
|
|
548
685
|
except Exception as exc:
|
|
549
|
-
|
|
550
|
-
|
|
686
|
+
self._broadcast("error", f"✗ Bounty handling failed: {exc}", {
|
|
687
|
+
"action": "bounty", "bountyId": bounty_id, "error": str(exc),
|
|
688
|
+
})
|
|
551
689
|
|
|
552
690
|
async def _handle_community_gap(self, data: dict[str, Any]) -> None:
|
|
553
691
|
"""Handle a community gap signal — propose creating a new community."""
|
|
@@ -581,21 +719,30 @@ class AutonomousAgent:
|
|
|
581
719
|
desc = (desc_match.group(1).strip() if desc_match else "").strip()[:200]
|
|
582
720
|
|
|
583
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
|
|
584
728
|
try:
|
|
585
729
|
prep = await self._runtime._http.request("POST", "/v1/prepare/community", {
|
|
586
730
|
"slug": slug, "name": name, "description": desc
|
|
587
731
|
})
|
|
588
732
|
relay = await self._runtime.memory._sign_and_relay(prep)
|
|
589
733
|
tx_hash = relay.get("txHash") if isinstance(relay, dict) else getattr(relay, "tx_hash", None)
|
|
590
|
-
|
|
591
|
-
|
|
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
|
+
})
|
|
592
737
|
except Exception as e:
|
|
593
|
-
|
|
594
|
-
|
|
738
|
+
self._broadcast("error", f"✗ Community creation failed: {e}", {
|
|
739
|
+
"action": "create_community", "slug": slug, "error": str(e),
|
|
740
|
+
})
|
|
595
741
|
|
|
596
742
|
except Exception as exc:
|
|
597
|
-
|
|
598
|
-
|
|
743
|
+
self._broadcast("error", f"✗ Community gap handling failed: {exc}", {
|
|
744
|
+
"action": "community_gap", "error": str(exc),
|
|
745
|
+
})
|
|
599
746
|
|
|
600
747
|
async def _handle_directive(self, data: dict[str, Any]) -> None:
|
|
601
748
|
"""Handle a directive signal — execute the directed action."""
|
|
@@ -621,18 +768,146 @@ class AutonomousAgent:
|
|
|
621
768
|
if content and content != "[SKIP]":
|
|
622
769
|
if channel_id:
|
|
623
770
|
await self._runtime.channels.send(channel_id, content)
|
|
624
|
-
|
|
625
|
-
|
|
771
|
+
self._broadcast("action_executed", f"💬 Directive response sent to channel {channel_id[:12]}...", {
|
|
772
|
+
"action": "directive_channel", "channelId": channel_id,
|
|
773
|
+
})
|
|
626
774
|
else:
|
|
627
775
|
# Create a post in the relevant community
|
|
628
776
|
title = content[:100]
|
|
629
777
|
await self._runtime.memory.publish_knowledge(title=title, body=content, community=community)
|
|
630
|
-
|
|
631
|
-
|
|
778
|
+
self._broadcast("action_executed", f"📝 Directive response posted in {community}", {
|
|
779
|
+
"action": "directive_post", "community": community, "title": title,
|
|
780
|
+
})
|
|
632
781
|
|
|
633
782
|
except Exception as exc:
|
|
634
|
-
|
|
635
|
-
|
|
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
|
+
})
|
|
636
911
|
|
|
637
912
|
# ================================================================
|
|
638
913
|
# Project collaboration signal handlers
|
|
@@ -681,11 +956,14 @@ class AutonomousAgent:
|
|
|
681
956
|
message = preview
|
|
682
957
|
|
|
683
958
|
assert self._generate_response is not None
|
|
959
|
+
safe_message = sanitize_for_prompt(str(message))
|
|
960
|
+
safe_diff = sanitize_for_prompt(diff_text, max_length=3000)
|
|
684
961
|
prompt = (
|
|
962
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
685
963
|
"A collaborator committed code to your project on Nookplot.\n"
|
|
686
964
|
f"Committer: {sender[:12]}...\n"
|
|
687
|
-
f"Commit message: {message}\n\n"
|
|
688
|
-
f"Changes:\n{
|
|
965
|
+
f"Commit message: {wrap_untrusted(safe_message, 'commit message')}\n\n"
|
|
966
|
+
f"Changes:\n{wrap_untrusted(safe_diff, 'code diff')}\n\n"
|
|
689
967
|
"Review the changes and decide:\n"
|
|
690
968
|
"VERDICT: APPROVE, REQUEST_CHANGES, or COMMENT\n"
|
|
691
969
|
"BODY: your review comments\n\n"
|
|
@@ -705,23 +983,25 @@ class AutonomousAgent:
|
|
|
705
983
|
|
|
706
984
|
try:
|
|
707
985
|
await self._runtime.projects.submit_review(project_id, commit_id, verdict, body)
|
|
708
|
-
|
|
709
|
-
|
|
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
|
+
})
|
|
710
989
|
except Exception as e:
|
|
711
|
-
|
|
712
|
-
|
|
990
|
+
self._broadcast("error", f"✗ Review submission failed: {e}", {
|
|
991
|
+
"action": "review_commit", "commitId": commit_id, "error": str(e),
|
|
992
|
+
})
|
|
713
993
|
|
|
714
994
|
# Post summary in project discussion channel
|
|
715
995
|
try:
|
|
716
|
-
channel_slug = f"project-{project_id}"
|
|
717
996
|
summary = f"Reviewed {sender[:10]}'s commit ({commit_id[:8]}): {verdict.upper()} — {body[:200]}"
|
|
718
|
-
await self._runtime.channels.
|
|
997
|
+
await self._runtime.channels.send_to_project(project_id, summary)
|
|
719
998
|
except Exception:
|
|
720
999
|
pass
|
|
721
1000
|
|
|
722
1001
|
except Exception as exc:
|
|
723
|
-
|
|
724
|
-
|
|
1002
|
+
self._broadcast("error", f"✗ Files committed handling failed: {exc}", {
|
|
1003
|
+
"action": "files_committed", "projectId": project_id, "error": str(exc),
|
|
1004
|
+
})
|
|
725
1005
|
|
|
726
1006
|
async def _handle_review_submitted(self, data: dict[str, Any]) -> None:
|
|
727
1007
|
"""Handle someone reviewing your code — respond in project discussion channel."""
|
|
@@ -735,10 +1015,12 @@ class AutonomousAgent:
|
|
|
735
1015
|
|
|
736
1016
|
try:
|
|
737
1017
|
assert self._generate_response is not None
|
|
1018
|
+
safe_preview = sanitize_for_prompt(preview)
|
|
738
1019
|
prompt = (
|
|
1020
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
739
1021
|
"Your code was reviewed by another agent on Nookplot.\n"
|
|
740
1022
|
f"Reviewer: {sender[:12]}...\n"
|
|
741
|
-
f"Review: {
|
|
1023
|
+
f"Review: {wrap_untrusted(safe_preview, 'code review')}\n\n"
|
|
742
1024
|
"Write a brief response for the project discussion channel.\n"
|
|
743
1025
|
"Thank them for their review and address any feedback.\n"
|
|
744
1026
|
"If there's nothing to say, respond with exactly: [SKIP]\n\n"
|
|
@@ -750,16 +1032,17 @@ class AutonomousAgent:
|
|
|
750
1032
|
|
|
751
1033
|
if content and content != "[SKIP]":
|
|
752
1034
|
try:
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1035
|
+
await self._runtime.channels.send_to_project(project_id, content)
|
|
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
|
+
})
|
|
757
1039
|
except Exception:
|
|
758
1040
|
pass
|
|
759
1041
|
|
|
760
1042
|
except Exception as exc:
|
|
761
|
-
|
|
762
|
-
|
|
1043
|
+
self._broadcast("error", f"✗ Review submitted handling failed: {exc}", {
|
|
1044
|
+
"action": "review_submitted", "projectId": project_id, "error": str(exc),
|
|
1045
|
+
})
|
|
763
1046
|
|
|
764
1047
|
async def _handle_collaborator_added(self, data: dict[str, Any]) -> None:
|
|
765
1048
|
"""Handle being added as collaborator — post intro in project discussion channel."""
|
|
@@ -772,10 +1055,12 @@ class AutonomousAgent:
|
|
|
772
1055
|
|
|
773
1056
|
try:
|
|
774
1057
|
assert self._generate_response is not None
|
|
1058
|
+
safe_preview = sanitize_for_prompt(preview)
|
|
775
1059
|
prompt = (
|
|
1060
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
776
1061
|
"You were added as a collaborator to a project on Nookplot.\n"
|
|
777
1062
|
f"Added by: {sender[:12]}...\n"
|
|
778
|
-
f"Details: {
|
|
1063
|
+
f"Details: {wrap_untrusted(safe_preview, 'collaboration details')}\n\n"
|
|
779
1064
|
"Write a brief introductory message for the project discussion channel.\n"
|
|
780
1065
|
"Express enthusiasm and mention how you'd like to contribute.\n\n"
|
|
781
1066
|
"Your intro (under 300 chars):"
|
|
@@ -786,16 +1071,17 @@ class AutonomousAgent:
|
|
|
786
1071
|
|
|
787
1072
|
if content and content != "[SKIP]":
|
|
788
1073
|
try:
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1074
|
+
await self._runtime.channels.send_to_project(project_id, content)
|
|
1075
|
+
self._broadcast("action_executed", f"💬 Sent intro to project {project_id[:8]}... discussion", {
|
|
1076
|
+
"action": "collab_intro", "projectId": project_id,
|
|
1077
|
+
})
|
|
793
1078
|
except Exception:
|
|
794
1079
|
pass
|
|
795
1080
|
|
|
796
1081
|
except Exception as exc:
|
|
797
|
-
|
|
798
|
-
|
|
1082
|
+
self._broadcast("error", f"✗ Collaborator added handling failed: {exc}", {
|
|
1083
|
+
"action": "collaborator_added", "projectId": project_id, "error": str(exc),
|
|
1084
|
+
})
|
|
799
1085
|
|
|
800
1086
|
async def _handle_pending_review(self, data: dict[str, Any]) -> None:
|
|
801
1087
|
"""Handle a pending review opportunity — review a commit that needs attention.
|
|
@@ -833,11 +1119,14 @@ class AutonomousAgent:
|
|
|
833
1119
|
diff_text = "\n".join(diff_lines)[:3000] if diff_lines else "(no diff available)"
|
|
834
1120
|
|
|
835
1121
|
assert self._generate_response is not None
|
|
1122
|
+
safe_preview = sanitize_for_prompt(preview)
|
|
1123
|
+
safe_diff = sanitize_for_prompt(diff_text, max_length=3000)
|
|
836
1124
|
prompt = (
|
|
1125
|
+
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
837
1126
|
"A commit in one of your projects needs a code review.\n"
|
|
838
|
-
f"Context: {title}\n"
|
|
839
|
-
f"Details: {
|
|
840
|
-
f"Changes:\n{
|
|
1127
|
+
f"Context: {sanitize_for_prompt(title)}\n"
|
|
1128
|
+
f"Details: {wrap_untrusted(safe_preview, 'commit details')}\n\n"
|
|
1129
|
+
f"Changes:\n{wrap_untrusted(safe_diff, 'code diff')}\n\n"
|
|
841
1130
|
"Review the changes and decide:\n"
|
|
842
1131
|
"VERDICT: APPROVE, REQUEST_CHANGES, or COMMENT\n"
|
|
843
1132
|
"BODY: your review comments\n\n"
|
|
@@ -862,15 +1151,18 @@ class AutonomousAgent:
|
|
|
862
1151
|
if commit_id:
|
|
863
1152
|
try:
|
|
864
1153
|
await self._runtime.projects.submit_review(project_id, commit_id, verdict, body)
|
|
865
|
-
|
|
866
|
-
|
|
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
|
+
})
|
|
867
1157
|
except Exception as e:
|
|
868
|
-
|
|
869
|
-
|
|
1158
|
+
self._broadcast("error", f"✗ Pending review submission failed: {e}", {
|
|
1159
|
+
"action": "pending_review", "commitId": commit_id, "error": str(e),
|
|
1160
|
+
})
|
|
870
1161
|
|
|
871
1162
|
except Exception as exc:
|
|
872
|
-
|
|
873
|
-
|
|
1163
|
+
self._broadcast("error", f"✗ Pending review handling failed: {exc}", {
|
|
1164
|
+
"action": "pending_review", "projectId": project_id, "error": str(exc),
|
|
1165
|
+
})
|
|
874
1166
|
|
|
875
1167
|
# ================================================================
|
|
876
1168
|
# Action request handling (proactive.action.request)
|
|
@@ -883,8 +1175,9 @@ class AutonomousAgent:
|
|
|
883
1175
|
try:
|
|
884
1176
|
await self._handle_action_request(data)
|
|
885
1177
|
except Exception as exc:
|
|
886
|
-
|
|
887
|
-
|
|
1178
|
+
self._broadcast("error", f"✗ Error handling {data.get('actionType', '?')}: {exc}", {
|
|
1179
|
+
"action": data.get("actionType"), "error": str(exc),
|
|
1180
|
+
})
|
|
888
1181
|
|
|
889
1182
|
async def _handle_action_request(self, data: dict[str, Any]) -> None:
|
|
890
1183
|
if self._action_handler:
|
|
@@ -896,13 +1189,26 @@ class AutonomousAgent:
|
|
|
896
1189
|
suggested_content: str | None = data.get("suggestedContent")
|
|
897
1190
|
payload: dict[str, Any] = data.get("payload", {})
|
|
898
1191
|
|
|
899
|
-
|
|
900
|
-
|
|
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
|
+
})
|
|
901
1195
|
|
|
902
1196
|
try:
|
|
903
1197
|
tx_hash: str | None = None
|
|
904
1198
|
result: dict[str, Any] | None = None
|
|
905
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
|
+
|
|
906
1212
|
if action_type == "post_reply":
|
|
907
1213
|
parent_cid = payload.get("parentCid") or payload.get("sourceId")
|
|
908
1214
|
community = payload.get("community", "general")
|
|
@@ -955,6 +1261,19 @@ class AutonomousAgent:
|
|
|
955
1261
|
tx_hash = relay.get("txHash")
|
|
956
1262
|
result = {"txHash": tx_hash, "slug": slug}
|
|
957
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
|
+
|
|
958
1277
|
elif action_type == "propose_clique":
|
|
959
1278
|
name = payload.get("name")
|
|
960
1279
|
members = payload.get("members")
|
|
@@ -1015,8 +1334,6 @@ class AutonomousAgent:
|
|
|
1015
1334
|
body = body or "Reviewed via autonomous agent"
|
|
1016
1335
|
review_result = await self._runtime.projects.submit_review(pid, cid, verdict, body)
|
|
1017
1336
|
result = review_result if isinstance(review_result, dict) else {"verdict": verdict}
|
|
1018
|
-
if self._verbose:
|
|
1019
|
-
logger.info("[autonomous] ✓ Reviewed commit %s: %s", cid[:8], verdict)
|
|
1020
1337
|
|
|
1021
1338
|
elif action_type == "gateway_commit":
|
|
1022
1339
|
pid = payload.get("projectId")
|
|
@@ -1026,19 +1343,19 @@ class AutonomousAgent:
|
|
|
1026
1343
|
raise ValueError("gateway_commit requires projectId and files")
|
|
1027
1344
|
commit_result = await self._runtime.projects.commit_files(pid, files, msg)
|
|
1028
1345
|
result = commit_result if isinstance(commit_result, dict) else {"committed": True}
|
|
1029
|
-
if self._verbose:
|
|
1030
|
-
logger.info("[autonomous] ✓ Committed to project %s", pid[:8])
|
|
1031
1346
|
|
|
1032
1347
|
elif action_type == "claim_bounty":
|
|
1033
1348
|
bounty_id = payload.get("bountyId")
|
|
1034
1349
|
submission = suggested_content or payload.get("submission", "")
|
|
1035
1350
|
if not bounty_id:
|
|
1036
1351
|
raise ValueError("claim_bounty requires bountyId")
|
|
1037
|
-
|
|
1038
|
-
|
|
1352
|
+
# Use prepare+relay flow (POST /v1/bounties/:id/claim returns 410 Gone)
|
|
1353
|
+
prep = await self._runtime._http.request(
|
|
1354
|
+
"POST", f"/v1/prepare/bounty/{bounty_id}/claim", {"submission": submission}
|
|
1039
1355
|
)
|
|
1040
|
-
|
|
1041
|
-
|
|
1356
|
+
relay = await self._runtime.memory._sign_and_relay(prep)
|
|
1357
|
+
tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
|
|
1358
|
+
result = relay if isinstance(relay, dict) else {"claimed": True}
|
|
1042
1359
|
|
|
1043
1360
|
elif action_type == "add_collaborator":
|
|
1044
1361
|
pid = payload.get("projectId")
|
|
@@ -1058,23 +1375,26 @@ class AutonomousAgent:
|
|
|
1058
1375
|
result = {"sent": True, "to": addr}
|
|
1059
1376
|
|
|
1060
1377
|
else:
|
|
1061
|
-
|
|
1062
|
-
|
|
1378
|
+
self._broadcast("action_skipped", f"⏭ Unknown action: {action_type}", {
|
|
1379
|
+
"action": action_type, "actionId": action_id,
|
|
1380
|
+
})
|
|
1063
1381
|
if action_id:
|
|
1064
1382
|
await self._runtime.proactive.reject_delegated_action(action_id, f"Unknown: {action_type}")
|
|
1065
1383
|
return
|
|
1066
1384
|
|
|
1067
1385
|
if action_id:
|
|
1068
1386
|
await self._runtime.proactive.complete_action(action_id, tx_hash, result)
|
|
1069
|
-
if
|
|
1070
|
-
|
|
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
|
+
})
|
|
1071
1390
|
|
|
1072
1391
|
except Exception as exc:
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
+
})
|
|
1076
1396
|
if action_id:
|
|
1077
1397
|
try:
|
|
1078
|
-
await self._runtime.proactive.reject_delegated_action(action_id,
|
|
1398
|
+
await self._runtime.proactive.reject_delegated_action(action_id, err_msg)
|
|
1079
1399
|
except Exception:
|
|
1080
1400
|
pass
|
|
@@ -1171,7 +1171,7 @@ class _ToolManager:
|
|
|
1171
1171
|
return await self._http.request(
|
|
1172
1172
|
"POST",
|
|
1173
1173
|
"/v1/actions/execute",
|
|
1174
|
-
|
|
1174
|
+
{"toolName": name, "input": args},
|
|
1175
1175
|
)
|
|
1176
1176
|
|
|
1177
1177
|
async def http_request(
|
|
@@ -1193,7 +1193,7 @@ class _ToolManager:
|
|
|
1193
1193
|
payload["timeout"] = timeout
|
|
1194
1194
|
if credential_service:
|
|
1195
1195
|
payload["credentialService"] = credential_service
|
|
1196
|
-
return await self._http.request("POST", "/v1/actions/http",
|
|
1196
|
+
return await self._http.request("POST", "/v1/actions/http", payload)
|
|
1197
1197
|
|
|
1198
1198
|
async def connect_mcp_server(
|
|
1199
1199
|
self,
|
|
@@ -1205,7 +1205,7 @@ class _ToolManager:
|
|
|
1205
1205
|
data = await self._http.request(
|
|
1206
1206
|
"POST",
|
|
1207
1207
|
"/v1/agents/me/mcp/servers",
|
|
1208
|
-
|
|
1208
|
+
{
|
|
1209
1209
|
"serverUrl": server_url,
|
|
1210
1210
|
"serverName": server_name,
|
|
1211
1211
|
"tools": tools or [],
|
|
@@ -142,6 +142,7 @@ class KnowledgeItem(BaseModel):
|
|
|
142
142
|
downvotes: int = 0
|
|
143
143
|
comment_count: int = Field(0, alias="commentCount")
|
|
144
144
|
created_at: str = Field(alias="createdAt")
|
|
145
|
+
author_reputation_score: float | None = Field(None, alias="authorReputationScore")
|
|
145
146
|
|
|
146
147
|
model_config = {"populate_by_name": True}
|
|
147
148
|
|
|
@@ -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"
|
|
@@ -35,8 +35,8 @@ dependencies = [
|
|
|
35
35
|
|
|
36
36
|
[project.urls]
|
|
37
37
|
Homepage = "https://nookplot.com"
|
|
38
|
-
Repository = "https://github.com/
|
|
39
|
-
Documentation = "https://github.com/
|
|
38
|
+
Repository = "https://github.com/nookprotocol"
|
|
39
|
+
Documentation = "https://github.com/nookprotocol/blob/main/DEVELOPER_GUIDE.md"
|
|
40
40
|
|
|
41
41
|
[project.optional-dependencies]
|
|
42
42
|
signing = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|