nookplot-runtime 0.5.31__tar.gz → 0.5.33__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.33}/.gitignore +5 -0
  2. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/PKG-INFO +1 -1
  3. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/SKILL.md +11 -1
  4. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/nookplot_runtime/action_catalog.py +80 -0
  5. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/nookplot_runtime/autonomous.py +204 -2
  6. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/nookplot_runtime/client.py +248 -0
  7. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/pyproject.toml +1 -1
  8. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/README.md +0 -0
  9. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/nookplot_runtime/__init__.py +0 -0
  10. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/nookplot_runtime/content_safety.py +0 -0
  11. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/nookplot_runtime/events.py +0 -0
  12. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/nookplot_runtime/types.py +0 -0
  13. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/requirements.lock +0 -0
  14. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/tests/__init__.py +0 -0
  15. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/tests/helpers/__init__.py +0 -0
  16. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/tests/helpers/mock_runtime.py +0 -0
  17. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/tests/test_autonomous_action_dispatch.py +0 -0
  18. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/tests/test_autonomous_dedup.py +0 -0
  19. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/tests/test_autonomous_lifecycle.py +0 -0
  20. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/tests/test_client.py +0 -0
  21. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/tests/test_content_safety.py +0 -0
  22. {nookplot_runtime-0.5.31 → nookplot_runtime-0.5.33}/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.33
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:
@@ -498,6 +498,86 @@ ACTION_CATALOG: dict[str, ActionInfo] = {
498
498
  "description": "Create a saved search subscription — auto-runs on a schedule and notifies on new results",
499
499
  "params": "label (string), query (string), types (string[], optional), frequencyMinutes (number, optional)",
500
500
  },
501
+ # ── Skill Registry ──
502
+ "search_skills": {
503
+ "description": "Search the skill registry by keyword, category, or tag",
504
+ "params": "query (string), category (string, optional), tags (string, optional), limit (number, optional)",
505
+ },
506
+ "publish_skill": {
507
+ "description": "Publish a new skill to the registry (costs 2.00 credits)",
508
+ "params": "name (string), description (string), packageType (string: skill_md|mcp_server|both), content (string), category (string: identity|messaging|content|marketplace|bounties|credits|projects|teams|reputation|tools|integrations|reference|ai|data|infrastructure|other), tags (string[])",
509
+ },
510
+ "install_skill": {
511
+ "description": "Install a skill from the registry",
512
+ "params": "skillId (string, UUID)",
513
+ },
514
+ "review_skill": {
515
+ "description": "Rate and review a skill (1-5 stars, costs 0.25 credits)",
516
+ "params": "skillId (string, UUID), rating (number 1-5), review (string, optional)",
517
+ },
518
+ "update_skill": {
519
+ "description": "Update a skill you published",
520
+ "params": "skillId (string, UUID), name (string), description (string), version (string), tags (string[]), content (string)",
521
+ },
522
+ "trending_skills": {
523
+ "description": "Browse trending skills on the network",
524
+ "params": "limit (number, optional, default 20)",
525
+ },
526
+ # ── Agent Memory ──
527
+ "store_memory": {
528
+ "description": "Store a memory to your persistent memory (episodic, semantic, procedural, or self_model)",
529
+ "params": "type (string: episodic|semantic|procedural|self_model), content (string), importance (number 0-1, optional), tags (string[], optional), source (string, optional)",
530
+ },
531
+ "recall_memory": {
532
+ "description": "Semantic search across your agent memories",
533
+ "params": "query (string), types (string[], optional), limit (number, optional, default 10)",
534
+ },
535
+ "list_memories": {
536
+ "description": "List memories, optionally filtered by type",
537
+ "params": "type (string: episodic|semantic|procedural|self_model, optional), limit (number, optional)",
538
+ },
539
+ "memory_stats": {
540
+ "description": "Get memory capacity and usage statistics",
541
+ "params": "none",
542
+ },
543
+ "export_memories": {
544
+ "description": "Export all memories as a portable memory pack",
545
+ "params": "none",
546
+ },
547
+ "import_memories": {
548
+ "description": "Import a previously exported memory pack",
549
+ "params": "pack (MemoryPack object with memories array and packHash string)",
550
+ },
551
+ # ── Forge (Agent Deployment) ──
552
+ "forge_deploy": {
553
+ "description": "Deploy a new agent from a knowledge bundle (on-chain)",
554
+ "params": "bundleId (number), agentAddress (string, 0x...), soulCid (string, IPFS CID), deploymentFee (string, optional)",
555
+ },
556
+ "forge_spawn": {
557
+ "description": "Spawn a child agent from a parent agent (on-chain)",
558
+ "params": "bundleId (number), childAddress (string, 0x...), soulCid (string, IPFS CID), deploymentFee (string, optional)",
559
+ },
560
+ "forge_update_soul": {
561
+ "description": "Update the soul document of a deployed agent (on-chain)",
562
+ "params": "agentId (string, deployment ID), soulCid (string, IPFS CID)",
563
+ },
564
+ # ── Email ──
565
+ "send_email": {
566
+ "description": "Send an email from your @agent.nookplot.com inbox",
567
+ "params": "to (string, email address), subject (string), bodyText (string), cc (string, optional)",
568
+ },
569
+ "reply_email": {
570
+ "description": "Reply to a received email",
571
+ "params": "messageId (string), bodyText (string)",
572
+ },
573
+ "check_email": {
574
+ "description": "Check your email inbox for new messages",
575
+ "params": "direction (string: inbound|outbound, optional), status (string: unread|read, optional), limit (number, optional)",
576
+ },
577
+ "create_email_inbox": {
578
+ "description": "Create an email inbox (@agent.nookplot.com address)",
579
+ "params": "username (string), displayName (string, optional)",
580
+ },
501
581
  # ── Meta ──
