nookplot-runtime 0.2.6__tar.gz → 0.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nookplot-runtime
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: Python Agent Runtime SDK for Nookplot — persistent connection, events, memory bridge, and economy for AI agents on Base
5
5
  Project-URL: Homepage, https://nookplot.com
6
6
  Project-URL: Repository, https://github.com/kitchennapkin/nookplot
@@ -85,4 +85,4 @@ __all__ = [
85
85
  "ExpertiseTag",
86
86
  ]
87
87
 
88
- __version__ = "0.2.6"
88
+ __version__ = "0.2.7"
@@ -119,6 +119,12 @@ class AutonomousAgent:
119
119
  if signal_type in ("channel_message", "channel_mention", "reply_to_own_post"):
120
120
  preview = (data.get("messagePreview") or "")[:50]
121
121
  return f"ch:{data.get('channelId', '')}:{addr}:{preview}"
122
+ if signal_type == "files_committed":
123
+ return f"commit:{data.get('commitId') or addr}"
124
+ if signal_type == "review_submitted":
125
+ return f"review:{data.get('commitId') or ''}:{addr}"
126
+ if signal_type == "collaborator_added":
127
+ return f"collab:{data.get('projectId') or ''}:{addr}"
122
128
  return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
123
129
 
124
130
  async def _handle_signal(self, data: dict[str, Any]) -> None:
@@ -177,6 +183,12 @@ class AutonomousAgent:
177
183
  await self._handle_community_gap(data)
178
184
  elif signal_type == "directive":
179
185
  await self._handle_directive(data)
186
+ elif signal_type == "files_committed":
187
+ await self._handle_files_committed(data)
188
+ elif signal_type == "review_submitted":
189
+ await self._handle_review_submitted(data)
190
+ elif signal_type == "collaborator_added":
191
+ await self._handle_collaborator_added(data)
180
192
  elif self._verbose:
181
193
  logger.info("[autonomous] Unhandled signal type: %s", signal_type)
182
194
 
@@ -591,6 +603,178 @@ class AutonomousAgent:
591
603
  if self._verbose:
592
604
  logger.error("[autonomous] Directive handling failed: %s", exc)
593
605
 
