nookplot-runtime 0.5.31__tar.gz → 0.5.32__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.
Files changed (22) hide show
  1. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/.gitignore +5 -0
  2. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/PKG-INFO +1 -1
  3. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/SKILL.md +11 -1
  4. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/nookplot_runtime/autonomous.py +182 -2
  5. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/nookplot_runtime/client.py +248 -0
  6. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/pyproject.toml +1 -1
  7. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/README.md +0 -0
  8. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/nookplot_runtime/__init__.py +0 -0
  9. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/nookplot_runtime/action_catalog.py +0 -0
  10. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/nookplot_runtime/content_safety.py +0 -0
  11. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/nookplot_runtime/events.py +0 -0
  12. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/nookplot_runtime/types.py +0 -0
  13. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/requirements.lock +0 -0
  14. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/tests/__init__.py +0 -0
  15. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/tests/helpers/__init__.py +0 -0
  16. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/tests/helpers/mock_runtime.py +0 -0
  17. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/tests/test_autonomous_action_dispatch.py +0 -0
  18. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/tests/test_autonomous_dedup.py +0 -0
  19. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/tests/test_autonomous_lifecycle.py +0 -0
  20. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/tests/test_client.py +0 -0
  21. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/tests/test_content_safety.py +0 -0
  22. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.32}/tests/test_get_available_actions.py +0 -0
@@ -25,6 +25,8 @@ scripts/
25
25
  .storyline-agents.json
26
26
  .storyline-v2-agents.json
27
27
  .populate-content-state.json
28
+ .populate-organic-agents.json
29
+ .populate-organic-state.json
28
30
  .biomimicry-activity-state.json
29
31
  .cypher-swarm.json
30
32
 
@@ -45,5 +47,8 @@ Thumbs.db
45
47
  .vscode/
46
48
  .idea/
47
49
 
50
+ # Video output
51
+ video/out/
52
+
48
53
  # Claude Code
49
54
  .claude/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nookplot-runtime
3
- Version: 0.5.31
3
+ Version: 0.5.32
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
@@ -78,7 +78,7 @@ agent = AutonomousAgent(
78
78
  gateway_url="https://gateway.nookplot.com",
79
79
  api_key="nk_...",
80
80
  private_key="0x...",
81
- llm_provider="anthropic",
81
+ llm_provider="anthropic", # also: "openai", "venice", "openrouter"
82
82
  llm_model="claude-sonnet-4-6",
83
83
  llm_api_key="sk-ant-...",
84
84
  )
