pynydus 0.0.4__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.
- pynydus-0.0.4/PKG-INFO +138 -0
- pynydus-0.0.4/README.md +118 -0
- pynydus-0.0.4/pynydus/__init__.py +33 -0
- pynydus-0.0.4/pynydus/agents/__init__.py +1 -0
- pynydus-0.0.4/pynydus/agents/letta/README.md +46 -0
- pynydus-0.0.4/pynydus/agents/letta/__init__.py +1 -0
- pynydus-0.0.4/pynydus/agents/letta/hatcher.py +230 -0
- pynydus-0.0.4/pynydus/agents/letta/spawner.py +658 -0
- pynydus-0.0.4/pynydus/agents/openclaw/README.md +33 -0
- pynydus-0.0.4/pynydus/agents/openclaw/__init__.py +1 -0
- pynydus-0.0.4/pynydus/agents/openclaw/hatcher.py +152 -0
- pynydus-0.0.4/pynydus/agents/openclaw/spawner.py +221 -0
- pynydus-0.0.4/pynydus/agents/zeroclaw/README.md +36 -0
- pynydus-0.0.4/pynydus/agents/zeroclaw/__init__.py +1 -0
- pynydus-0.0.4/pynydus/agents/zeroclaw/hatcher.py +155 -0
- pynydus-0.0.4/pynydus/agents/zeroclaw/spawner.py +349 -0
- pynydus-0.0.4/pynydus/api/__init__.py +4 -0
- pynydus-0.0.4/pynydus/api/errors.py +34 -0
- pynydus-0.0.4/pynydus/api/raw_types.py +65 -0
- pynydus-0.0.4/pynydus/api/schemas.py +347 -0
- pynydus-0.0.4/pynydus/api/skill_format.py +222 -0
- pynydus-0.0.4/pynydus/client/__init__.py +5 -0
- pynydus-0.0.4/pynydus/client/client.py +265 -0
- pynydus-0.0.4/pynydus/cmd/__init__.py +1 -0
- pynydus-0.0.4/pynydus/cmd/main.py +666 -0
- pynydus-0.0.4/pynydus/engine/__init__.py +1 -0
- pynydus-0.0.4/pynydus/engine/differ.py +154 -0
- pynydus-0.0.4/pynydus/engine/hatcher.py +307 -0
- pynydus-0.0.4/pynydus/engine/merger.py +213 -0
- pynydus-0.0.4/pynydus/engine/nydusfile.py +400 -0
- pynydus-0.0.4/pynydus/engine/packager.py +413 -0
- pynydus-0.0.4/pynydus/engine/pipeline.py +642 -0
- pynydus-0.0.4/pynydus/engine/refinement.py +501 -0
- pynydus-0.0.4/pynydus/engine/validator.py +79 -0
- pynydus-0.0.4/pynydus/pkg/__init__.py +1 -0
- pynydus-0.0.4/pynydus/pkg/config.py +90 -0
- pynydus-0.0.4/pynydus/pkg/connector_utils.py +113 -0
- pynydus-0.0.4/pynydus/pkg/credential_scanner.py +185 -0
- pynydus-0.0.4/pynydus/pkg/llm.py +170 -0
- pynydus-0.0.4/pynydus/pkg/presidio.py +296 -0
- pynydus-0.0.4/pynydus/pkg/signing.py +183 -0
- pynydus-0.0.4/pynydus/remote/__init__.py +5 -0
- pynydus-0.0.4/pynydus/remote/registry.py +342 -0
- pynydus-0.0.4/pyproject.toml +70 -0
pynydus-0.0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pynydus
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Portable state transport for AI agents
|
|
5
|
+
Author: Nydus Team
|
|
6
|
+
Author-email: Nydus Team <contact@nydus.ag>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Dist: cryptography>=43.0.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
Requires-Dist: typer>=0.15
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Requires-Dist: presidio-analyzer>=2.2
|
|
13
|
+
Requires-Dist: presidio-anonymizer>=2.2
|
|
14
|
+
Requires-Dist: instructor>=1.0
|
|
15
|
+
Requires-Dist: anthropic>=0.40
|
|
16
|
+
Requires-Dist: pyyaml>=6.0
|
|
17
|
+
Requires-Dist: tomli>=2.0 ; python_full_version < '3.11'
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src="https://raw.githubusercontent.com/NydusAI/nydus/main/assets/logo.png" alt="Nydus" width="420">
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<strong>Portable state transport for AI agents</strong><br>
|
|
27
|
+
Transport agent state between any two frameworks without manual migration
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<a href="https://pypi.org/project/pynydus/"><img src="https://img.shields.io/pypi/v/pynydus?color=blue" alt="PyPI"></a>
|
|
32
|
+
<a href="https://pypi.org/project/pynydus/"><img src="https://img.shields.io/pypi/pyversions/pynydus" alt="Python"></a>
|
|
33
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
|
|
34
|
+
<a href="#status"><img src="https://img.shields.io/badge/status-early%20development-orange" alt="Status"></a>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<a href="#install">Install</a> •
|
|
39
|
+
<a href="#how-it-works">How it works</a> •
|
|
40
|
+
<a href="#cli">CLI</a> •
|
|
41
|
+
<a href="#python-sdk">Python SDK</a>
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
- **Spawn**: read source agent artifacts, redact PII, and package everything into a portable **Egg** (`.egg` archive)
|
|
49
|
+
- **Hatch**: decode an Egg into any supported target runtime, with optional LLM refinement and secret injection
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Source artifacts → Spawn → Egg (.egg) → Hatch → Target-native files
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### The Egg format
|
|
56
|
+
|
|
57
|
+
An `.egg` file is a signed ZIP archive containing:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
manifest.json # metadata, source type, versions
|
|
61
|
+
memory.json # labeled memory records
|
|
62
|
+
secrets.json # PII placeholders + secret requirements
|
|
63
|
+
skills/<slug>/SKILL.md # portable skill definitions (agentskills.io)
|
|
64
|
+
mcp/<server>.json # MCP server configs
|
|
65
|
+
raw/... # redacted source files (for pass-through hatch)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install pynydus
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## CLI
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Create a Nydusfile in your project directory
|
|
78
|
+
cat > Nydusfile << 'EOF'
|
|
79
|
+
SOURCE openclaw ./my-agent/
|
|
80
|
+
REDACT pii
|
|
81
|
+
EOF
|
|
82
|
+
|
|
83
|
+
# Spawn an egg
|
|
84
|
+
nydus spawn -o agent.egg
|
|
85
|
+
|
|
86
|
+
# Inspect and validate
|
|
87
|
+
nydus inspect agent.egg
|
|
88
|
+
nydus inspect agent.egg --secrets --logs
|
|
89
|
+
nydus validate agent.egg
|
|
90
|
+
|
|
91
|
+
# Generate a template .env from the egg's secret requirements
|
|
92
|
+
nydus env agent.egg -o agent.env
|
|
93
|
+
|
|
94
|
+
# Hatch into a target runtime
|
|
95
|
+
nydus hatch agent.egg --target letta --secrets agent.env
|
|
96
|
+
|
|
97
|
+
# Compare two eggs
|
|
98
|
+
nydus diff v1.egg v2.egg
|
|
99
|
+
|
|
100
|
+
# Signing
|
|
101
|
+
nydus keygen
|
|
102
|
+
nydus spawn -o signed.egg # auto-signs if key exists
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Python SDK
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from pynydus.client import Nydus
|
|
109
|
+
|
|
110
|
+
ny = Nydus()
|
|
111
|
+
|
|
112
|
+
# Spawn from a Nydusfile in the current directory
|
|
113
|
+
egg_path = ny.spawn()
|
|
114
|
+
|
|
115
|
+
# Hatch into a target runtime with secret injection
|
|
116
|
+
result = ny.hatch(egg_path, target="letta", secrets="agent.env")
|
|
117
|
+
print(result.output_dir, result.files_created)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Project structure
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
pynydus/
|
|
124
|
+
api/ # Egg data model, schemas, errors
|
|
125
|
+
agents/ # Per-platform spawners + hatchers
|
|
126
|
+
openclaw/
|
|
127
|
+
zeroclaw/
|
|
128
|
+
letta/
|
|
129
|
+
engine/ # Core pipelines: spawn, hatch, pack, validate, diff, refine
|
|
130
|
+
cmd/ # Typer CLI
|
|
131
|
+
client/ # Python SDK
|
|
132
|
+
pkg/ # Utilities: presidio, LLM, signing, config
|
|
133
|
+
remote/ # Nest registry client
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
pynydus-0.0.4/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/NydusAI/nydus/main/assets/logo.png" alt="Nydus" width="420">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<strong>Portable state transport for AI agents</strong><br>
|
|
7
|
+
Transport agent state between any two frameworks without manual migration
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="https://pypi.org/project/pynydus/"><img src="https://img.shields.io/pypi/v/pynydus?color=blue" alt="PyPI"></a>
|
|
12
|
+
<a href="https://pypi.org/project/pynydus/"><img src="https://img.shields.io/pypi/pyversions/pynydus" alt="Python"></a>
|
|
13
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
|
|
14
|
+
<a href="#status"><img src="https://img.shields.io/badge/status-early%20development-orange" alt="Status"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="#install">Install</a> •
|
|
19
|
+
<a href="#how-it-works">How it works</a> •
|
|
20
|
+
<a href="#cli">CLI</a> •
|
|
21
|
+
<a href="#python-sdk">Python SDK</a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
- **Spawn**: read source agent artifacts, redact PII, and package everything into a portable **Egg** (`.egg` archive)
|
|
29
|
+
- **Hatch**: decode an Egg into any supported target runtime, with optional LLM refinement and secret injection
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Source artifacts → Spawn → Egg (.egg) → Hatch → Target-native files
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### The Egg format
|
|
36
|
+
|
|
37
|
+
An `.egg` file is a signed ZIP archive containing:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
manifest.json # metadata, source type, versions
|
|
41
|
+
memory.json # labeled memory records
|
|
42
|
+
secrets.json # PII placeholders + secret requirements
|
|
43
|
+
skills/<slug>/SKILL.md # portable skill definitions (agentskills.io)
|
|
44
|
+
mcp/<server>.json # MCP server configs
|
|
45
|
+
raw/... # redacted source files (for pass-through hatch)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install pynydus
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## CLI
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Create a Nydusfile in your project directory
|
|
58
|
+
cat > Nydusfile << 'EOF'
|
|
59
|
+
SOURCE openclaw ./my-agent/
|
|
60
|
+
REDACT pii
|
|
61
|
+
EOF
|
|
62
|
+
|
|
63
|
+
# Spawn an egg
|
|
64
|
+
nydus spawn -o agent.egg
|
|
65
|
+
|
|
66
|
+
# Inspect and validate
|
|
67
|
+
nydus inspect agent.egg
|
|
68
|
+
nydus inspect agent.egg --secrets --logs
|
|
69
|
+
nydus validate agent.egg
|
|
70
|
+
|
|
71
|
+
# Generate a template .env from the egg's secret requirements
|
|
72
|
+
nydus env agent.egg -o agent.env
|
|
73
|
+
|
|
74
|
+
# Hatch into a target runtime
|
|
75
|
+
nydus hatch agent.egg --target letta --secrets agent.env
|
|
76
|
+
|
|
77
|
+
# Compare two eggs
|
|
78
|
+
nydus diff v1.egg v2.egg
|
|
79
|
+
|
|
80
|
+
# Signing
|
|
81
|
+
nydus keygen
|
|
82
|
+
nydus spawn -o signed.egg # auto-signs if key exists
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Python SDK
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from pynydus.client import Nydus
|
|
89
|
+
|
|
90
|
+
ny = Nydus()
|
|
91
|
+
|
|
92
|
+
# Spawn from a Nydusfile in the current directory
|
|
93
|
+
egg_path = ny.spawn()
|
|
94
|
+
|
|
95
|
+
# Hatch into a target runtime with secret injection
|
|
96
|
+
result = ny.hatch(egg_path, target="letta", secrets="agent.env")
|
|
97
|
+
print(result.output_dir, result.files_created)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Project structure
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
pynydus/
|
|
104
|
+
api/ # Egg data model, schemas, errors
|
|
105
|
+
agents/ # Per-platform spawners + hatchers
|
|
106
|
+
openclaw/
|
|
107
|
+
zeroclaw/
|
|
108
|
+
letta/
|
|
109
|
+
engine/ # Core pipelines: spawn, hatch, pack, validate, diff, refine
|
|
110
|
+
cmd/ # Typer CLI
|
|
111
|
+
client/ # Python SDK
|
|
112
|
+
pkg/ # Utilities: presidio, LLM, signing, config
|
|
113
|
+
remote/ # Nest registry client
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""pynydus — Portable state transport for AI agents."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
EGG_SPEC_VERSION = "2.0"
|
|
5
|
+
|
|
6
|
+
from pynydus.api.schemas import (
|
|
7
|
+
DiffReport,
|
|
8
|
+
Egg,
|
|
9
|
+
HatchResult,
|
|
10
|
+
Manifest,
|
|
11
|
+
McpServerConfig,
|
|
12
|
+
MemoryRecord,
|
|
13
|
+
SecretRecord,
|
|
14
|
+
SkillRecord,
|
|
15
|
+
SpawnAttachments,
|
|
16
|
+
ValidationReport,
|
|
17
|
+
)
|
|
18
|
+
from pynydus.client.client import Nydus
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"DiffReport",
|
|
22
|
+
"EGG_SPEC_VERSION",
|
|
23
|
+
"Egg",
|
|
24
|
+
"HatchResult",
|
|
25
|
+
"Manifest",
|
|
26
|
+
"McpServerConfig",
|
|
27
|
+
"MemoryRecord",
|
|
28
|
+
"Nydus",
|
|
29
|
+
"SecretRecord",
|
|
30
|
+
"SkillRecord",
|
|
31
|
+
"SpawnAttachments",
|
|
32
|
+
"ValidationReport",
|
|
33
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agent connectors — one sub-package per framework (spawner + hatcher)."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Letta Agent
|
|
2
|
+
|
|
3
|
+
## Spawner: source files → Egg
|
|
4
|
+
|
|
5
|
+
Supports both directory-based and SQLite database inputs.
|
|
6
|
+
|
|
7
|
+
### Directory mode
|
|
8
|
+
|
|
9
|
+
| Source file | Egg field | Label |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `agent_state.json` → `system` | `MemoryRecord` | `FLOW` |
|
|
12
|
+
| `agent_state.json` → `memory.persona` | `MemoryRecord` | `PERSONA` |
|
|
13
|
+
| `agent_state.json` → `memory.human` | `MemoryRecord` | `CONTEXT` |
|
|
14
|
+
| `agent_state.json` → `memory.<other>` | `MemoryRecord` | `CONTEXT` |
|
|
15
|
+
| `agent_state.json` → `tools[]` | `SkillRecord` | |
|
|
16
|
+
| `system_prompt.md` / `.txt` | `MemoryRecord` (fallback if no system in state) | `FLOW` |
|
|
17
|
+
| `archival_memory.json` | `MemoryRecord` | `STATE` |
|
|
18
|
+
| `archival/*` | `MemoryRecord` | `STATE` |
|
|
19
|
+
| `tools/*.py` | `SkillRecord` | |
|
|
20
|
+
| `.letta/config.json` | `SecretRecord` | |
|
|
21
|
+
|
|
22
|
+
### Database mode (`agent.db`)
|
|
23
|
+
|
|
24
|
+
| DB table | Egg field | Label |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `blocks` (persona) | `MemoryRecord` | `PERSONA` |
|
|
27
|
+
| `blocks` (human) | `MemoryRecord` | `CONTEXT` |
|
|
28
|
+
| `blocks` (system) | `MemoryRecord` | `FLOW` |
|
|
29
|
+
| `blocks` (other) | `MemoryRecord` | `CONTEXT` |
|
|
30
|
+
| `archival_memory` | `MemoryRecord` | `STATE` |
|
|
31
|
+
| `tools` | `SkillRecord` | |
|
|
32
|
+
| `agents` → config JSON | `SecretRecord` | |
|
|
33
|
+
|
|
34
|
+
Detection: directory with `agent_state.json`, `.letta/`, `tools/`, or
|
|
35
|
+
`agent.db`, or a `.db` file directly.
|
|
36
|
+
|
|
37
|
+
## Hatcher: Egg → target files
|
|
38
|
+
|
|
39
|
+
| Egg field | Target file | Notes |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `MemoryRecord` (`PERSONA`) | `agent_state.json` → `memory.persona` | Appended to block value |
|
|
42
|
+
| `MemoryRecord` (`CONTEXT`) | `agent_state.json` → `memory.human` | Appended to block value |
|
|
43
|
+
| `MemoryRecord` (`FLOW`) | `agent_state.json` → `system` + `system_prompt.md` | Duplicate for convenience |
|
|
44
|
+
| `MemoryRecord` (`STATE`) | `archival_memory.json` | Array of `{text, timestamp}` |
|
|
45
|
+
| `SkillRecord` | `tools/<name>.py` + `agent_state.json` → `tools[]` | Source code in both |
|
|
46
|
+
| `SecretRecord` (`CREDENTIAL`) | `.letta/config.json` | `{name: placeholder}` |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Letta agent connector."""
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Letta hatcher connector. Spec §10.3.
|
|
2
|
+
|
|
3
|
+
Produces a valid Letta agent project directory from an Egg:
|
|
4
|
+
- agent_state.json <- memory blocks + agent config skeleton
|
|
5
|
+
- tools/ <- skill records as Python tool files
|
|
6
|
+
- archival_memory.json <- state memory
|
|
7
|
+
- system_prompt.md <- flow memory
|
|
8
|
+
|
|
9
|
+
All 4 MemoryLabel values have explicit mappings:
|
|
10
|
+
- PERSONA -> memory.persona block in agent_state.json
|
|
11
|
+
- CONTEXT -> memory.human block in agent_state.json
|
|
12
|
+
- FLOW -> system field + system_prompt.md
|
|
13
|
+
- STATE -> archival_memory.json
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from pynydus.api.errors import HatchError
|
|
22
|
+
from pynydus.api.raw_types import RenderResult
|
|
23
|
+
from pynydus.api.schemas import (
|
|
24
|
+
Egg,
|
|
25
|
+
HatchResult,
|
|
26
|
+
MemoryLabel,
|
|
27
|
+
SecretKind,
|
|
28
|
+
ValidationIssue,
|
|
29
|
+
ValidationReport,
|
|
30
|
+
)
|
|
31
|
+
from pynydus.pkg.connector_utils import skill_to_filename as _skill_to_filename
|
|
32
|
+
|
|
33
|
+
_LABEL_TO_BLOCK: dict[MemoryLabel, str] = {
|
|
34
|
+
MemoryLabel.PERSONA: "persona",
|
|
35
|
+
MemoryLabel.CONTEXT: "human",
|
|
36
|
+
MemoryLabel.FLOW: "system",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LettaHatcher:
|
|
41
|
+
"""Produce a valid Letta agent directory from an Egg."""
|
|
42
|
+
|
|
43
|
+
def render(self, egg: Egg) -> RenderResult:
|
|
44
|
+
"""Render Egg records into target file contents.
|
|
45
|
+
|
|
46
|
+
Returns a dict of ``filename -> content`` with ``{{SECRET_NNN}}``
|
|
47
|
+
and ``{{PII_NNN}}`` placeholders intact.
|
|
48
|
+
"""
|
|
49
|
+
files: dict[str, str] = {}
|
|
50
|
+
warnings: list[str] = []
|
|
51
|
+
|
|
52
|
+
# --- agent_state.json ---
|
|
53
|
+
agent_state = self._build_agent_state(egg)
|
|
54
|
+
files["agent_state.json"] = json.dumps(agent_state, indent=2) + "\n"
|
|
55
|
+
|
|
56
|
+
# --- system_prompt.md ---
|
|
57
|
+
system_records = [
|
|
58
|
+
m for m in egg.memory.memory if m.label == MemoryLabel.FLOW
|
|
59
|
+
]
|
|
60
|
+
if system_records:
|
|
61
|
+
files["system_prompt.md"] = "\n\n".join(r.text for r in system_records) + "\n"
|
|
62
|
+
|
|
63
|
+
# --- tools/ directory ---
|
|
64
|
+
if egg.skills.skills:
|
|
65
|
+
for skill in egg.skills.skills:
|
|
66
|
+
fname = _skill_to_filename(skill.name)
|
|
67
|
+
files[f"tools/{fname}"] = skill.content + "\n"
|
|
68
|
+
|
|
69
|
+
# --- archival_memory.json (state) ---
|
|
70
|
+
state_records = [
|
|
71
|
+
m for m in egg.memory.memory if m.label == MemoryLabel.STATE
|
|
72
|
+
]
|
|
73
|
+
if state_records:
|
|
74
|
+
entries = []
|
|
75
|
+
for rec in state_records:
|
|
76
|
+
entry: dict[str, str | None] = {"text": rec.text}
|
|
77
|
+
entry["timestamp"] = rec.timestamp.isoformat() if rec.timestamp else None
|
|
78
|
+
entries.append(entry)
|
|
79
|
+
files["archival_memory.json"] = json.dumps(entries, indent=2) + "\n"
|
|
80
|
+
|
|
81
|
+
# --- .letta/config.json (credential placeholders) ---
|
|
82
|
+
credentials = [
|
|
83
|
+
s for s in egg.secrets.secrets if s.kind == SecretKind.CREDENTIAL
|
|
84
|
+
]
|
|
85
|
+
if credentials:
|
|
86
|
+
config = {s.name: s.placeholder for s in credentials}
|
|
87
|
+
files[".letta/config.json"] = json.dumps(config, indent=2) + "\n"
|
|
88
|
+
|
|
89
|
+
if not files:
|
|
90
|
+
raise HatchError("Egg produced no output files for Letta target")
|
|
91
|
+
|
|
92
|
+
return RenderResult(files=files, warnings=warnings)
|
|
93
|
+
|
|
94
|
+
def hatch(self, egg: Egg, output_dir: Path) -> HatchResult:
|
|
95
|
+
"""Generate Letta project files from an Egg.
|
|
96
|
+
|
|
97
|
+
.. deprecated::
|
|
98
|
+
Use :meth:`render` instead. The pipeline now handles disk I/O.
|
|
99
|
+
"""
|
|
100
|
+
result = self.render(egg)
|
|
101
|
+
|
|
102
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
files_created: list[str] = []
|
|
104
|
+
for fname, content in result.files.items():
|
|
105
|
+
fpath = output_dir / fname
|
|
106
|
+
fpath.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
fpath.write_text(content)
|
|
108
|
+
files_created.append(fname)
|
|
109
|
+
|
|
110
|
+
return HatchResult(
|
|
111
|
+
target="letta",
|
|
112
|
+
output_dir=output_dir,
|
|
113
|
+
files_created=files_created,
|
|
114
|
+
warnings=list(result.warnings),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def validate(self, result: HatchResult) -> ValidationReport:
|
|
118
|
+
"""Validate generated Letta output."""
|
|
119
|
+
issues: list[ValidationIssue] = []
|
|
120
|
+
|
|
121
|
+
if "agent_state.json" not in result.files_created:
|
|
122
|
+
issues.append(
|
|
123
|
+
ValidationIssue(
|
|
124
|
+
level="warning",
|
|
125
|
+
message="agent_state.json was not generated",
|
|
126
|
+
location=str(result.output_dir),
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
for fname in result.files_created:
|
|
131
|
+
fpath = result.output_dir / fname
|
|
132
|
+
if not fpath.exists():
|
|
133
|
+
issues.append(
|
|
134
|
+
ValidationIssue(
|
|
135
|
+
level="error",
|
|
136
|
+
message=f"Expected file not found: {fname}",
|
|
137
|
+
location=str(fpath),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
state_path = result.output_dir / "agent_state.json"
|
|
142
|
+
if state_path.exists():
|
|
143
|
+
try:
|
|
144
|
+
data = json.loads(state_path.read_text())
|
|
145
|
+
if not isinstance(data, dict):
|
|
146
|
+
issues.append(
|
|
147
|
+
ValidationIssue(
|
|
148
|
+
level="error",
|
|
149
|
+
message="agent_state.json is not a JSON object",
|
|
150
|
+
location=str(state_path),
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
elif "memory" not in data and "system" not in data:
|
|
154
|
+
issues.append(
|
|
155
|
+
ValidationIssue(
|
|
156
|
+
level="warning",
|
|
157
|
+
message="agent_state.json has no memory blocks or system prompt",
|
|
158
|
+
location=str(state_path),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
issues.append(
|
|
163
|
+
ValidationIssue(
|
|
164
|
+
level="error",
|
|
165
|
+
message="agent_state.json is not valid JSON",
|
|
166
|
+
location=str(state_path),
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return ValidationReport(
|
|
171
|
+
valid=not any(i.level == "error" for i in issues),
|
|
172
|
+
issues=issues,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# ------------------------------------------------------------------
|
|
176
|
+
# Helpers
|
|
177
|
+
# ------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
def _build_agent_state(self, egg: Egg) -> dict:
|
|
180
|
+
"""Build the agent_state.json content from Egg data."""
|
|
181
|
+
state: dict = {
|
|
182
|
+
"name": "nydus_agent",
|
|
183
|
+
"memory": {},
|
|
184
|
+
"tools": [],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for mem_rec in egg.memory.memory:
|
|
188
|
+
if mem_rec.label in _LABEL_TO_BLOCK:
|
|
189
|
+
block_name = _LABEL_TO_BLOCK[mem_rec.label]
|
|
190
|
+
if block_name in state["memory"]:
|
|
191
|
+
state["memory"][block_name]["value"] += "\n\n" + mem_rec.text
|
|
192
|
+
else:
|
|
193
|
+
state["memory"][block_name] = {
|
|
194
|
+
"value": mem_rec.text,
|
|
195
|
+
"limit": 5000,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
flow_records = [
|
|
199
|
+
m for m in egg.memory.memory if m.label == MemoryLabel.FLOW
|
|
200
|
+
]
|
|
201
|
+
if flow_records:
|
|
202
|
+
state["system"] = "\n\n".join(r.text for r in flow_records)
|
|
203
|
+
|
|
204
|
+
for skill in egg.skills.skills:
|
|
205
|
+
state["tools"].append({
|
|
206
|
+
"name": _skill_to_module_name(skill.name),
|
|
207
|
+
"source_code": skill.content,
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
if egg.manifest.source_metadata:
|
|
211
|
+
state["metadata"] = {
|
|
212
|
+
"nydus_source": egg.manifest.source_connector,
|
|
213
|
+
"nydus_version": egg.manifest.nydus_version,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return state
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# Utility functions
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _skill_to_module_name(name: str) -> str:
|
|
225
|
+
"""Convert a skill display name to a Python module name."""
|
|
226
|
+
module = name.lower().strip().replace(" ", "_").replace("-", "_")
|
|
227
|
+
module = "".join(c for c in module if c.isalnum() or c == "_")
|
|
228
|
+
if module and module[0].isdigit():
|
|
229
|
+
module = f"tool_{module}"
|
|
230
|
+
return module or "tool"
|