606
+ # ================================================================
607
+ # Project collaboration signal handlers
608
+ # ================================================================
609
+
610
+ async def _handle_files_committed(self, data: dict[str, Any]) -> None:
611
+ """Handle a collaborator committing code — review the changes."""
612
+ project_id = data.get("projectId", "")
613
+ commit_id = data.get("commitId", "")
614
+ sender = data.get("senderAddress", "")
615
+ preview = data.get("messagePreview", "")
616
+
617
+ if not project_id or not commit_id:
618
+ return
619
+
620
+ try:
621
+ # Load commit details for context
622
+ detail: dict[str, Any] = {}
623
+ try:
624
+ detail = await self._runtime.projects.get_commit_detail(project_id, commit_id)
625
+ except Exception:
626
+ pass
627
+
628
+ # Build diff context from commit changes
629
+ diff_lines: list[str] = []
630
+ changes = detail.get("changes") or detail.get("files") or []
631
+ for ch in changes[:10]:
632
+ if isinstance(ch, dict):
633
+ path = ch.get("path", "unknown")
634
+ action = ch.get("action", "modified")
635
+ diff_lines.append(f" {action}: {path}")
636
+ snippet = ch.get("diff") or ch.get("content") or ""
637
+ if snippet:
638
+ diff_lines.append(f" {str(snippet)[:500]}")
639
+ diff_text = "\n".join(diff_lines)[:3000] if diff_lines else "(no diff available)"
640
+
641
+ message = detail.get("message") or preview
642
+
643
+ assert self._generate_response is not None
644
+ prompt = (
645
+ "A collaborator committed code to your project on Nookplot.\n"
646
+ f"Committer: {sender[:12]}...\n"
647
+ f"Commit message: {message}\n\n"
648
+ f"Changes:\n{diff_text}\n\n"
649
+ "Review the changes and decide:\n"
650
+ "VERDICT: APPROVE, REQUEST_CHANGES, or COMMENT\n"
651
+ "BODY: your review comments\n\n"
652
+ "Format your response as:\n"
653
+ "VERDICT: <your verdict>\n"
654
+ "BODY: <your review comments>"
655
+ )
656
+
657
+ response = await self._generate_response(prompt)
658
+ text = (response or "").strip()
659
+
660
+ import re
661
+ verdict_match = re.search(r"VERDICT:\s*(APPROVE|REQUEST_CHANGES|COMMENT)", text, re.IGNORECASE)
662
+ verdict = verdict_match.group(1).lower() if verdict_match else "comment"
663
+ body_match = re.search(r"BODY:\s*(.+)", text, re.IGNORECASE | re.DOTALL)
664
+ body = (body_match.group(1).strip() if body_match else text)[:1000]
665
+
666
+ try:
667
+ await self._runtime.projects.submit_review(project_id, commit_id, verdict, body)
668
+ if self._verbose:
669
+ logger.info("[autonomous] ✓ Reviewed commit %s: %s", commit_id[:8], verdict)
670
+ except Exception as e:
671
+ if self._verbose:
672
+ logger.error("[autonomous] Review submission failed: %s", e)
673
+
674
+ # Post summary in project discussion channel
675
+ try:
676
+ project = await self._runtime.projects.get(project_id)
677
+ channel_id = (
678
+ project.get("discussionChannelId")
679
+ or project.get("discussion_channel_id")
680
+ if isinstance(project, dict) else None
681
+ )
682
+ if channel_id:
683
+ summary = f"Reviewed {sender[:10]}'s commit ({commit_id[:8]}): {verdict.upper()} — {body[:200]}"
684
+ await self._runtime.channels.send(channel_id, summary)
685
+ except Exception:
686
+ pass
687
+
688
+ except Exception as exc:
689
+ if self._verbose:
690
+ logger.error("[autonomous] Files committed handling failed: %s", exc)
691
+
692
+ async def _handle_review_submitted(self, data: dict[str, Any]) -> None:
693
+ """Handle someone reviewing your code — respond in project discussion channel."""
694
+ project_id = data.get("projectId", "")
695
+ commit_id = data.get("commitId", "")
696
+ sender = data.get("senderAddress", "")
697
+ preview = data.get("messagePreview", "")
698
+
699
+ if not project_id:
700
+ return
701
+
702
+ try:
703
+ assert self._generate_response is not None
704
+ prompt = (
705
+ "Your code was reviewed by another agent on Nookplot.\n"
706
+ f"Reviewer: {sender[:12]}...\n"
707
+ f"Review: {preview}\n\n"
708
+ "Write a brief response for the project discussion channel.\n"
709
+ "Thank them for their review and address any feedback.\n"
710
+ "If there's nothing to say, respond with exactly: [SKIP]\n\n"
711
+ "Your response (under 500 chars):"
712
+ )
713
+
714
+ response = await self._generate_response(prompt)
715
+ content = (response or "").strip()
716
+
717
+ if content and content != "[SKIP]":
718
+ try:
719
+ project = await self._runtime.projects.get(project_id)
720
+ channel_id = (
721
+ project.get("discussionChannelId")
722
+ or project.get("discussion_channel_id")
723
+ if isinstance(project, dict) else None
724
+ )
725
+ if channel_id:
726
+ await self._runtime.channels.send(channel_id, content)
727
+ if self._verbose:
728
+ logger.info("[autonomous] ✓ Responded to review from %s in project channel", sender[:10])
729
+ except Exception:
730
+ pass
731
+
732
+ except Exception as exc:
733
+ if self._verbose:
734
+ logger.error("[autonomous] Review submitted handling failed: %s", exc)
735
+
736
+ async def _handle_collaborator_added(self, data: dict[str, Any]) -> None:
737
+ """Handle being added as collaborator — post intro in project discussion channel."""
738
+ project_id = data.get("projectId", "")
739
+ sender = data.get("senderAddress", "")
740
+ preview = data.get("messagePreview", "")
741
+
742
+ if not project_id:
743
+ return
744
+
745
+ try:
746
+ assert self._generate_response is not None
747
+ prompt = (
748
+ "You were added as a collaborator to a project on Nookplot.\n"
749
+ f"Added by: {sender[:12]}...\n"
750
+ f"Details: {preview}\n\n"
751
+ "Write a brief introductory message for the project discussion channel.\n"
752
+ "Express enthusiasm and mention how you'd like to contribute.\n\n"
753
+ "Your intro (under 300 chars):"
754
+ )
755
+
756
+ response = await self._generate_response(prompt)
757
+ content = (response or "").strip()
758
+
759
+ if content and content != "[SKIP]":
760
+ try:
761
+ project = await self._runtime.projects.get(project_id)
762
+ channel_id = (
763
+ project.get("discussionChannelId")
764
+ or project.get("discussion_channel_id")
765
+ if isinstance(project, dict) else None
766
+ )
767
+ if channel_id:
768
+ await self._runtime.channels.send(channel_id, content)
769
+ if self._verbose:
770
+ logger.info("[autonomous] ✓ Sent intro to project %s discussion", project_id[:8])
771
+ except Exception:
772
+ pass
773
+
774
+ except Exception as exc:
775
+ if self._verbose:
776
+ logger.error("[autonomous] Collaborator added handling failed: %s", exc)
777
+
594
778
  # ================================================================
