vystak 0.1.0__py3-none-any.whl

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.
vystak/__init__.py ADDED
@@ -0,0 +1,126 @@
1
+ """Vystak — declarative AI agent orchestration."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ # Schema models
6
+ # Hash engine
7
+ from vystak.hash import AgentHashTree, hash_agent, hash_dict, hash_model
8
+
9
+ # Provider ABCs and supporting types
10
+ from vystak.providers import (
11
+ AgentStatus,
12
+ ChannelAdapter,
13
+ DeployPlan,
14
+ DeployResult,
15
+ FrameworkAdapter,
16
+ GeneratedCode,
17
+ PlatformProvider,
18
+ ValidationError,
19
+ )
20
+
21
+ # Provisioning engine
22
+ from vystak.provisioning import (
23
+ CommandHealthCheck,
24
+ CycleError,
25
+ HealthCheck,
26
+ HttpHealthCheck,
27
+ NoopHealthCheck,
28
+ Provisionable,
29
+ ProvisionError,
30
+ ProvisionGraph,
31
+ ProvisionResult,
32
+ TcpHealthCheck,
33
+ )
34
+ from vystak.schema import (
35
+ Agent,
36
+ Cache,
37
+ Channel,
38
+ ChannelProvider,
39
+ ChannelType,
40
+ Database,
41
+ Embedding,
42
+ Gateway,
43
+ McpServer,
44
+ McpTransport,
45
+ Model,
46
+ NamedModel,
47
+ ObjectStore,
48
+ Platform,
49
+ Postgres,
50
+ Provider,
51
+ Qdrant,
52
+ Queue,
53
+ Redis,
54
+ Resource,
55
+ Secret,
56
+ Service,
57
+ SessionStore,
58
+ Skill,
59
+ SkillRequirements,
60
+ SlackChannel,
61
+ Sqlite,
62
+ VectorStore,
63
+ Workspace,
64
+ WorkspaceType,
65
+ )
66
+
67
+ # Loader
68
+ from vystak.schema.loader import dump_agent, load_agent
69
+
70
+ __all__ = [
71
+ "__version__",
72
+ "Agent",
73
+ "Cache",
74
+ "Channel",
75
+ "ChannelProvider",
76
+ "ChannelType",
77
+ "Database",
78
+ "Embedding",
79
+ "Gateway",
80
+ "McpServer",
81
+ "McpTransport",
82
+ "Model",
83
+ "NamedModel",
84
+ "ObjectStore",
85
+ "Platform",
86
+ "Postgres",
87
+ "Provider",
88
+ "Qdrant",
89
+ "Queue",
90
+ "Redis",
91
+ "Resource",
92
+ "Secret",
93
+ "Service",
94
+ "SessionStore",
95
+ "Skill",
96
+ "SkillRequirements",
97
+ "SlackChannel",
98
+ "Sqlite",
99
+ "VectorStore",
100
+ "Workspace",
101
+ "WorkspaceType",
102
+ "AgentHashTree",
103
+ "hash_agent",
104
+ "hash_dict",
105
+ "hash_model",
106
+ "dump_agent",
107
+ "load_agent",
108
+ "AgentStatus",
109
+ "ChannelAdapter",
110
+ "DeployPlan",
111
+ "DeployResult",
112
+ "FrameworkAdapter",
113
+ "GeneratedCode",
114
+ "PlatformProvider",
115
+ "ValidationError",
116
+ "CommandHealthCheck",
117
+ "CycleError",
118
+ "HealthCheck",
119
+ "HttpHealthCheck",
120
+ "NoopHealthCheck",
121
+ "Provisionable",
122
+ "ProvisionError",
123
+ "ProvisionGraph",
124
+ "ProvisionResult",
125
+ "TcpHealthCheck",
126
+ ]
@@ -0,0 +1,6 @@
1
+ """Content-addressable hash engine for stateless change detection."""
2
+
3
+ from vystak.hash.hasher import hash_dict, hash_model
4
+ from vystak.hash.tree import AgentHashTree, hash_agent
5
+
6
+ __all__ = ["AgentHashTree", "hash_agent", "hash_dict", "hash_model"]
vystak/hash/hasher.py ADDED
@@ -0,0 +1,18 @@
1
+ """Leaf-level hashing for Pydantic models and dicts."""
2
+
3
+ import hashlib
4
+ import json
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ def hash_model(model: BaseModel) -> str:
10
+ """SHA-256 of canonical JSON representation of a Pydantic model."""
11
+ canonical = json.dumps(model.model_dump(mode="python"), sort_keys=True, default=str)
12
+ return hashlib.sha256(canonical.encode()).hexdigest()
13
+
14
+
15
+ def hash_dict(data: dict) -> str:
16
+ """SHA-256 of canonical JSON representation of a dict."""
17
+ canonical = json.dumps(data, sort_keys=True, default=str)
18
+ return hashlib.sha256(canonical.encode()).hexdigest()
vystak/hash/tree.py ADDED
@@ -0,0 +1,82 @@
1
+ """Hash tree composition for agent definitions."""
2
+
3
+ import hashlib
4
+ from dataclasses import dataclass
5
+
6
+ from vystak.hash.hasher import hash_model
7
+ from vystak.schema.agent import Agent
8
+
9
+
10
+ @dataclass
11
+ class AgentHashTree:
12
+ """Per-section hashes for an agent, enabling partial deploy detection."""
13
+
14
+ brain: str
15
+ skills: str
16
+ mcp_servers: str
17
+ channels: str
18
+ workspace: str
19
+ resources: str
20
+ secrets: str
21
+ sessions: str
22
+ memory: str
23
+ services: str
24
+ root: str
25
+
26
+
27
+ def _hash_list(items: list) -> str:
28
+ if not items:
29
+ return hashlib.sha256(b"[]").hexdigest()
30
+ individual = sorted(hash_model(item) for item in items)
31
+ combined = "|".join(individual)
32
+ return hashlib.sha256(combined.encode()).hexdigest()
33
+
34
+
35
+ def _hash_optional(item) -> str:
36
+ if item is None:
37
+ return hashlib.sha256(b"null").hexdigest()
38
+ return hash_model(item)
39
+
40
+
41
+ def hash_agent(agent: Agent) -> AgentHashTree:
42
+ """Compute the full hash tree for an agent definition."""
43
+ brain = hash_model(agent.model)
44
+ skills = _hash_list(agent.skills)
45
+ mcp_servers = _hash_list(agent.mcp_servers)
46
+ channels = _hash_list(agent.channels)
47
+ workspace = _hash_optional(agent.workspace)
48
+ resources = _hash_list(agent.resources)
49
+ secrets = _hash_list(agent.secrets)
50
+ sessions = _hash_optional(agent.sessions)
51
+ memory = _hash_optional(agent.memory)
52
+ services = _hash_list(agent.services)
53
+
54
+ sections = "|".join(
55
+ [
56
+ brain,
57
+ skills,
58
+ mcp_servers,
59
+ channels,
60
+ workspace,
61
+ resources,
62
+ secrets,
63
+ sessions,
64
+ memory,
65
+ services,
66
+ ]
67
+ )
68
+ root = hashlib.sha256(sections.encode()).hexdigest()
69
+
70
+ return AgentHashTree(
71
+ brain=brain,
72
+ skills=skills,
73
+ mcp_servers=mcp_servers,
74
+ channels=channels,
75
+ workspace=workspace,
76
+ resources=resources,
77
+ secrets=secrets,
78
+ sessions=sessions,
79
+ memory=memory,
80
+ services=services,
81
+ root=root,
82
+ )
vystak/ir/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Intermediate representation consumed by framework adapters."""
@@ -0,0 +1,23 @@
1
+ """Provider base classes for platform and resource provisioning."""
2
+
3
+ from vystak.providers.base import (
4
+ AgentStatus,
5
+ ChannelAdapter,
6
+ DeployPlan,
7
+ DeployResult,
8
+ FrameworkAdapter,
9
+ GeneratedCode,
10
+ PlatformProvider,
11
+ ValidationError,
12
+ )
13
+
14
+ __all__ = [
15
+ "AgentStatus",
16
+ "ChannelAdapter",
17
+ "DeployPlan",
18
+ "DeployResult",
19
+ "FrameworkAdapter",
20
+ "GeneratedCode",
21
+ "PlatformProvider",
22
+ "ValidationError",
23
+ ]
@@ -0,0 +1,77 @@
1
+ """Abstract base classes for framework adapters, platform providers, and channel adapters."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+
6
+ from vystak.schema.agent import Agent
7
+ from vystak.schema.channel import Channel
8
+
9
+
10
+ @dataclass
11
+ class GeneratedCode:
12
+ files: dict[str, str]
13
+ entrypoint: str
14
+
15
+
16
+ @dataclass
17
+ class DeployPlan:
18
+ agent_name: str
19
+ actions: list[str]
20
+ current_hash: str | None
21
+ target_hash: str
22
+ changes: dict[str, tuple[str | None, str]]
23
+
24
+
25
+ @dataclass
26
+ class DeployResult:
27
+ agent_name: str
28
+ success: bool
29
+ hash: str
30
+ message: str
31
+
32
+
33
+ @dataclass
34
+ class AgentStatus:
35
+ agent_name: str
36
+ running: bool
37
+ hash: str | None
38
+ info: dict = field(default_factory=dict)
39
+
40
+
41
+ @dataclass
42
+ class ValidationError:
43
+ field: str
44
+ message: str
45
+
46
+
47
+ class FrameworkAdapter(ABC):
48
+ @abstractmethod
49
+ def generate(self, agent: Agent) -> GeneratedCode: ...
50
+
51
+ @abstractmethod
52
+ def validate(self, agent: Agent) -> list[ValidationError]: ...
53
+
54
+
55
+ class PlatformProvider(ABC):
56
+ @abstractmethod
57
+ def plan(self, agent: Agent, current_hash: str | None) -> DeployPlan: ...
58
+
59
+ @abstractmethod
60
+ def apply(self, plan: DeployPlan) -> DeployResult: ...
61
+
62
+ @abstractmethod
63
+ def destroy(self, agent_name: str) -> None: ...
64
+
65
+ @abstractmethod
66
+ def status(self, agent_name: str) -> AgentStatus: ...
67
+
68
+ @abstractmethod
69
+ def get_hash(self, agent_name: str) -> str | None: ...
70
+
71
+
72
+ class ChannelAdapter(ABC):
73
+ @abstractmethod
74
+ def setup(self, agent: Agent, channel: Channel) -> None: ...
75
+
76
+ @abstractmethod
77
+ def teardown(self, channel: Channel) -> None: ...
@@ -0,0 +1,37 @@
1
+ """Provisioning engine — dependency graph, health checks, and provisionable protocol."""
2
+
3
+ from vystak.provisioning.graph import CycleError, ProvisionError, ProvisionGraph
4
+ from vystak.provisioning.grouping import group_agents_by_platform, platform_fingerprint
5
+ from vystak.provisioning.health import (
6
+ CommandHealthCheck,
7
+ HealthCheck,
8
+ HttpHealthCheck,
9
+ NoopHealthCheck,
10
+ TcpHealthCheck,
11
+ )
12
+ from vystak.provisioning.listener import (
13
+ NullListener,
14
+ PrintListener,
15
+ ProvisionEvent,
16
+ ProvisionListener,
17
+ )
18
+ from vystak.provisioning.node import Provisionable, ProvisionResult
19
+
20
+ __all__ = [
21
+ "CommandHealthCheck",
22
+ "CycleError",
23
+ "group_agents_by_platform",
24
+ "HealthCheck",
25
+ "HttpHealthCheck",
26
+ "NoopHealthCheck",
27
+ "NullListener",
28
+ "platform_fingerprint",
29
+ "PrintListener",
30
+ "Provisionable",
31
+ "ProvisionError",
32
+ "ProvisionEvent",
33
+ "ProvisionGraph",
34
+ "ProvisionListener",
35
+ "ProvisionResult",
36
+ "TcpHealthCheck",
37
+ ]
@@ -0,0 +1,122 @@
1
+ """Dependency graph for provisioning resources."""
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from vystak.provisioning.listener import NullListener, ProvisionEvent, ProvisionListener
6
+ from vystak.provisioning.node import Provisionable, ProvisionResult
7
+
8
+
9
+ class CycleError(Exception):
10
+ pass
11
+
12
+
13
+ class ProvisionError(Exception):
14
+ pass
15
+
16
+
17
+ @dataclass
18
+ class ProvisionGraph:
19
+ _nodes: dict[str, Provisionable] = field(default_factory=dict)
20
+ _implicit_deps: dict[str, list[str]] = field(default_factory=dict)
21
+ _listener: ProvisionListener = field(default_factory=NullListener)
22
+
23
+ def set_listener(self, listener: ProvisionListener) -> None:
24
+ """Set the event listener for provision progress."""
25
+ self._listener = listener
26
+
27
+ def add(self, node: Provisionable) -> None:
28
+ self._nodes[node.name] = node
29
+
30
+ def add_dependency(self, name: str, depends_on: str) -> None:
31
+ self._implicit_deps.setdefault(name, []).append(depends_on)
32
+
33
+ def _all_deps(self, name: str) -> list[str]:
34
+ node = self._nodes[name]
35
+ all_deps = list(node.depends_on)
36
+ all_deps.extend(self._implicit_deps.get(name, []))
37
+ return [d for d in all_deps if d in self._nodes]
38
+
39
+ def _resolve_order(self) -> list[str]:
40
+ reverse: dict[str, list[str]] = {name: [] for name in self._nodes}
41
+ in_degree: dict[str, int] = {name: 0 for name in self._nodes}
42
+
43
+ for name in self._nodes:
44
+ deps = self._all_deps(name)
45
+ in_degree[name] = len(deps)
46
+ for dep in deps:
47
+ reverse[dep].append(name)
48
+
49
+ queue = [n for n, d in in_degree.items() if d == 0]
50
+ order: list[str] = []
51
+
52
+ while queue:
53
+ node = queue.pop(0)
54
+ order.append(node)
55
+ for dependent in reverse[node]:
56
+ in_degree[dependent] -= 1
57
+ if in_degree[dependent] == 0:
58
+ queue.append(dependent)
59
+
60
+ if len(order) != len(self._nodes):
61
+ raise CycleError("Dependency cycle detected in provision graph")
62
+
63
+ return order
64
+
65
+ def execute(self) -> dict[str, ProvisionResult]:
66
+ order = self._resolve_order()
67
+ results: dict[str, ProvisionResult] = {}
68
+
69
+ for name in order:
70
+ node = self._nodes[name]
71
+
72
+ self._listener.on_start(
73
+ ProvisionEvent(
74
+ node_name=name,
75
+ event_type="start",
76
+ message=name,
77
+ )
78
+ )
79
+
80
+ # Pass listener to node if it supports it
81
+ if hasattr(node, "set_listener"):
82
+ node.set_listener(self._listener)
83
+
84
+ result = node.provision(context=results)
85
+ results[name] = result
86
+
87
+ if not result.success:
88
+ self._listener.on_error(
89
+ ProvisionEvent(
90
+ node_name=name,
91
+ event_type="error",
92
+ message=name,
93
+ detail=result.error or "",
94
+ )
95
+ )
96
+ raise ProvisionError(f"Failed to provision {name}: {result.error}")
97
+
98
+ self._listener.on_complete(
99
+ ProvisionEvent(
100
+ node_name=name,
101
+ event_type="complete",
102
+ message=name,
103
+ detail=result.info.get("detail", ""),
104
+ )
105
+ )
106
+
107
+ check = node.health_check()
108
+ self._listener.on_health_check(
109
+ ProvisionEvent(
110
+ node_name=name,
111
+ event_type="health_check",
112
+ message=f"Waiting for {name}",
113
+ )
114
+ )
115
+ check.wait()
116
+
117
+ return results
118
+
119
+ def destroy_all(self) -> None:
120
+ order = self._resolve_order()
121
+ for name in reversed(order):
122
+ self._nodes[name].destroy()
@@ -0,0 +1,26 @@
1
+ """Group agents by platform fingerprint for shared infrastructure."""
2
+
3
+ import hashlib
4
+ import json
5
+
6
+ from vystak.schema.agent import Agent
7
+
8
+
9
+ def platform_fingerprint(agent: Agent) -> str:
10
+ if agent.platform is None:
11
+ return "docker:default"
12
+ key = {
13
+ "provider_type": agent.platform.provider.type,
14
+ "provider_config": agent.platform.provider.config,
15
+ "platform_type": agent.platform.type,
16
+ }
17
+ canonical = json.dumps(key, sort_keys=True, default=str)
18
+ return hashlib.md5(canonical.encode()).hexdigest()
19
+
20
+
21
+ def group_agents_by_platform(agents: list[Agent]) -> dict[str, list[Agent]]:
22
+ groups: dict[str, list[Agent]] = {}
23
+ for agent in agents:
24
+ fp = platform_fingerprint(agent)
25
+ groups.setdefault(fp, []).append(agent)
26
+ return groups
@@ -0,0 +1,62 @@
1
+ """Health check classes for resource readiness verification."""
2
+
3
+ import socket
4
+ import time
5
+ import urllib.request
6
+ from abc import ABC, abstractmethod
7
+
8
+
9
+ class HealthCheck(ABC):
10
+ @abstractmethod
11
+ def check(self) -> bool: ...
12
+
13
+ def wait(self, timeout: int = 60, interval: float = 1.0) -> None:
14
+ deadline = time.time() + timeout
15
+ while time.time() < deadline:
16
+ if self.check():
17
+ return
18
+ time.sleep(interval)
19
+ raise TimeoutError(f"Health check did not pass within {timeout}s")
20
+
21
+
22
+ class NoopHealthCheck(HealthCheck):
23
+ def check(self) -> bool:
24
+ return True
25
+
26
+
27
+ class TcpHealthCheck(HealthCheck):
28
+ def __init__(self, host: str, port: int):
29
+ self.host = host
30
+ self.port = port
31
+
32
+ def check(self) -> bool:
33
+ try:
34
+ with socket.create_connection((self.host, self.port), timeout=2):
35
+ return True
36
+ except (TimeoutError, ConnectionRefusedError, OSError):
37
+ return False
38
+
39
+
40
+ class CommandHealthCheck(HealthCheck):
41
+ def __init__(self, container, command: list[str]):
42
+ self.container = container
43
+ self.command = command
44
+
45
+ def check(self) -> bool:
46
+ try:
47
+ result = self.container.exec_run(self.command, demux=False)
48
+ return result.exit_code == 0
49
+ except Exception:
50
+ return False
51
+
52
+
53
+ class HttpHealthCheck(HealthCheck):
54
+ def __init__(self, url: str):
55
+ self.url = url
56
+
57
+ def check(self) -> bool:
58
+ try:
59
+ resp = urllib.request.urlopen(self.url, timeout=2)
60
+ return resp.status == 200
61
+ except Exception:
62
+ return False
@@ -0,0 +1,68 @@
1
+ """Provision event listener — callbacks for graph execution progress."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class ProvisionEvent:
8
+ """An event emitted during provisioning."""
9
+
10
+ node_name: str
11
+ event_type: str # "start", "step", "complete", "error", "health_check"
12
+ message: str = ""
13
+ detail: str = ""
14
+
15
+
16
+ class ProvisionListener:
17
+ """Base listener for provision graph events. Override methods to handle events."""
18
+
19
+ def on_start(self, event: ProvisionEvent) -> None:
20
+ """Called before a node starts provisioning."""
21
+
22
+ def on_step(self, event: ProvisionEvent) -> None:
23
+ """Called during provisioning for sub-step progress (build, push, etc.)."""
24
+
25
+ def on_complete(self, event: ProvisionEvent) -> None:
26
+ """Called after a node finishes provisioning successfully."""
27
+
28
+ def on_error(self, event: ProvisionEvent) -> None:
29
+ """Called when a node fails to provision."""
30
+
31
+ def on_health_check(self, event: ProvisionEvent) -> None:
32
+ """Called when waiting for a health check."""
33
+
34
+
35
+ class PrintListener(ProvisionListener):
36
+ """Prints provision events to stdout with indentation."""
37
+
38
+ def __init__(self, indent: str = " "):
39
+ self._indent = indent
40
+
41
+ def on_start(self, event: ProvisionEvent) -> None:
42
+ print(f"{self._indent}{event.message}... ", end="", flush=True)
43
+
44
+ def on_step(self, event: ProvisionEvent) -> None:
45
+ if event.detail:
46
+ print(f"\n{self._indent}{event.message}: {event.detail}... ", end="", flush=True)
47
+ else:
48
+ print(f"\n{self._indent}{event.message}... ", end="", flush=True)
49
+
50
+ def on_complete(self, event: ProvisionEvent) -> None:
51
+ if event.detail:
52
+ print(f"OK ({event.detail})")
53
+ else:
54
+ print("OK")
55
+
56
+ def on_error(self, event: ProvisionEvent) -> None:
57
+ print("FAILED")
58
+ if event.detail:
59
+ print(f"{self._indent} Error: {event.detail}")
60
+
61
+ def on_health_check(self, event: ProvisionEvent) -> None:
62
+ pass # silent by default
63
+
64
+
65
+ class NullListener(ProvisionListener):
66
+ """Does nothing. Used when no listener is provided."""
67
+
68
+ pass