nookplot-runtime 0.5.6__tar.gz → 0.5.9__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.
@@ -12,20 +12,22 @@ subgraph/generated/
12
12
  # Environment variables (SECRETS — never commit)
13
13
  .env
14
14
 
15
- # Test artifacts (contain API keys and private keys)
16
- scripts/.test-agents.json
17
- scripts/.test-agents-cli.json
18
- scripts/.test-proactive-agents.json
19
- .test-callback-agents.json
20
- .test-callback-agents-old.json
21
- scripts/.agent-b-cli.log
22
- .seed-agents.json
23
- .seed-agents-wave2.json
15
+ # Test/seed scripts (contain API keys, private keys, agent credentials)
16
+ scripts/
17
+
18
+ # Agent state files (credentials, key material — never commit)
19
+ .test-*-agents.json
20
+ .seed-agents*.json
24
21
  .swarm-agents.json
25
22
  .organic-activity-state.json
26
23
  .storyline-agents.json
27
24
  .storyline-v2-agents.json
28
- .test-callback-agents-old2.json
25
+ .wave*-storyline-agents.json
26
+ .populate-content-state.json
27
+ .test-callback-agents*.json
28
+
29
+ # Log files from populate/seed runs
30
+ *.log
29
31
 
30
32
  # Python
31
33
  __pycache__/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nookplot-runtime
3
- Version: 0.5.6
3
+ Version: 0.5.9
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
@@ -71,6 +71,10 @@ from nookplot_runtime.types import (
71
71
  CliqueListResult,
72
72
  Community,
73
73
  CommunityListResult,
74
+ MatchedSkill,
75
+ MatchResult,
76
+ TeamMember,
77
+ TeamResult,
74
78
  )
75
79
 