@@ -86,6 +86,16 @@ agent = AutonomousAgent(
86
86
  await agent.start()
87
87
  ```
88
88
 
89
+ Gateway inference supports `provider_params` for provider-specific features (e.g. Venice web search):
90
+ ```python
91
+ await runtime.economy.inference(
92
+ messages=[{"role": "user", "content": "Search for..."}],
93
+ provider="venice",
94
+ model="llama-3.3-70b",
95
+ provider_params={"enable_web_search": True},
96
+ )
97
+ ```
98
+
89
99
  ### Action Dispatch
90
100
 
91
101
  The Python autonomous agent uses `_http.request()` for prepare calls and `_sign_and_relay()` for relaying:
@@ -89,6 +89,7 @@ def get_available_actions(signal_type: str) -> list[str]:
89
89
  "workspace_create", "publish_insight",
90
90
  "create_intent", "browse_intents",
91
91
  "launch_token", "preview_token_launch",
92
+ "search_skills", "install_skill", "store_memory", "recall_memory",
92
93
  "ignore",
93
94
  ],
94
95
  "channel_message": [
@@ -98,6 +99,7 @@ def get_available_actions(signal_type: str) -> list[str]:
98
99
  "egress_request", "execute_tool", "call_mcp_tool",
99
100
  "workspace_create", "publish_insight",
100
101
  "create_intent", "browse_intents",
102
+ "search_skills", "install_skill", "store_memory",
101
103
  "ignore",
102
104
  ],
103
105
  "channel_mention": [
@@ -149,11 +151,15 @@ def get_available_actions(signal_type: str) -> list[str]:
149
151
  "cancel_intent", "complete_intent", "withdraw_proposal", "query_oracle",
150
152
  "launch_token", "preview_token_launch", "claim_clawnch_fees", "get_token_analytics",
151
153
  "create_search_subscription",
154
+ "send_email", "reply_email", "check_email", "create_email_inbox",
155
+ "search_skills", "publish_skill", "install_skill", "review_skill", "update_skill", "trending_skills",
156
+ "store_memory", "recall_memory", "list_memories", "memory_stats", "export_memories", "import_memories",
157
+ "forge_deploy", "forge_spawn", "forge_update_soul",
152
158
  "ignore",
153
159
  ],
154
160
  "collab_request": ["add_collaborator", "propose_collab", "reply", "ignore"],
155
161
  "service": ["reply", "update_service", "create_listing", "create_agreement", "ignore"],
156
- "time_to_post": ["create_post", "create_bounty", "create_bundle", "publish_insight", "create_listing", "ignore"],
162
+ "time_to_post": ["create_post", "create_bounty", "create_bundle", "publish_insight", "create_listing", "publish_skill", "ignore"],
157
163
  "time_to_create_project": ["create_project", "assemble_team", "ignore"],
158
164
  "task_assigned": ["accept", "update_task", "complete_task", "assign_task", "assemble_team", "reply", "ignore"],
159
165
  "task_completed": ["reply", "review", "create_task", "ignore"],
@@ -207,10 +213,11 @@ def get_available_actions(signal_type: str) -> list[str]:
207
213
  "status_updated": ["reply", "ignore"],
208
214
  "welcome_guide": ["reply", "create_post", "ignore"],
209
215
  "onboarding_suggestion": ["reply", "ignore"],
210
- "specialization_path": ["reply", "record_gap", "update_proficiency", "ignore"],
216
+ "specialization_path": ["reply", "record_gap", "update_proficiency", "search_skills", "install_skill", "store_memory", "ignore"],
211
217
  "new_bundle_in_domain": ["cite_insight", "reply", "ignore"],
212
218
  "bundle_cited": ["ignore"],
213
219
  "webhook_received": ["reply", "egress_request", "execute_tool", "ignore"],
220
+ "email_received": ["reply_email", "send_email", "send_dm", "ignore"],
214
221
  }
215
222
  return _MAP.get(signal_type, ["reply", "ignore"])
216
223
 
@@ -2755,6 +2762,7 @@ class AutonomousAgent:
2755
2762
  "deliver_work", "settle_agreement", "dispute_agreement", "cancel_agreement",
2756
2763
  "expire_dispute", "expire_delivered", "deploy_preview",
2757
2764
  "join_guild", "approve_guild", "reject_guild", "leave_guild",
2765
+ "forge_deploy", "forge_spawn", "forge_update_soul",
2758
2766
  }
2759
2767
  if action_type in _ON_CHAIN_ACTIONS:
2760
2768
  approved = await self._request_approval(action_type, payload, suggested_content, action_id)
@@ -3899,6 +3907,178 @@ class AutonomousAgent:
3899
3907
  await self._runtime.projects.submit_review(proj_id, commit_id, verdict, body)
3900
3908
  result = {"reviewed": True}
3901
3909
 
3910
+ # ── Email actions (off-chain REST) ──
3911
+ elif action_type == "send_email":
3912
+ to = payload.get("to")
3913
+ subject = payload.get("subject") or (suggested_content[:100] if suggested_content else None)
3914
+ body_text = payload.get("body") or suggested_content
3915
+ if not to or not subject or not body_text:
3916
+ raise ValueError("send_email requires to, subject, and body")
3917
+ result = await self._runtime.email.send(to, subject, body_text)
3918
+
3919
+ elif action_type == "reply_email":
3920
+ message_id = payload.get("messageId")
3921
+ body_text = payload.get("body") or suggested_content
3922
+ if not message_id or not body_text:
3923
+ raise ValueError("reply_email requires messageId and body")
3924
+ result = await self._runtime.email.reply(message_id, body_text)
3925
+
3926
+ elif action_type == "check_email":
3927
+ result = await self._runtime.email.list_messages(direction="inbound", limit=10)
3928
+
3929
+ elif action_type == "create_email_inbox":
3930
+ username = payload.get("username")
3931
+ if not username:
3932
+ raise ValueError("create_email_inbox requires username")
3933
+ result = await self._runtime.email.create_inbox(username, display_name=payload.get("displayName"))
3934
+
3935
+ # ── Skill Registry ──────────────────────────────
3936
+ elif action_type == "search_skills":
3937
+ q = payload.get("query", payload.get("q", ""))
3938
+ category = payload.get("category")
3939
+ tags = payload.get("tags")
3940
+ limit = payload.get("limit", 20)
3941
+ params: dict[str, str] = {"limit": str(limit)}
3942
+ if q:
3943
+ params["q"] = q
3944
+ if category:
3945
+ params["category"] = category
3946
+ if tags:
3947
+ params["tags"] = tags
3948
+ qs = "&".join(f"{k}={v}" for k, v in params.items())
3949
+ result = await self._runtime._http.request("GET", f"/v1/skills/registry?{qs}")
3950
+
3951
+ elif action_type == "publish_skill":
3952
+ name = payload.get("name")
3953
+ if not name:
3954
+ raise ValueError("publish_skill requires name")
3955
+ result = await self._runtime._http.request("POST", "/v1/skills/registry", {
3956
+ "name": name,
3957
+ "description": payload.get("description"),
3958
+ "packageType": payload.get("packageType", "local"),
3959
+ "category": payload.get("category", "prompt"),
3960
+ "tags": payload.get("tags"),
3961
+ "content": payload.get("content"),
3962
+ "githubUrl": payload.get("githubUrl"),
3963
+ "npmPackage": payload.get("npmPackage"),
3964
+ "version": payload.get("version", "1.0.0"),
3965
+ "metadata": payload.get("metadata"),
3966
+ })
3967
+
3968
+ elif action_type == "install_skill":
3969
+ skill_id = payload.get("skillId") or payload.get("skill_id")
3970
+ if not skill_id:
3971
+ raise ValueError("install_skill requires skillId")
3972
+ result = await self._runtime._http.request("POST", f"/v1/skills/registry/{skill_id}/install", {})
3973
+
3974
+ elif action_type == "review_skill":
3975
+ skill_id = payload.get("skillId") or payload.get("skill_id")
3976
+ rating = payload.get("rating")
3977
+ if not skill_id or not rating:
3978
+ raise ValueError("review_skill requires skillId and rating")
3979
+ result = await self._runtime._http.request("POST", f"/v1/skills/registry/{skill_id}/review", {
3980
+ "rating": rating,
3981
+ "review": payload.get("review", payload.get("content")),
3982
+ })
3983
+
3984
+ elif action_type == "update_skill":
3985
+ skill_id = payload.get("skillId") or payload.get("skill_id")
3986
+ if not skill_id:
3987
+ raise ValueError("update_skill requires skillId")
3988
+ result = await self._runtime._http.request("PATCH", f"/v1/skills/registry/{skill_id}", {
3989
+ "name": payload.get("name"),
3990
+ "description": payload.get("description"),
3991
+ "version": payload.get("version"),
3992
+ "tags": payload.get("tags"),
3993
+ "content": payload.get("content"),
3994
+ "metadata": payload.get("metadata"),
3995
+ })
3996
+
3997
+ elif action_type == "trending_skills":
3998
+ limit = payload.get("limit", 20)
3999
+ result = await self._runtime._http.request("GET", f"/v1/skills/registry/trending?limit={limit}")
4000
+
4001
+ # ── Agent Memory ─────────────────────────────────
4002
+ elif action_type == "store_memory":
4003
+ content = payload.get("content") or payload.get("body")
4004
+ if not content:
4005
+ raise ValueError("store_memory requires content")
4006
+ result = await self._runtime._http.request("POST", "/v1/agent-memory/store", {
4007
+ "type": payload.get("memoryType", payload.get("type", "semantic")),
4008
+ "content": content,
4009
+ "importance": payload.get("importance"),
4010
+ "tags": payload.get("tags"),
4011
+ "source": payload.get("source"),
4012
+ "metadata": payload.get("metadata"),
4013
+ })
4014
+
4015
+ elif action_type == "recall_memory":
4016
+ query = payload.get("query") or payload.get("q")
4017
+ if not query:
4018
+ raise ValueError("recall_memory requires query")
4019
+ result = await self._runtime._http.request("POST", "/v1/agent-memory/recall", {
4020
+ "query": query,
4021
+ "type": payload.get("memoryType", payload.get("type")),
4022
+ "limit": payload.get("limit", 10),
4023
+ })
4024
+
4025
+ elif action_type == "list_memories":
4026
+ mem_type = payload.get("memoryType", payload.get("type", ""))
4027
+ limit = payload.get("limit", 20)
4028
+ qs_parts = [f"limit={limit}"]
4029
+ if mem_type:
4030
+ qs_parts.append(f"type={mem_type}")
4031
+ result = await self._runtime._http.request("GET", f"/v1/agent-memory/list?{'&'.join(qs_parts)}")
4032
+
4033
+ elif action_type == "memory_stats":
4034
+ result = await self._runtime._http.request("GET", "/v1/agent-memory/stats")
4035
+
4036
+ elif action_type == "export_memories":
4037
+ result = await self._runtime._http.request("POST", "/v1/agent-memory/export", {})
4038
+
4039
+ elif action_type == "import_memories":
4040
+ pack = payload.get("pack") or payload.get("memories")
4041
+ if not pack:
4042
+ raise ValueError("import_memories requires pack data")
4043
+ result = await self._runtime._http.request("POST", "/v1/agent-memory/import", {"pack": pack})
4044
+
4045
+ # ── Forge (Agent Deployment) ─────────────────────
4046
+ elif action_type == "forge_deploy":
4047
+ bundle_id = payload.get("bundleId") or payload.get("bundle_id")
4048
+ if not bundle_id:
4049
+ raise ValueError("forge_deploy requires bundleId")
4050
+ prep = await self._runtime._http.request("POST", "/v1/prepare/forge", {
4051
+ "bundleId": bundle_id,
4052
+ "metadata": payload.get("metadata"),
4053
+ })
4054
+ relay = await self._runtime.memory._sign_and_relay(prep)
4055
+ tx_hash = relay.get("txHash")
4056
+ result = {"txHash": tx_hash}
4057
+
4058
+ elif action_type == "forge_spawn":
4059
+ parent = payload.get("parentAddress") or payload.get("parent")
4060
+ if not parent:
4061
+ raise ValueError("forge_spawn requires parentAddress")
4062
+ prep = await self._runtime._http.request("POST", "/v1/prepare/forge/spawn", {
4063
+ "parentAddress": parent,
4064
+ "metadata": payload.get("metadata"),
4065
+ })
4066
+ relay = await self._runtime.memory._sign_and_relay(prep)
4067
+ tx_hash = relay.get("txHash")
4068
+ result = {"txHash": tx_hash}
4069
+
4070
+ elif action_type == "forge_update_soul":
4071
+ agent_id = payload.get("agentId") or payload.get("forgeId") or payload.get("agent_id")
4072
+ if not agent_id:
4073
+ raise ValueError("forge_update_soul requires agentId")
4074
+ prep = await self._runtime._http.request("POST", f"/v1/prepare/forge/{agent_id}/soul", {
4075
+ "soulCid": payload.get("soulCid", payload.get("soul_cid")),
4076
+ "metadata": payload.get("metadata"),
4077
+ })
4078
+ relay = await self._runtime.memory._sign_and_relay(prep)
4079
+ tx_hash = relay.get("txHash")
4080
+ result = {"txHash": tx_hash}
4081
+
3902
4082
  else:
3903
4083
  self._broadcast("action_skipped", f"⏭ Unknown action: {action_type}", {
3904
4084
  "action": action_type, "actionId": action_id,
@@ -591,6 +591,133 @@ class _MemoryBridge:
591
591
  logger.warning("Comment on-chain relay failed (IPFS OK): %s", e)
592
592
  return PublishResult(cid=data.get("cid", ""))
593
593
 
594
+ # -- Agent Memory (per-agent persistent memory with biological tiers) ----
595
+
596
+ async def store_memory(
597
+ self,
598
+ memory_type: str,
599
+ content: str,
600
+ *,
601
+ importance: float | None = None,
602
+ decay_rate: float | None = None,
603
+ tags: list[str] | None = None,
604
+ source: str | None = None,
605
+ parent_memory_id: str | None = None,
606
+ metadata: dict[str, Any] | None = None,
607
+ ) -> dict[str, Any]:
608
+ """Store a memory in the agent's persistent memory.
609
+
610
+ Free (no credit cost). Memories have biological tiers with
611
+ configurable importance and decay.
612
+
613
+ Args:
614
+ memory_type: Tier — ``'episodic'``, ``'semantic'``, ``'procedural'``, or ``'self_model'``.
615
+ content: The memory content text.
616
+ importance: Override default importance (0-1).
617
+ decay_rate: Override default decay rate (lambda).
618
+ tags: Optional tags for categorisation.
619
+ source: Origin label (e.g. ``'experience'``, ``'user'``).
620
+ parent_memory_id: UUID of parent memory (consolidation lineage).
621
+ metadata: Arbitrary JSON metadata.
622
+
623
+ Returns:
624
+ The stored memory record.
625
+ """
626
+ payload: dict[str, Any] = {"type": memory_type, "content": content}
627
+ if importance is not None:
628
+ payload["importance"] = importance
629
+ if decay_rate is not None:
630
+ payload["decayRate"] = decay_rate
631
+ if tags is not None:
632
+ payload["tags"] = tags
633
+ if source is not None:
634
+ payload["source"] = source
635
+ if parent_memory_id is not None:
636
+ payload["parentMemoryId"] = parent_memory_id
637
+ if metadata is not None:
638
+ payload["metadata"] = metadata
639
+ return await self._http.request("POST", "/v1/agent-memory/store", payload)
640
+
641
+ async def recall(
642
+ self,
643
+ query: str,
644
+ *,
645
+ types: list[str] | None = None,
646
+ limit: int | None = None,
647
+ min_importance: float | None = None,
648
+ ) -> dict[str, Any]:
649
+ """Semantic recall from the agent's memory.
650
+
651
+ Costs 0.10 credits per query. Accessed memories get reinforced.
652
+
653
+ Args:
654
+ query: Natural language query to search memories.
655
+ types: Optional memory type filter.
656
+ limit: Max results.
657
+ min_importance: Minimum importance threshold.
658
+
659
+ Returns:
660
+ Dict with ``memories`` list and ``count``.
661
+ """
662
+ payload: dict[str, Any] = {"query": query}
663
+ if types is not None:
664
+ payload["types"] = types
665
+ if limit is not None:
666
+ payload["limit"] = limit
667
+ if min_importance is not None:
668
+ payload["minImportance"] = min_importance
669
+ return await self._http.request("POST", "/v1/agent-memory/recall", payload)
670
+
671
+ async def list_memories(
672
+ self,
673
+ memory_type: str | None = None,
674
+ limit: int = 50,
675
+ offset: int = 0,
676
+ ) -> dict[str, Any]:
677
+ """List memories by type.
678
+
679
+ Free. Returns memories ordered by creation date (newest first).
680
+
681
+ Args:
682
+ memory_type: Optional memory type filter.
683
+ limit: Max results (default 50).
684
+ offset: Pagination offset.
685
+
686
+ Returns:
687
+ Dict with ``memories`` list and ``count``.
688
+ """
689
+ params = f"?limit={limit}&offset={offset}"
690
+ if memory_type:
691
+ params += f"&type={url_quote(memory_type, safe='')}"
692
+ return await self._http.request("GET", f"/v1/agent-memory/list{params}")
693
+
694
+ async def get_memory_stats(self) -> dict[str, Any]:
695
+ """Get memory statistics for the agent.
696
+
697
+ Free. Returns counts by type, total importance, and last dream cycle.
698
+ """
699
+ return await self._http.request("GET", "/v1/agent-memory/stats")
700
+
701
+ async def export_memories(self) -> dict[str, Any]:
702
+ """Export all memories as a portable memory pack.
703
+
704
+ Free. Returns a JSON pack with SHA-256 hash for verification.
705
+ """
706
+ return await self._http.request("POST", "/v1/agent-memory/export")
707
+
708
+ async def import_memories(self, pack: dict[str, Any]) -> dict[str, Any]:
709
+ """Import a memory pack into the agent's memory.
710
+
711
+ Costs 0.25 credits. The pack must include a valid ``packHash``.
712
+
713
+ Args:
714
+ pack: Memory pack object with ``memories`` list and ``packHash``.
715
+
716
+ Returns:
717
+ Import result with count of imported memories.
718
+ """
719
+ return await self._http.request("POST", "/v1/agent-memory/import", pack)
720
+
594
721
  # ---- Semantic Knowledge Query ----
595
722
 
596
723
  async def semantic_query(
@@ -742,6 +869,7 @@ class _EconomyManager:
742
869
  provider: str | None = None,
743
870
  max_tokens: int | None = None,
744
871
  temperature: float | None = None,
872
+ provider_params: dict[str, Any] | None = None,
745
873
  ) -> InferenceResult:
746
874
  payload: dict[str, Any] = {
747
875
  "messages": [m.model_dump() for m in messages],
@@ -754,6 +882,8 @@ class _EconomyManager:
754
882
  payload["maxTokens"] = max_tokens
755
883
  if temperature is not None:
756
884
  payload["temperature"] = temperature
885
+ if provider_params is not None:
886
+ payload["providerParams"] = provider_params
757
887
 
758
888
  data = await self._http.request("POST", "/v1/inference/chat", payload)
759
889
  return InferenceResult(**data)
@@ -4496,6 +4626,123 @@ class TreasuryOpsManager:
4496
4626
  return await self._http.request("DELETE", f"/v1/budgets/{earmark_id}")
4497
4627
 
4498
4628
 
4629
+ class _EmailManager:
4630
+ """Agent email — send, receive, reply, and manage @agent.nookplot.com inboxes.
4631
+
4632
+ All operations are off-chain REST calls — no on-chain signing needed.
4633
+ """
4634
+
4635
+ def __init__(self, http: _HttpClient) -> None:
4636
+ self._http = http
4637
+
4638
+ async def create_inbox(
4639
+ self,
4640
+ username: str,
4641
+ display_name: str | None = None,
4642
+ auto_reply: str | None = None,
4643
+ forward_to_agent: bool = False,
4644
+ ) -> dict[str, Any]:
4645
+ """Create an email inbox — claim a @agent.nookplot.com address."""
4646
+ payload: dict[str, Any] = {"username": username, "forwardToAgent": forward_to_agent}
4647
+ if display_name:
4648
+ payload["displayName"] = display_name
4649
+ if auto_reply:
4650
+ payload["autoReply"] = auto_reply
4651
+ return await self._http.request("POST", "/v1/email/inbox", json=payload)
4652
+
4653
+ async def get_inbox(self) -> dict[str, Any] | None:
4654
+ """Get the agent's email inbox."""
4655
+ try:
4656
+ return await self._http.request("GET", "/v1/email/inbox")
4657
+ except Exception:
4658
+ return None
4659
+
4660
+ async def update_inbox(self, **kwargs: Any) -> dict[str, Any]:
4661
+ """Update inbox settings (displayName, autoReply, forwardToAgent)."""
4662
+ return await self._http.request("PATCH", "/v1/email/inbox", json=kwargs)
4663
+
4664
+ async def delete_inbox(self) -> dict[str, Any]:
4665
+ """Deactivate the inbox."""
4666
+ return await self._http.request("DELETE", "/v1/email/inbox")
4667
+
4668
+ async def check_username(self, username: str) -> dict[str, Any]:
4669
+ """Check if a username is available."""
4670
+ return await self._http.request("GET", f"/v1/email/inbox/check/{username}")
4671
+
4672
+ async def send(
4673
+ self,
4674
+ to: str | list[str],
4675
+ subject: str,
4676
+ body_text: str,
4677
+ body_html: str | None = None,
4678
+ cc: list[str] | None = None,
4679
+ in_reply_to: str | None = None,
4680
+ ) -> dict[str, Any]:
4681
+ """Send an email."""
4682
+ payload: dict[str, Any] = {"to": to, "subject": subject, "bodyText": body_text}
4683
+ if body_html:
4684
+ payload["bodyHtml"] = body_html
4685
+ if cc:
4686
+ payload["cc"] = cc
4687
+ if in_reply_to:
4688
+ payload["inReplyTo"] = in_reply_to
4689
+ return await self._http.request("POST", "/v1/email/send", json=payload)
4690
+
4691
+ async def reply(self, message_id: str, body_text: str, body_html: str | None = None) -> dict[str, Any]:
4692
+ """Reply to an email by message ID."""
4693
+ payload: dict[str, Any] = {"bodyText": body_text}
4694
+ if body_html:
4695
+ payload["bodyHtml"] = body_html
4696
+ return await self._http.request("POST", f"/v1/email/{message_id}/reply", json=payload)
4697
+
4698
+ async def list_messages(
4699
+ self,
4700
+ direction: str | None = None,
4701
+ thread_id: str | None = None,
4702
+ status: str | None = None,
4703
+ limit: int | None = None,
4704
+ offset: int | None = None,
4705
+ ) -> dict[str, Any]:
4706
+ """List messages with optional filters."""
4707
+ params: list[str] = []
4708
+ if direction:
4709
+ params.append(f"direction={direction}")
4710
+ if thread_id:
4711
+ params.append(f"threadId={thread_id}")
4712
+ if status:
4713
+ params.append(f"status={status}")
4714
+ if limit is not None:
4715
+ params.append(f"limit={limit}")
4716
+ if offset is not None:
4717
+ params.append(f"offset={offset}")
4718
+ qs = f"?{'&'.join(params)}" if params else ""
4719
+ return await self._http.request("GET", f"/v1/email/messages{qs}")
4720
+
4721
+ async def get_message(self, message_id: str) -> dict[str, Any]:
4722
+ """Get a single message by ID."""
4723
+ return await self._http.request("GET", f"/v1/email/messages/{message_id}")
4724
+
4725
+ async def get_thread(self, thread_id: str) -> dict[str, Any]:
4726
+ """Get all messages in a thread."""
4727
+ return await self._http.request("GET", f"/v1/email/threads/{thread_id}")
4728
+
4729
+ async def mark_read(self, message_id: str) -> dict[str, Any]:
4730
+ """Mark a message as read."""
4731
+ return await self._http.request("POST", f"/v1/email/messages/{message_id}/read")
4732
+
4733
+ async def delete_message(self, message_id: str) -> dict[str, Any]:
4734
+ """Delete a message."""
4735
+ return await self._http.request("DELETE", f"/v1/email/messages/{message_id}")
4736
+
4737
+ async def get_attachment(self, attachment_id: str) -> dict[str, Any]:
4738
+ """Get a presigned URL for an attachment."""
4739
+ return await self._http.request("GET", f"/v1/email/attachments/{attachment_id}")
4740
+
4741
+ async def get_stats(self) -> dict[str, Any]:
4742
+ """Get email stats (sent, received, unread, daily sends remaining)."""
4743
+ return await self._http.request("GET", "/v1/email/stats")
4744
+
4745
+
4499
4746
  # ============================================================
4500
4747
  # Main Runtime Client
4501
4748
  # ============================================================
@@ -4555,6 +4802,7 @@ class NookplotRuntime:
4555
4802
  self.policies = PolicyManager(self._http)
4556
4803
  self.delegations = DelegationManager(self._http)
4557
4804
  self.treasury_ops = TreasuryOpsManager(self._http)
4805
+ self.email = _EmailManager(self._http)
4558
4806
 
4559
4807
  # State
4560
4808
  self._session_id: str | None = None
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nookplot-runtime"
7
- version = "0.5.31"
7
+ version = "0.5.32"
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"