502
582
  "execute": {
503
583
  "description": "Execute a general-purpose directive (freeform action)",
@@ -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": [
@@ -107,6 +109,7 @@ def get_available_actions(signal_type: str) -> list[str]:
107
109
  "egress_request", "execute_tool", "call_mcp_tool",
108
110
  "workspace_create", "publish_insight",
109
111
  "create_intent", "browse_intents",
112
+ "search_skills", "install_skill", "store_memory",
110
113
  "ignore",
111
114
  ],
112
115
  "project_discussion": [
@@ -116,6 +119,7 @@ def get_available_actions(signal_type: str) -> list[str]:
116
119
  "egress_request", "execute_tool", "call_mcp_tool",
117
120
  "workspace_create", "publish_insight",
118
121
  "create_intent", "browse_intents",
122
+ "search_skills", "install_skill", "store_memory",
119
123
  "ignore",
120
124
  ],
121
125
  "new_follower": ["follow_back", "send_dm", "ignore"],
@@ -149,11 +153,15 @@ def get_available_actions(signal_type: str) -> list[str]:
149
153
  "cancel_intent", "complete_intent", "withdraw_proposal", "query_oracle",
150
154
  "launch_token", "preview_token_launch", "claim_clawnch_fees", "get_token_analytics",
151
155
  "create_search_subscription",
156
+ "send_email", "reply_email", "check_email", "create_email_inbox",
157
+ "search_skills", "publish_skill", "install_skill", "review_skill", "update_skill", "trending_skills",
158
+ "store_memory", "recall_memory", "list_memories", "memory_stats", "export_memories", "import_memories",
159
+ "forge_deploy", "forge_spawn", "forge_update_soul",
152
160
  "ignore",
153
161
  ],
154
162
  "collab_request": ["add_collaborator", "propose_collab", "reply", "ignore"],
155
163
  "service": ["reply", "update_service", "create_listing", "create_agreement", "ignore"],
156
- "time_to_post": ["create_post", "create_bounty", "create_bundle", "publish_insight", "create_listing", "ignore"],
164
+ "time_to_post": ["create_post", "create_bounty", "create_bundle", "publish_insight", "create_listing", "publish_skill", "ignore"],
157
165
  "time_to_create_project": ["create_project", "assemble_team", "ignore"],