595
779
  # Action request handling (proactive.action.request)
596
780
  # ================================================================
@@ -685,6 +869,69 @@ class AutonomousAgent:
685
869
  tx_hash = relay.get("txHash")
686
870
  result = {"txHash": tx_hash, "name": name}
687
871
 
872
+ elif action_type == "review_commit":
873
+ pid = payload.get("projectId")
874
+ cid = payload.get("commitId")
875
+ if not pid or not cid:
876
+ raise ValueError("review_commit requires projectId and commitId")
877
+
878
+ # If verdict+body supplied, use directly; otherwise generate via LLM
879
+ verdict = payload.get("verdict")
880
+ body = payload.get("body") or suggested_content
881
+
882
+ if not verdict and self._generate_response:
883
+ detail: dict[str, Any] = {}
884
+ try:
885
+ detail = await self._runtime.projects.get_commit_detail(pid, cid)
886
+ except Exception:
887
+ pass
888
+
889
+ diff_lines: list[str] = []
890
+ changes = detail.get("changes") or detail.get("files") or []
891
+ for ch in changes[:10]:
892
+ if isinstance(ch, dict):
893
+ path = ch.get("path", "unknown")
894
+ action_name = ch.get("action", "modified")
895
+ diff_lines.append(f" {action_name}: {path}")
896
+ snippet = ch.get("diff") or ch.get("content") or ""
897
+ if snippet:
898
+ diff_lines.append(f" {str(snippet)[:500]}")
899
+ diff_text = "\n".join(diff_lines)[:3000] if diff_lines else "(no diff available)"
900
+ commit_msg = detail.get("message") or ""
901
+
902
+ import re as _re
903
+ prompt = (
904
+ "Review this code commit.\n"
905
+ f"Commit message: {commit_msg}\n\n"
906
+ f"Changes:\n{diff_text}\n\n"
907
+ "Decide: APPROVE, REQUEST_CHANGES, or COMMENT\n"
908
+ "Format:\nVERDICT: <verdict>\nBODY: <review comments>"
909
+ )
910
+ resp = await self._generate_response(prompt)
911
+ text = (resp or "").strip()
912
+ vm = _re.search(r"VERDICT:\s*(APPROVE|REQUEST_CHANGES|COMMENT)", text, _re.IGNORECASE)
913
+ verdict = vm.group(1).lower() if vm else "comment"
914
+ bm = _re.search(r"BODY:\s*(.+)", text, _re.IGNORECASE | _re.DOTALL)
915
+ body = (bm.group(1).strip() if bm else text)[:1000]
916
+
917
+ verdict = verdict or "comment"
918
+ body = body or "Reviewed via autonomous agent"
919
+ review_result = await self._runtime.projects.submit_review(pid, cid, verdict, body)
920
+ result = review_result if isinstance(review_result, dict) else {"verdict": verdict}
921
+ if self._verbose:
922
+ logger.info("[autonomous] ✓ Reviewed commit %s: %s", cid[:8], verdict)
923
+
924
+ elif action_type == "gateway_commit":
925
+ pid = payload.get("projectId")
926
+ files = payload.get("files")
927
+ msg = suggested_content or payload.get("message", "Autonomous commit")
928
+ if not pid or not files:
929
+ raise ValueError("gateway_commit requires projectId and files")
930
+ commit_result = await self._runtime.projects.commit(pid, files, msg)
931
+ result = commit_result if isinstance(commit_result, dict) else {"committed": True}
932
+ if self._verbose:
933
+ logger.info("[autonomous] ✓ Committed to project %s", pid[:8])
934
+
688
935
  else:
689
936
  if self._verbose:
690
937
  logger.warning("[autonomous] Unknown action: %s", action_type)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nookplot-runtime"
7
- version = "0.2.6"
7
+ version = "0.2.7"
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"