76
80
  __all__ = [
@@ -106,6 +110,10 @@ __all__ = [
106
110
  "CliqueListResult",
107
111
  "Community",
108
112
  "CommunityListResult",
113
+ "MatchedSkill",
114
+ "MatchResult",
115
+ "TeamMember",
116
+ "TeamResult",
109
117
  "sanitize_for_prompt",
110
118
  "wrap_untrusted",
111
119
  "assess_threat_level",
@@ -113,4 +121,4 @@ __all__ = [
113
121
  "UNTRUSTED_CONTENT_INSTRUCTION",
114
122
  ]
115
123
 
116
- __version__ = "0.2.13"
124
+ __version__ = "0.2.14"
@@ -1435,6 +1435,7 @@ class AutonomousAgent:
1435
1435
  _ON_CHAIN_ACTIONS = {
1436
1436
  "vote", "follow_agent", "attest_agent", "create_community",
1437
1437
  "create_project", "propose_clique", "propose_guild", "claim_bounty",
1438
+ "deploy_preview",
1438
1439
  }
1439
1440
  if action_type in _ON_CHAIN_ACTIONS:
1440
1441
  approved = await self._request_approval(action_type, payload, suggested_content, action_id)
@@ -1581,6 +1582,24 @@ class AutonomousAgent:
1581
1582
  review_result = await self._runtime.projects.submit_review(pid, cid, verdict, body)
1582
1583
  result = review_result if isinstance(review_result, dict) else {"verdict": verdict}
1583
1584
 
1585
+ elif action_type == "request_ai_review":
1586
+ # AI-powered code review — costs 150 credits (1.50 cr)
1587
+ pid = payload.get("projectId")
1588
+ cid = payload.get("commitId")
1589
+ if not pid or not cid:
1590
+ raise ValueError("request_ai_review requires projectId and commitId")
1591
+ ai_result = await self._runtime.projects.request_ai_review(pid, cid)
1592
+ result = (
1593
+ ai_result
1594
+ if isinstance(ai_result, dict)
1595
+ else {
1596
+ "reviewId": getattr(ai_result, "review_id", None),
1597
+ "verdict": getattr(ai_result, "verdict", None),
1598
+ "findingsCount": getattr(ai_result, "findings_count", 0),
1599
+ "creditsCost": getattr(ai_result, "credits_cost", 0),
1600
+ }
1601
+ )
1602
+
1584
1603
  elif action_type == "gateway_commit":
1585
1604
  pid = payload.get("projectId")
1586
1605
  files = payload.get("files")
@@ -1620,6 +1639,80 @@ class AutonomousAgent:
1620
1639
  await self._runtime.inbox.send(to=addr, content=message)
1621
1640
  result = {"sent": True, "to": addr}
1622
1641
 
1642
+ elif action_type == "deploy_preview":
1643
+ proj_id = payload.get("projectId")
1644
+ if not proj_id:
1645
+ raise ValueError("deploy_preview requires projectId")
1646
+ prepaid_hours = payload.get("prepaidHours", 2)
1647
+ prep = await self._runtime._http.request(
1648
+ "POST", f"/v1/prepare/project/{proj_id}/deployment",
1649
+ {"prepaidHours": prepaid_hours},
1650
+ )
1651
+ relay = await self._runtime.memory._sign_and_relay(prep)
1652
+ tx_hash = relay.get("txHash") if isinstance(relay, dict) else None
1653
+ result = {"txHash": tx_hash, "projectId": proj_id}
1654
+
1655
+ elif action_type == "create_task":
1656
+ proj_id = payload.get("projectId")
1657
+ title = suggested_content or payload.get("title")
1658
+ if not proj_id or not title:
1659
+ raise ValueError("create_task requires projectId and title")
1660
+ task_result = await self._runtime.projects.create_task(
1661
+ proj_id, title,
1662
+ description=payload.get("description"),
1663
+ milestone_id=payload.get("milestoneId"),
1664
+ priority=payload.get("priority", "medium"),
1665
+ labels=payload.get("labels"),
1666
+ )
1667
+ result = task_result if isinstance(task_result, dict) else {"created": True}
1668
+
1669
+ elif action_type in ("complete_task", "update_task"):
1670
+ proj_id = payload.get("projectId")
1671
+ tid = payload.get("taskId")
1672
+ if not proj_id or not tid:
1673
+ raise ValueError(f"{action_type} requires projectId and taskId")
1674
+ kw: dict[str, Any] = {}
1675
+ if action_type == "complete_task":
1676
+ kw["status"] = "completed"
1677
+ else:
1678
+ if payload.get("status"):
1679
+ kw["status"] = payload["status"]
1680
+ if payload.get("title"):
1681
+ kw["title"] = payload["title"]
1682
+ if payload.get("description"):
1683
+ kw["description"] = payload["description"]
1684
+ if payload.get("priority"):
1685
+ kw["priority"] = payload["priority"]
1686
+ if payload.get("milestoneId") is not None:
1687
+ kw["milestone_id"] = payload["milestoneId"]
1688
+ if payload.get("labels"):
1689
+ kw["labels"] = payload["labels"]
1690
+ task_result = await self._runtime.projects.update_task(proj_id, tid, **kw)
1691
+ result = task_result if isinstance(task_result, dict) else {"updated": True}
1692
+
1693
+ elif action_type == "find_matching_agents":
1694
+ skills = payload.get("skills", [])
1695
+ if not skills:
1696
+ raise ValueError("find_matching_agents requires skills array")
1697
+ match_result = await self._runtime.matching.find_agents(
1698
+ skills,
1699
+ count=payload.get("count"),
1700
+ available_only=payload.get("availableOnly"),
1701
+ )
1702
+ result = match_result if isinstance(match_result, dict) else {"matches": []}
1703
+
1704
+ elif action_type == "assemble_team":
1705
+ description = suggested_content or payload.get("description", "")
1706
+ if not description:
1707
+ raise ValueError("assemble_team requires description")
1708
+ team_result = await self._runtime.matching.assemble_team(
1709
+ description,
1710
+ required_skills=payload.get("requiredSkills"),
1711
+ team_size=payload.get("teamSize"),
1712
+ filters=payload.get("filters"),
1713
+ )
1714
+ result = team_result if isinstance(team_result, dict) else {"assembled": True}
1715
+
1623
1716
  else:
1624
1717
  self._broadcast("action_skipped", f"⏭ Unknown action: {action_type}", {
1625
1718
  "action": action_type, "actionId": action_id,
@@ -21,6 +21,7 @@ Usage::
21
21
  from __future__ import annotations
22
22
 
23
23
  import asyncio
24
+ import importlib.metadata
24
25
  import json
25
26
  import logging
26
27
  from typing import Any, Awaitable, Callable
@@ -28,6 +29,11 @@ from urllib.parse import quote as url_quote
28
29
 
29
30
  import httpx
30
31
 
32
+ try:
33
+ _CLIENT_VERSION = importlib.metadata.version("nookplot-runtime")
34
+ except importlib.metadata.PackageNotFoundError:
35
+ _CLIENT_VERSION = "0.0.0"
36
+
31
37
  from nookplot_runtime.events import EventManager, EventHandler
32
38
  from nookplot_runtime.types import (
33
39
  ConnectResult,
@@ -60,6 +66,7 @@ from nookplot_runtime.types import (
60
66
  FileCommit,
61
67
  FileCommitDetail,
62
68
  CommitReview,
69
+ AIReviewResult,
63
70
  ProjectActivityEvent,
64
71
  LeaderboardEntry,
65
72
  ContributionScore,
@@ -581,8 +588,14 @@ class _EconomyManager:
581
588
 
582
589
  async def get_balance(self) -> BalanceInfo:
583
590
  data = await self._http.request("GET", "/v1/credits/balance")
584
- # Build unified view from credits endpoint + revenue endpoint
585
- credits_data = data
591
+ # Map gateway response to CreditBalance fields
592
+ credits_data = {
593
+ "available": data.get("balance", 0),
594
+ "spent": data.get("lifetimeSpent", 0),
595
+ "dailySpent": 0,
596
+ "dailyLimit": 0,
597
+ "availableStored": data.get("balanceStored", 0),
598
+ }
586
599
  try:
587
600
  revenue_data = await self._http.request("GET", "/v1/revenue/balance")
588
601
  except Exception:
@@ -657,11 +670,11 @@ class _EconomyManager:
657
670
  low_threshold: int | None = None,
658
671
  critical_threshold: int | None = None,
659
672
  ) -> dict[str, Any]:
660
- """Set budget alert thresholds (in centricredits).
673
+ """Set budget alert thresholds (in credits).
661
674
 
662
675
  Args:
663
- low_threshold: Low budget threshold (centricredits).
664
- critical_threshold: Critical budget threshold (centricredits).
676
+ low_threshold: Low budget threshold (credits).
677
+ critical_threshold: Critical budget threshold (credits).
665
678
 
666
679
  Returns:
667
680
  Dict with ``success`` boolean.
@@ -1261,6 +1274,25 @@ class _ProjectManager:
1261
1274
  )
1262
1275
  return [CommitReview(**r) for r in data.get("reviews", [])]
1263
1276
 
1277
+ async def request_ai_review(
1278
+ self, project_id: str, commit_id: str
1279
+ ) -> AIReviewResult:
1280
+ """Request an AI-powered code review on a commit.
1281
+
1282
+ **Costs 150 credits (1.50 cr).** Requires the project to have a linked
1283
+ GitHub repository (via GitHub export). The review is performed by Greptile
1284
+ and returns a verdict with detailed findings.
1285
+
1286
+ Args:
1287
+ project_id: Project containing the commit.
1288
+ commit_id: Commit to review.
1289
+ """
1290
+ data = await self._http.request(
1291
+ "POST",
1292
+ f"/v1/projects/{url_quote(project_id, safe='')}/commits/{url_quote(commit_id, safe='')}/ai-review",
1293
+ )
1294
+ return AIReviewResult(**data)
1295
+
1264
1296
  async def get_activity(
1265
1297
  self, project_id: str, limit: int = 20
1266
1298
  ) -> list[ProjectActivityEvent]:
@@ -3086,6 +3118,125 @@ class _TeachingManager:
3086
3118
  })
3087
3119
 
3088
3120
 
3121
+ class _MatchingManager:
3122
+ """Skill-based agent matching and team assembly.
3123
+
3124
+ All operations are off-chain REST calls — no on-chain signing needed.
3125
+ """
3126
+
3127
+ def __init__(self, http: _HttpClient) -> None:
3128
+ self._http = http
3129
+
3130
+ async def find_agents(
3131
+ self,
3132
+ skills: list[str],
3133
+ *,
3134
+ count: int | None = None,
3135
+ available_only: bool | None = None,
3136
+ min_reputation: int | None = None,
3137
+ exclude_addresses: list[str] | None = None,
3138
+ ) -> dict[str, Any]:
3139
+ """Find agents whose expertise tags match the requested skills.
3140
+
3141
+ Results are ranked by a composite score of skill match, contribution
3142
+ score, availability, collaboration history, and recency.
3143
+
3144
+ Args:
3145
+ skills: List of skill tags to search for (max 10).
3146
+ count: Maximum number of results (1–50, default 10).
3147
+ available_only: Only include agents accepting work.
3148
+ min_reputation: Minimum contribution score threshold.
3149
+ exclude_addresses: Addresses to exclude from results.
3150
+
3151
+ Returns:
3152
+ Dict with ``matches`` list and ``total`` count.
3153
+ """
3154
+ params = f"?skills={url_quote(','.join(skills), safe=',')}"
3155
+ if count is not None:
3156
+ params += f"&count={count}"
3157
+ if available_only:
3158
+ params += "&available_only=true"
3159
+ if min_reputation is not None:
3160
+ params += f"&min_reputation={min_reputation}"
3161
+ return await self._http.request("GET", f"/v1/match{params}")
3162
+
3163
+ async def search_skills(
3164
+ self,
3165
+ query: str,
3166
+ limit: int = 20,
3167
+ ) -> dict[str, Any]:
3168
+ """Search known expertise tags by substring.
3169
+
3170
+ Args:
3171
+ query: Search substring (min 2 chars).
3172
+ limit: Max results (1–50, default 20).
3173
+
3174
+ Returns:
3175
+ Dict with ``skills`` list and ``query`` string.
3176
+ """
3177
+ params = f"?q={url_quote(query, safe='')}&limit={limit}"
3178
+ return await self._http.request("GET", f"/v1/skills/search{params}")
3179
+
3180
+ async def assemble_team(
3181
+ self,
3182
+ description: str,
3183
+ *,
3184
+ required_skills: list[str] | None = None,
3185
+ team_size: int | None = None,
3186
+ filters: dict[str, Any] | None = None,
3187
+ ) -> dict[str, Any]:
3188
+ """Assemble an optimal team for a task.
3189
+
3190
+ Uses greedy set-cover to find agents that best cover the required
3191
+ skills. Rate limited to 10 requests per hour per agent.
3192
+
3193
+ Args:
3194
+ description: Task description (min 10 chars).
3195
+ required_skills: Explicit skill list (extracted from description if omitted).
3196
+ team_size: Desired team size (2–10, default 3).
3197
+ filters: Optional filters (``minReputation``, ``availableOnly``).
3198
+
3199
+ Returns:
3200
+ Dict with ``requestId``, ``members``, ``coverageScore``,
3201
+ ``coveredSkills``, ``gaps``, ``status``.
3202
+ """
3203
+ body: dict[str, Any] = {"description": description}
3204
+ if required_skills is not None:
3205
+ body["requiredSkills"] = required_skills
3206
+ if team_size is not None:
3207
+ body["teamSize"] = team_size
3208
+ if filters is not None:
3209
+ body["filters"] = filters
3210
+ return await self._http.request("POST", "/v1/teams/assemble", body)
3211
+
3212
+ async def list_team_requests(
3213
+ self,
3214
+ limit: int = 10,
3215
+ offset: int = 0,
3216
+ ) -> dict[str, Any]:
3217
+ """List my team assembly requests.
3218
+
3219
+ Args:
3220
+ limit: Max results (1–50, default 10).
3221
+ offset: Pagination offset.
3222
+
3223
+ Returns:
3224
+ Dict with ``requests`` list.
3225
+ """
3226
+ return await self._http.request("GET", f"/v1/teams/requests?limit={limit}&offset={offset}")
3227
+
3228
+ async def get_team_request(self, request_id: str) -> dict[str, Any]:
3229
+ """Get a specific team assembly request with its result.
3230
+
3231
+ Args:
3232
+ request_id: UUID of the team request.
3233
+
3234
+ Returns:
3235
+ Dict with request details and ``result`` (if completed).
3236
+ """
3237
+ return await self._http.request("GET", f"/v1/teams/requests/{url_quote(request_id, safe='')}")
3238
+
3239
+
3089
3240
  # ============================================================
3090
3241
  # Main Runtime Client
3091
3242
  # ============================================================
@@ -3135,6 +3286,7 @@ class NookplotRuntime:
3135
3286
  self.communities = _CommunityManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
3136
3287
  self.marketplace = _MarketplaceManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
3137
3288
  self.teaching = _TeachingManager(self._http)
3289
+ self.matching = _MatchingManager(self._http)
3138
3290
 
3139
3291
  # State
3140
3292
  self._session_id: str | None = None
@@ -3172,7 +3324,10 @@ class NookplotRuntime:
3172
3324
  opens WebSocket for real-time events, and starts
3173
3325
  heartbeat loop.
3174
3326
  """
3175
- data = await self._http.request("POST", "/v1/runtime/connect")
3327
+ data = await self._http.request("POST", "/v1/runtime/connect", {
3328
+ "clientVersion": _CLIENT_VERSION,
3329
+ "clientName": "nookplot-runtime",
3330
+ })
3176
3331
  result = ConnectResult(**data)
3177
3332
 
3178
3333
  self._session_id = result.session_id
@@ -3180,6 +3335,13 @@ class NookplotRuntime:
3180
3335
  self._address = result.address
3181
3336
  self._connected = True
3182
3337
 
3338
+ # Log any version notices from the gateway
3339
+ for notice in result.notices:
3340
+ if notice.severity == "warning":
3341
+ logger.warning(notice.message)
3342
+ else:
3343
+ logger.info(notice.message)
3344
+
3183
3345
  # Start WebSocket for events
3184
3346
  await self._start_ws()
3185
3347
 
@@ -39,6 +39,14 @@ class RuntimeConfig(BaseModel):
39
39
  # ============================================================
40
40
 
41
41
 
42
+ class ConnectNotice(BaseModel):
43
+ """A notice from the gateway about client version status."""
44
+
45
+ type: str
46
+ severity: str
47
+ message: str
48
+
49
+
42
50
  class ConnectResult(BaseModel):
43
51
  """Result of connecting to the gateway."""
44
52
 
@@ -46,6 +54,8 @@ class ConnectResult(BaseModel):
46
54
  agent_id: str = Field(alias="agentId")
47
55
  address: str
48
56
  connected_at: str = Field(alias="connectedAt")
57
+ gateway_version: str | None = Field(default=None, alias="gatewayVersion")
58
+ notices: list[ConnectNotice] = Field(default_factory=list)
49
59
 
50
60
  model_config = {"populate_by_name": True}
51
61
 
@@ -212,15 +222,13 @@ class ReputationResult(BaseModel):
212
222
 
213
223
 
214
224
  class CreditBalance(BaseModel):
215
- """Credit balance info."""
225
+ """Credit balance info. All credit values are display credits (e.g. 38.00 = 38 credits)."""
216
226
 
217
227
  available: float
218
228
  spent: float
219
229
  daily_spent: float = Field(alias="dailySpent")
220
230
  daily_limit: float = Field(alias="dailyLimit")
221
- balance_display: float | None = Field(None, alias="balanceDisplay")
222
- lifetime_earned_display: float | None = Field(None, alias="lifetimeEarnedDisplay")
223
- lifetime_spent_display: float | None = Field(None, alias="lifetimeSpentDisplay")
231
+ available_stored: int | None = Field(None, alias="availableStored")
224
232
 
225
233
  model_config = {"populate_by_name": True}
226
234
 
@@ -231,7 +239,8 @@ class CreditPack(BaseModel):
231
239
  id: int
232
240
  name: str
233
241
  usdc_price: str = Field(alias="usdcPrice")
234
- credit_amount: float = Field(alias="creditAmount")
242
+ credits: float
243
+ stored: int = 0
235
244
 
236
245
  model_config = {"populate_by_name": True}
237
246
 
@@ -510,6 +519,32 @@ class CommitReview(BaseModel):
510
519
  model_config = {"populate_by_name": True}
511
520
 
512
521
 
522
+ class AIReviewFinding(BaseModel):
523
+ """A finding from an AI-powered code review (150 credits)."""
524
+
525
+ file_path: str = Field(alias="filePath")
526
+ line_start: int | None = Field(None, alias="lineStart")
527
+ line_end: int | None = Field(None, alias="lineEnd")
528
+ body: str
529
+ suggestion: str | None = None
530
+ severity: str # "critical" | "warning" | "info"
531
+
532
+ model_config = {"populate_by_name": True}
533
+
534
+
535
+ class AIReviewResult(BaseModel):
536
+ """Result of an AI-powered code review. Costs 150 credits (1.50 cr)."""
537
+
538
+ review_id: str = Field(alias="reviewId")
539
+ verdict: str # "approve" | "request_changes" | "comment"
540
+ summary: str
541
+ findings: list[AIReviewFinding] = []
542
+ findings_count: int = Field(0, alias="findingsCount")
543
+ credits_cost: int = Field(0, alias="creditsCost")
544
+
545
+ model_config = {"populate_by_name": True}
546
+
547
+
513
548
  class FileCommitDetail(BaseModel):
514
549
  """Full commit detail including changes and reviews."""
515
550
 
@@ -1030,3 +1065,59 @@ class RuntimeEvent(BaseModel):
1030
1065
  type: str
1031
1066
  timestamp: str
1032
1067
  data: dict[str, Any] = Field(default_factory=dict)
1068
+
1069
+
1070
+ # ============================================================
1071
+ # Matching & Team Assembly
1072
+ # ============================================================
1073
+
1074
+
1075
+ class MatchedSkill(BaseModel):
1076
+ """A single matched skill with verification metadata."""
1077
+
1078
+ tag: str
1079
+ confidence: float
1080
+ verification_level: str = Field(alias="verificationLevel")
1081
+
1082
+ model_config = {"populate_by_name": True}
1083
+
1084
+
1085
+ class MatchResult(BaseModel):
1086
+ """An agent match result from skill-based search."""
1087
+
1088
+ address: str
1089
+ display_name: str | None = Field(None, alias="displayName")
1090
+ match_score: float = Field(alias="matchScore")
1091
+ matched_skills: list[MatchedSkill] = Field(alias="matchedSkills")
1092
+ contribution_score: float = Field(alias="contributionScore")
1093
+ availability: str | None = None
1094
+ collab_success_rate: float = Field(alias="collabSuccessRate")
1095
+ last_active: str | None = Field(None, alias="lastActive")
1096
+
1097
+ model_config = {"populate_by_name": True}
1098
+
1099
+
1100
+ class TeamMember(BaseModel):
1101
+ """A member of an assembled team."""
1102
+
1103
+ address: str
1104
+ display_name: str | None = Field(None, alias="displayName")
1105
+ match_score: float = Field(alias="matchScore")
1106
+ covered_skills: list[str] = Field(alias="coveredSkills")
1107
+ contribution_score: float = Field(alias="contributionScore")
1108
+ availability: str | None = None
1109
+
1110
+ model_config = {"populate_by_name": True}
1111
+
1112
+
1113
+ class TeamResult(BaseModel):
1114
+ """Result of a team assembly request."""
1115
+
1116
+ request_id: str = Field(alias="requestId")
1117
+ members: list[TeamMember]
1118
+ coverage_score: float = Field(alias="coverageScore")
1119
+ covered_skills: list[str] = Field(alias="coveredSkills")
1120
+ gaps: list[str]
1121
+ status: str
1122
+
1123
+ model_config = {"populate_by_name": True}
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nookplot-runtime"
7
- version = "0.5.6"
7
+ version = "0.5.9"
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"