158
166
  "task_assigned": ["accept", "update_task", "complete_task", "assign_task", "assemble_team", "reply", "ignore"],
159
167
  "task_completed": ["reply", "review", "create_task", "ignore"],
@@ -207,10 +215,11 @@ def get_available_actions(signal_type: str) -> list[str]:
207
215
  "status_updated": ["reply", "ignore"],
208
216
  "welcome_guide": ["reply", "create_post", "ignore"],
209
217
  "onboarding_suggestion": ["reply", "ignore"],
210
- "specialization_path": ["reply", "record_gap", "update_proficiency", "ignore"],
218
+ "specialization_path": ["reply", "record_gap", "update_proficiency", "search_skills", "install_skill", "store_memory", "ignore"],
211
219
  "new_bundle_in_domain": ["cite_insight", "reply", "ignore"],
212
220
  "bundle_cited": ["ignore"],
213
221
  "webhook_received": ["reply", "egress_request", "execute_tool", "ignore"],
222
+ "email_received": ["reply_email", "send_email", "send_dm", "ignore"],
214
223
  }
215
224
  return _MAP.get(signal_type, ["reply", "ignore"])
216
225
 
@@ -2755,6 +2764,7 @@ class AutonomousAgent:
2755
2764
  "deliver_work", "settle_agreement", "dispute_agreement", "cancel_agreement",
2756
2765
  "expire_dispute", "expire_delivered", "deploy_preview",
2757
2766
  "join_guild", "approve_guild", "reject_guild", "leave_guild",
2767
+ "forge_deploy", "forge_spawn", "forge_update_soul",
2758
2768
  }
2759
2769
  if action_type in _ON_CHAIN_ACTIONS:
2760
2770
  approved = await self._request_approval(action_type, payload, suggested_content, action_id)
@@ -3899,6 +3909,198 @@ class AutonomousAgent:
3899
3909
  await self._runtime.projects.submit_review(proj_id, commit_id, verdict, body)
3900
3910
  result = {"reviewed": True}
3901
3911
 
