sanctuary-dna 0.1.0__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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .DS_Store
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: sanctuary-dna
3
+ Version: 0.1.0
4
+ Summary: Sanctuary DNA - Gnostic agent workflow DSL. Ariadne (threading) + Poimandres (generation) = SDNA spiral
5
+ Project-URL: Homepage, https://github.com/sancovp/sdna
6
+ Project-URL: Repository, https://github.com/sancovp/sdna
7
+ Author-email: Isaac Wostrel-Rubin <isaac@sancovp.com>
8
+ License: MIT
9
+ Keywords: agent,anthropic,claude,composition,sdk,workflow
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: anthropic>=0.40.0
19
+ Requires-Dist: pydantic>=2.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
22
+ Requires-Dist: pytest>=7.0; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # SDNA - Sanctuary DNA
26
+
27
+ Gnostic agent workflow composition for Claude Agent SDK.
28
+
29
+ **Ariadne** (threading) + **Poimandres** (generation) = **SDNA** spiral
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install sdna-agent-sdk
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from sdna import ariadne, human, inject_file, sdnac, sdna_flow, HermesConfig
41
+
42
+ # 1. Build Ariadne thread (context prep)
43
+ thread = ariadne('my-thread',
44
+ inject_file('spec.md', 'spec'),
45
+ human('Approve spec?', 'approval'), # Pauses for human input
46
+ )
47
+
48
+ # 2. Create HermesConfig (the message)
49
+ config = HermesConfig(
50
+ name="generator",
51
+ system_prompt="You are a code generator...",
52
+ )
53
+
54
+ # 3. Combine into SDNAC
55
+ unit = sdnac('generate-code', thread, config)
56
+
57
+ # 4. Execute
58
+ result = await unit.execute({'initial': 'context'})
59
+ ```
60
+
61
+ ## The Trinity
62
+
63
+ | Component | Role | What It Does |
64
+ |-----------|------|--------------|
65
+ | **Ariadne** | Threader | Context manipulation: inject, weave, dovetail, human input |
66
+ | **Poimandres** | Divine Mind | Generation moment - takes config, runs agent, returns output |
67
+ | **HermesConfig** | The Message | Runner configuration Ariadne sends to Poimandres |
68
+
69
+ ## Decision Tree: What to Build
70
+
71
+ ```
72
+ Is this continuous improvement / optimization loop?
73
+ ├── YES → SDNA^F (SDNAFlowchain)
74
+ │ Optimizer + target pairs. Meta-optimization.
75
+
76
+ └── NO → Are you composing multiple agent units in sequence?
77
+ ├── YES → SDNAF (SDNAFlow)
78
+ │ Flow of SDNACs. Sequential execution.
79
+
80
+ └── NO → SDNAC
81
+ Single unit: AriadneChain → HermesConfig → Poimandres executes
82
+ ```
83
+
84
+ ## Ariadne Elements
85
+
86
+ ```python
87
+ from sdna import (
88
+ ariadne, # Create chain
89
+ human, # Human input stop step
90
+ inject_file, # Inject file contents
91
+ inject_func, # Inject function result
92
+ inject_literal, # Inject literal value
93
+ inject_env, # Inject env variable
94
+ weave, # Context surgery between sessions
95
+ )
96
+ ```
97
+
98
+ ## License
99
+
100
+ MIT
@@ -0,0 +1,76 @@
1
+ # SDNA - Sanctuary DNA
2
+
3
+ Gnostic agent workflow composition for Claude Agent SDK.
4
+
5
+ **Ariadne** (threading) + **Poimandres** (generation) = **SDNA** spiral
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install sdna-agent-sdk
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ from sdna import ariadne, human, inject_file, sdnac, sdna_flow, HermesConfig
17
+
18
+ # 1. Build Ariadne thread (context prep)
19
+ thread = ariadne('my-thread',
20
+ inject_file('spec.md', 'spec'),
21
+ human('Approve spec?', 'approval'), # Pauses for human input
22
+ )
23
+
24
+ # 2. Create HermesConfig (the message)
25
+ config = HermesConfig(
26
+ name="generator",
27
+ system_prompt="You are a code generator...",
28
+ )
29
+
30
+ # 3. Combine into SDNAC
31
+ unit = sdnac('generate-code', thread, config)
32
+
33
+ # 4. Execute
34
+ result = await unit.execute({'initial': 'context'})
35
+ ```
36
+
37
+ ## The Trinity
38
+
39
+ | Component | Role | What It Does |
40
+ |-----------|------|--------------|
41
+ | **Ariadne** | Threader | Context manipulation: inject, weave, dovetail, human input |
42
+ | **Poimandres** | Divine Mind | Generation moment - takes config, runs agent, returns output |
43
+ | **HermesConfig** | The Message | Runner configuration Ariadne sends to Poimandres |
44
+
45
+ ## Decision Tree: What to Build
46
+
47
+ ```
48
+ Is this continuous improvement / optimization loop?
49
+ ├── YES → SDNA^F (SDNAFlowchain)
50
+ │ Optimizer + target pairs. Meta-optimization.
51
+
52
+ └── NO → Are you composing multiple agent units in sequence?
53
+ ├── YES → SDNAF (SDNAFlow)
54
+ │ Flow of SDNACs. Sequential execution.
55
+
56
+ └── NO → SDNAC
57
+ Single unit: AriadneChain → HermesConfig → Poimandres executes
58
+ ```
59
+
60
+ ## Ariadne Elements
61
+
62
+ ```python
63
+ from sdna import (
64
+ ariadne, # Create chain
65
+ human, # Human input stop step
66
+ inject_file, # Inject file contents
67
+ inject_func, # Inject function result
68
+ inject_literal, # Inject literal value
69
+ inject_env, # Inject env variable
70
+ weave, # Context surgery between sessions
71
+ )
72
+ ```
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sanctuary-dna"
7
+ version = "0.1.0"
8
+ description = "Sanctuary DNA - Gnostic agent workflow DSL. Ariadne (threading) + Poimandres (generation) = SDNA spiral"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "Isaac Wostrel-Rubin", email = "isaac@sancovp.com"}
14
+ ]
15
+ keywords = ["agent", "workflow", "claude", "anthropic", "sdk", "composition"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ ]
25
+ dependencies = [
26
+ "pydantic>=2.0",
27
+ "anthropic>=0.40.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=7.0",
33
+ "pytest-asyncio>=0.21",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/sancovp/sdna"
38
+ Repository = "https://github.com/sancovp/sdna"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["sdna"]
42
+
43
+ [tool.pytest.ini_options]
44
+ asyncio_mode = "auto"
45
+ testpaths = ["tests"]
@@ -0,0 +1,79 @@
1
+ """
2
+ SDNA - Sanctuary DNA
3
+
4
+ Gnostic agent workflow DSL for Claude Agent SDK.
5
+ Ariadne (threading) + Poimandres (generation) = SDNA spiral.
6
+
7
+ Components:
8
+ - Ariadne: context threading (inject, weave, human input)
9
+ - Poimandres: generation moment (execute)
10
+ - SDNA: spiral composition (SDNAC → SDNAF → SDNA^F)
11
+ """
12
+
13
+ from .config import HermesConfig, DovetailModel, HermesConfigInput
14
+ from .tools import BlockedReport, parse_blocked_from_text, get_cached_reports, clear_cached_reports
15
+ from .runner import agent_step, StepResult, StepStatus
16
+ from .ariadne import (
17
+ AriadneChain, AriadneResult, AriadneStatus,
18
+ AriadneElement, HumanInput, InjectConfig, WeaveConfig, BrainInjectConfig,
19
+ ariadne, human, inject_file, inject_func, inject_literal, inject_env, weave, inject_brain,
20
+ )
21
+ from .brain import Brain, BrainConfig, Neuron, CognitionResult
22
+ from .sdna import (
23
+ SDNAC, SDNAFlow, SDNAFlowchain,
24
+ SDNAResult, SDNAStatus,
25
+ SDNACConfig, OptimizerSDNACConfig, SDNAFlowConfig,
26
+ sdnac, sdna_flow,
27
+ )
28
+ from . import poimandres
29
+
30
+ __all__ = [
31
+ # Config
32
+ "HermesConfig",
33
+ "DovetailModel",
34
+ "HermesConfigInput",
35
+ # Tools
36
+ "BlockedReport",
37
+ "parse_blocked_from_text",
38
+ "get_cached_reports",
39
+ "clear_cached_reports",
40
+ # Runner
41
+ "agent_step",
42
+ "StepResult",
43
+ "StepStatus",
44
+ # Ariadne (context threading)
45
+ "AriadneChain",
46
+ "AriadneResult",
47
+ "AriadneStatus",
48
+ "AriadneElement",
49
+ "HumanInput",
50
+ "InjectConfig",
51
+ "WeaveConfig",
52
+ "BrainInjectConfig",
53
+ "ariadne",
54
+ "human",
55
+ "inject_file",
56
+ "inject_func",
57
+ "inject_literal",
58
+ "inject_env",
59
+ "weave",
60
+ "inject_brain",
61
+ # Brain (neural knowledge retrieval)
62
+ "Brain",
63
+ "BrainConfig",
64
+ "Neuron",
65
+ "CognitionResult",
66
+ # Poimandres (generation moment)
67
+ "poimandres",
68
+ # SDNA (spiral composition)
69
+ "SDNAC",
70
+ "SDNAFlow",
71
+ "SDNAFlowchain",
72
+ "SDNAResult",
73
+ "SDNAStatus",
74
+ "SDNACConfig",
75
+ "OptimizerSDNACConfig",
76
+ "SDNAFlowConfig",
77
+ "sdnac",
78
+ "sdna_flow",
79
+ ]
@@ -0,0 +1,328 @@
1
+ """
2
+ Ariadne - The Threader
3
+
4
+ Context manipulation: inject, weave, dovetail, human input.
5
+ Ariadne prepares the thread that guides Poimandres.
6
+ """
7
+
8
+ from typing import Dict, Any, List, Union, Optional
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from pydantic import BaseModel, Field
12
+ import importlib
13
+ import os
14
+
15
+ from .config import DovetailModel
16
+
17
+
18
+ # =============================================================================
19
+ # ARIADNE ELEMENTS
20
+ # =============================================================================
21
+
22
+ class HumanInput(BaseModel):
23
+ """Stop step - pause, return to human, resume with answer."""
24
+ prompt: str
25
+ input_key: str
26
+ choices: Optional[List[str]] = None
27
+
28
+
29
+ class InjectConfig(BaseModel):
30
+ """Inject external data into context."""
31
+ source: str # "file", "function", "literal", "env"
32
+ inject_as: str
33
+ path: Optional[str] = None
34
+ module: Optional[str] = None
35
+ func: Optional[str] = None
36
+ args: Dict[str, Any] = Field(default_factory=dict)
37
+ value: Optional[Any] = None
38
+ env_var: Optional[str] = None
39
+ default: Optional[str] = None
40
+
41
+ async def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
42
+ if self.source == "file":
43
+ with open(self.path, 'r') as f:
44
+ context[self.inject_as] = f.read()
45
+ elif self.source == "function":
46
+ mod = importlib.import_module(self.module)
47
+ fn = getattr(mod, self.func)
48
+ resolved = {
49
+ k: context.get(v[1:], v) if isinstance(v, str) and v.startswith("$") else v
50
+ for k, v in self.args.items()
51
+ }
52
+ context[self.inject_as] = fn(**resolved)
53
+ elif self.source == "literal":
54
+ context[self.inject_as] = self.value
55
+ elif self.source == "env":
56
+ context[self.inject_as] = os.environ.get(self.env_var, self.default)
57
+ return context
58
+
59
+
60
+ class WeaveConfig(BaseModel):
61
+ """Context surgery - move message ranges between sessions."""
62
+ source_session: Optional[str] = None
63
+ target_session: Optional[str] = None
64
+ start_index: Optional[int] = None
65
+ end_index: Optional[int] = None
66
+ inject_as: str = "woven_context"
67
+
68
+ async def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
69
+ # TODO: Implement with SDK session access
70
+ context[self.inject_as] = {
71
+ "source": self.source_session,
72
+ "range": (self.start_index, self.end_index),
73
+ "_pending": True,
74
+ }
75
+ return context
76
+
77
+
78
+ AriadneElement = Union[HumanInput, InjectConfig, WeaveConfig, DovetailModel, "BrainInjectConfig"]
79
+
80
+
81
+ # =============================================================================
82
+ # RESULT
83
+ # =============================================================================
84
+
85
+ class AriadneStatus(str, Enum):
86
+ SUCCESS = "success"
87
+ ERROR = "error"
88
+ AWAITING_INPUT = "awaiting_input"
89
+
90
+
91
+ @dataclass
92
+ class AriadneResult:
93
+ """Result of Ariadne chain execution."""
94
+ status: AriadneStatus
95
+ context: Dict[str, Any] = field(default_factory=dict)
96
+ error: Optional[str] = None
97
+ # For AWAITING_INPUT
98
+ pending_prompt: Optional[str] = None
99
+ pending_input_key: Optional[str] = None
100
+ pending_choices: Optional[List[str]] = None
101
+ resume_at: Optional[int] = None
102
+
103
+
104
+ # =============================================================================
105
+ # ARIADNE CHAIN
106
+ # =============================================================================
107
+
108
+ class AriadneChain:
109
+ """
110
+ Chain of context operations.
111
+ Prepares the thread that guides Poimandres.
112
+ """
113
+
114
+ def __init__(self, name: str, elements: List[AriadneElement]):
115
+ self.name = name
116
+ self.elements = elements
117
+
118
+ async def execute(
119
+ self,
120
+ context: Optional[Dict[str, Any]] = None,
121
+ start_at: int = 0,
122
+ ) -> AriadneResult:
123
+ ctx = dict(context) if context else {}
124
+
125
+ for i in range(start_at, len(self.elements)):
126
+ elem = self.elements[i]
127
+
128
+ try:
129
+ if isinstance(elem, HumanInput):
130
+ return AriadneResult(
131
+ status=AriadneStatus.AWAITING_INPUT,
132
+ context=ctx,
133
+ pending_prompt=elem.prompt,
134
+ pending_input_key=elem.input_key,
135
+ pending_choices=elem.choices,
136
+ resume_at=i + 1,
137
+ )
138
+
139
+ elif isinstance(elem, InjectConfig):
140
+ ctx = await elem.execute(ctx)
141
+
142
+ elif isinstance(elem, WeaveConfig):
143
+ ctx = await elem.execute(ctx)
144
+
145
+ elif isinstance(elem, BrainInjectConfig):
146
+ ctx = await elem.execute(ctx)
147
+
148
+ elif isinstance(elem, DovetailModel):
149
+ next_inputs = elem.prepare_next_inputs(ctx)
150
+ ctx.update(next_inputs)
151
+
152
+ except Exception as e:
153
+ return AriadneResult(status=AriadneStatus.ERROR, context=ctx, error=str(e))
154
+
155
+ return AriadneResult(status=AriadneStatus.SUCCESS, context=ctx)
156
+
157
+ def __repr__(self):
158
+ return f"AriadneChain('{self.name}', {len(self.elements)} elements)"
159
+
160
+
161
+ # =============================================================================
162
+ # CONVENIENCE CONSTRUCTORS
163
+ # =============================================================================
164
+
165
+ def ariadne(name: str, *elements: AriadneElement) -> AriadneChain:
166
+ """
167
+ Create an Ariadne chain for context threading.
168
+
169
+ Args:
170
+ name: Chain identifier
171
+ *elements: HumanInput, InjectConfig, WeaveConfig, or DovetailModel
172
+
173
+ Example:
174
+ thread = ariadne('prep',
175
+ inject_file('spec.md', 'spec'),
176
+ human('Approve?', 'approval'),
177
+ )
178
+ """
179
+ return AriadneChain(name, list(elements))
180
+
181
+
182
+ def human(prompt: str, as_key: str, choices: List[str] = None) -> HumanInput:
183
+ """
184
+ Create a human input stop step. Pauses chain, awaits input, resumes.
185
+
186
+ Args:
187
+ prompt: Question to show the human
188
+ as_key: Context key where answer is stored
189
+ choices: Optional list of choices to present
190
+
191
+ Example:
192
+ human('Which approach?', 'choice', ['A', 'B', 'C'])
193
+ """
194
+ return HumanInput(prompt=prompt, input_key=as_key, choices=choices)
195
+
196
+
197
+ def inject_file(path: str, as_key: str) -> InjectConfig:
198
+ """
199
+ Inject file contents into context.
200
+
201
+ Args:
202
+ path: Path to file to read
203
+ as_key: Context key where contents are stored
204
+
205
+ Example:
206
+ inject_file('README.md', 'readme')
207
+ """
208
+ return InjectConfig(source="file", path=path, inject_as=as_key)
209
+
210
+
211
+ def inject_func(module: str, func: str, as_key: str, **args) -> InjectConfig:
212
+ """
213
+ Inject function result into context.
214
+
215
+ Args:
216
+ module: Python module path (e.g., 'mypackage.utils')
217
+ func: Function name to call
218
+ as_key: Context key where result is stored
219
+ **args: Arguments to pass (use $key to reference context values)
220
+
221
+ Example:
222
+ inject_func('utils', 'get_data', 'data', id='$user_id')
223
+ """
224
+ return InjectConfig(source="function", module=module, func=func, args=args, inject_as=as_key)
225
+
226
+
227
+ def inject_literal(value: Any, as_key: str) -> InjectConfig:
228
+ """
229
+ Inject a literal value into context.
230
+
231
+ Args:
232
+ value: Any value to inject
233
+ as_key: Context key where value is stored
234
+
235
+ Example:
236
+ inject_literal({'mode': 'fast'}, 'config')
237
+ """
238
+ return InjectConfig(source="literal", value=value, inject_as=as_key)
239
+
240
+
241
+ def inject_env(env_var: str, as_key: str, default: str = None) -> InjectConfig:
242
+ """
243
+ Inject environment variable into context.
244
+
245
+ Args:
246
+ env_var: Environment variable name
247
+ as_key: Context key where value is stored
248
+ default: Default if env var not set
249
+
250
+ Example:
251
+ inject_env('API_KEY', 'api_key', default='none')
252
+ """
253
+ return InjectConfig(source="env", env_var=env_var, default=default, inject_as=as_key)
254
+
255
+
256
+ def weave(source: str = None, start: int = None, end: int = None, as_key: str = "woven") -> WeaveConfig:
257
+ """
258
+ Weave message ranges between sessions (context surgery).
259
+
260
+ Args:
261
+ source: Source session ID
262
+ start: Start message index
263
+ end: End message index
264
+ as_key: Context key where woven content is stored
265
+
266
+ Example:
267
+ weave(source='session_123', start=5, end=10, as_key='prior_context')
268
+ """
269
+ return WeaveConfig(source_session=source, start_index=start, end_index=end, inject_as=as_key)
270
+
271
+
272
+ class BrainInjectConfig(BaseModel):
273
+ """Inject knowledge from Brain neurons into context."""
274
+ brain_directory: str
275
+ query_key: str # Context key containing query (or literal if starts with !)
276
+ inject_as: str
277
+ max_neurons: int = 5
278
+ extensions: list = Field(default_factory=lambda: [".md", ".txt", ".py"])
279
+
280
+ async def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
281
+ from .brain import Brain, BrainConfig
282
+
283
+ # Resolve query from context or use literal
284
+ if self.query_key.startswith("$"):
285
+ query = context.get(self.query_key[1:], "")
286
+ else:
287
+ query = self.query_key
288
+
289
+ # Create and run brain
290
+ brain_config = BrainConfig(
291
+ name="ariadne_brain",
292
+ directory=self.brain_directory,
293
+ extensions=self.extensions,
294
+ )
295
+ brain = Brain(brain_config)
296
+ brain.load_neurons()
297
+
298
+ # Cognize and synthesize
299
+ result = await brain.think(query)
300
+ context[self.inject_as] = result.instructions
301
+ context[f"{self.inject_as}_neurons"] = [
302
+ {"name": n.name, "relevance": n.relevance}
303
+ for n in result.relevant_neurons
304
+ ]
305
+ return context
306
+
307
+
308
+ def inject_brain(directory: str, query_key: str, as_key: str, max_neurons: int = 5) -> BrainInjectConfig:
309
+ """
310
+ Inject knowledge from Brain neurons into context.
311
+
312
+ Uses Haiku to find relevant documents and synthesize instructions.
313
+
314
+ Args:
315
+ directory: Path to directory containing neuron files (.md, .txt, .py)
316
+ query_key: Context key with query (use $key) or literal query
317
+ as_key: Context key where synthesized instructions are stored
318
+ max_neurons: Max relevant neurons to use (default 5)
319
+
320
+ Example:
321
+ inject_brain('/docs/mcp-guides', '$user_question', 'knowledge')
322
+ """
323
+ return BrainInjectConfig(
324
+ brain_directory=directory,
325
+ query_key=query_key,
326
+ inject_as=as_key,
327
+ max_neurons=max_neurons,
328
+ )