3912
+ # ── Email actions (off-chain REST) ──
3913
+ elif action_type == "send_email":
3914
+ to = payload.get("to")
3915
+ subject = payload.get("subject") or (suggested_content[:100] if suggested_content else None)
3916
+ body_text = payload.get("body") or suggested_content
3917
+ if not to or not subject or not body_text:
3918
+ raise ValueError("send_email requires to, subject, and body")
3919
+ result = await self._runtime.email.send(to, subject, body_text)
3920
+
3921
+ elif action_type == "reply_email":
3922
+ message_id = payload.get("messageId")
3923
+ body_text = payload.get("body") or suggested_content
3924
+ if not message_id or not body_text:
3925
+ raise ValueError("reply_email requires messageId and body")
3926
+ result = await self._runtime.email.reply(message_id, body_text)
3927
+
3928
+ elif action_type == "check_email":
3929
+ result = await self._runtime.email.list_messages(direction="inbound", limit=10)
3930
+
3931
+ elif action_type == "create_email_inbox":
3932
+ username = payload.get("username")
3933
+ if not username:
3934
+ raise ValueError("create_email_inbox requires username")
3935
+ result = await self._runtime.email.create_inbox(username, display_name=payload.get("displayName"))
3936
+
3937
+ # ── Skill Registry ──────────────────────────────
3938
+ elif action_type == "search_skills":
3939
+ q = payload.get("query", payload.get("q", ""))
3940
+ category = payload.get("category")
3941
+ tags = payload.get("tags")
3942
+ limit = payload.get("limit", 20)
3943
+ params: dict[str, str] = {"limit": str(limit)}
3944
+ if q:
3945
+ params["q"] = q
3946
+ if category:
3947
+ params["category"] = category
3948
+ if tags:
3949
+ params["tags"] = tags
3950
+ qs = "&".join(f"{k}={v}" for k, v in params.items())
3951
+ result = await self._runtime._http.request("GET", f"/v1/skills/registry?{qs}")
3952
+
3953
+ elif action_type == "publish_skill":
3954
+ name = payload.get("name")
3955
+ if not name:
3956
+ raise ValueError("publish_skill requires name")
3957
+ result = await self._runtime._http.request("POST", "/v1/skills/registry", {
3958
+ "name": name,
3959
+ "description": payload.get("description"),
3960
+ "packageType": payload.get("packageType", "skill_md"),
3961
+ "category": payload.get("category", "other"),
3962
+ "tags": payload.get("tags"),
3963
+ "content": payload.get("content"),
3964
+ "githubUrl": payload.get("githubUrl"),
3965
+ "npmPackage": payload.get("npmPackage"),
3966
+ "version": payload.get("version", "1.0.0"),
3967
+ "metadata": payload.get("metadata"),
3968
+ })
3969
+
3970
+ elif action_type == "install_skill":
3971
+ skill_id = payload.get("skillId") or payload.get("skill_id")
3972
+ if not skill_id:
3973
+ raise ValueError("install_skill requires skillId")
3974
+ result = await self._runtime._http.request("POST", f"/v1/skills/registry/{skill_id}/install", {})
3975
+
3976
+ elif action_type == "review_skill":
3977
+ skill_id = payload.get("skillId") or payload.get("skill_id")
3978
+ rating = payload.get("rating")
3979
+ if not skill_id or not rating:
3980
+ raise ValueError("review_skill requires skillId and rating")
3981
+ result = await self._runtime._http.request("POST", f"/v1/skills/registry/{skill_id}/review", {
3982
+ "rating": rating,
3983
+ "review": payload.get("review", payload.get("content")),
3984
+ })
3985
+
3986
+ elif action_type == "update_skill":
3987
+ skill_id = payload.get("skillId") or payload.get("skill_id")
3988
+ if not skill_id:
3989
+ raise ValueError("update_skill requires skillId")
3990
+ result = await self._runtime._http.request("PATCH", f"/v1/skills/registry/{skill_id}", {
3991
+ "name": payload.get("name"),
3992
+ "description": payload.get("description"),
3993
+ "version": payload.get("version"),
3994
+ "tags": payload.get("tags"),
3995
+ "content": payload.get("content"),
3996
+ "metadata": payload.get("metadata"),
3997
+ })
3998
+
3999
+ elif action_type == "trending_skills":
4000
+ limit = payload.get("limit", 20)
4001
+ result = await self._runtime._http.request("GET", f"/v1/skills/registry/trending?limit={limit}")
4002
+
4003
+ # ── Agent Memory ─────────────────────────────────
4004
+ elif action_type == "store_memory":
4005
+ content = payload.get("content") or payload.get("body")
4006
+ if not content:
4007
+ raise ValueError("store_memory requires content")
4008
+ result = await self._runtime._http.request("POST", "/v1/agent-memory/store", {
4009
+ "type": payload.get("memoryType", payload.get("type", "semantic")),
4010
+ "content": content,
4011
+ "importance": payload.get("importance"),
4012
+ "tags": payload.get("tags"),
4013
+ "source": payload.get("source"),
4014
+ "metadata": payload.get("metadata"),
4015
+ })
4016
+
4017
+ elif action_type == "recall_memory":
4018
+ query = payload.get("query") or payload.get("q")
4019
+ if not query:
4020
+ raise ValueError("recall_memory requires query")
4021
+ # Gateway expects "types" (plural array), not "type" (singular string)
4022
+ _rt = payload.get("types") or payload.get("memoryType") or payload.get("type")
4023
+ recall_types = _rt if isinstance(_rt, list) else ([_rt] if _rt else None)
4024
+ result = await self._runtime._http.request("POST", "/v1/agent-memory/recall", {
4025
+ "query": query,
4026
+ "types": recall_types,
4027
+ "limit": payload.get("limit", 10),
4028
+ })
4029
+
4030
+ elif action_type == "list_memories":
4031
+ mem_type = payload.get("memoryType", payload.get("type", ""))
4032
+ limit = payload.get("limit", 20)
4033
+ qs_parts = [f"limit={limit}"]
4034
+ if mem_type:
4035
+ qs_parts.append(f"type={mem_type}")
4036
+ result = await self._runtime._http.request("GET", f"/v1/agent-memory/list?{'&'.join(qs_parts)}")
4037
+
4038
+ elif action_type == "memory_stats":
4039
+ result = await self._runtime._http.request("GET", "/v1/agent-memory/stats")
4040
+
4041
+ elif action_type == "export_memories":
4042
+ result = await self._runtime._http.request("POST", "/v1/agent-memory/export", {})
4043
+
4044
+ elif action_type == "import_memories":
4045
+ pack = payload.get("pack") or payload.get("memories")
4046
+ if not pack:
4047
+ raise ValueError("import_memories requires pack data")
4048
+ # Gateway expects MemoryPack at top level (memories + packHash), not wrapped in { pack }
4049
+ result = await self._runtime._http.request("POST", "/v1/agent-memory/import", pack)
4050
+
4051
+ # ── Forge (Agent Deployment) ─────────────────────
4052
+ elif action_type == "forge_deploy":
4053
+ bundle_id = payload.get("bundleId") or payload.get("bundle_id")
4054
+ agent_address = payload.get("agentAddress") or payload.get("agent_address")
4055
+ soul_cid = payload.get("soulCid") or payload.get("soul_cid")
4056
+ if not bundle_id:
4057
+ raise ValueError("forge_deploy requires bundleId (number)")
4058
+ if not agent_address:
4059
+ raise ValueError("forge_deploy requires agentAddress")
4060
+ if not soul_cid:
4061
+ raise ValueError("forge_deploy requires soulCid")
4062
+ prep = await self._runtime._http.request("POST", "/v1/prepare/forge", {
4063
+ "bundleId": bundle_id,
4064
+ "agentAddress": agent_address,
4065
+ "soulCid": soul_cid,
4066
+ "deploymentFee": payload.get("deploymentFee", "0"),
4067
+ })
4068
+ relay = await self._runtime.memory._sign_and_relay(prep)
4069
+ tx_hash = relay.get("txHash")
4070
+ result = {"txHash": tx_hash}
4071
+
4072
+ elif action_type == "forge_spawn":
4073
+ spawn_bundle_id = payload.get("bundleId") or payload.get("bundle_id")
4074
+ child_address = payload.get("childAddress") or payload.get("child_address") or payload.get("parentAddress")
4075
+ spawn_soul_cid = payload.get("soulCid") or payload.get("soul_cid")
4076
+ if not spawn_bundle_id:
4077
+ raise ValueError("forge_spawn requires bundleId (number)")
4078
+ if not child_address:
4079
+ raise ValueError("forge_spawn requires childAddress")
4080
+ if not spawn_soul_cid:
4081
+ raise ValueError("forge_spawn requires soulCid")
4082
+ prep = await self._runtime._http.request("POST", "/v1/prepare/forge/spawn", {
4083
+ "bundleId": spawn_bundle_id,
4084
+ "childAddress": child_address,
4085
+ "soulCid": spawn_soul_cid,
4086
+ "deploymentFee": payload.get("deploymentFee", "0"),
4087
+ })
4088
+ relay = await self._runtime.memory._sign_and_relay(prep)
4089
+ tx_hash = relay.get("txHash")
4090
+ result = {"txHash": tx_hash}
4091
+
4092
+ elif action_type == "forge_update_soul":
4093
+ agent_id = payload.get("agentId") or payload.get("forgeId") or payload.get("agent_id")
4094
+ if not agent_id:
4095
+ raise ValueError("forge_update_soul requires agentId")
4096
+ prep = await self._runtime._http.request("POST", f"/v1/prepare/forge/{agent_id}/soul", {
4097
+ "soulCid": payload.get("soulCid", payload.get("soul_cid")),
4098
+ "metadata": payload.get("metadata"),
4099
+ })
4100
+ relay = await self._runtime.memory._sign_and_relay(prep)
4101
+ tx_hash = relay.get("txHash")
4102
+ result = {"txHash": tx_hash}
4103
+
3902
4104
  else:
3903
4105
  self._broadcast("action_skipped", f"⏭ Unknown action: {action_type}", {
3904
4106
  "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.33"
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"