rusticai-forge 0.0.5__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,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: rusticai-forge
3
+ Version: 0.0.5
4
+ Summary: Python agent wrapper and execution engine for Forge
5
+ Requires-Python: <3.14,>=3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: pydantic<3.0.0,>=2.12.5
8
+ Requires-Dist: redis<7.0.0,>=6.4.0
9
+ Requires-Dist: rusticai-core<2.0.0,>=1.1.0
10
+ Requires-Dist: rusticai-redis<2.0.0,>=1.1.0
11
+ Requires-Dist: psycopg[binary]<4.0.0,>=3.3.3
12
+ Requires-Dist: httpx<1.0.0,>=0.28.1
13
+
14
+ # rusticai-forge
15
+
16
+ Python execution bridge and system agents for Forge.
@@ -0,0 +1,3 @@
1
+ # rusticai-forge
2
+
3
+ Python execution bridge and system agents for Forge.
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "rusticai-forge"
3
+ version = "0.0.5"
4
+ description = "Python agent wrapper and execution engine for Forge"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13,<3.14"
7
+ dependencies = [
8
+ "pydantic>=2.12.5,<3.0.0",
9
+ "redis>=6.4.0,<7.0.0",
10
+ # DO NOT CHANGE THESE TO LOCAL FILE PATHS. Always use package references.
11
+ "rusticai-core>=1.1.0,<2.0.0",
12
+ "rusticai-redis>=1.1.0,<2.0.0",
13
+ "psycopg[binary]>=3.3.3,<4.0.0",
14
+ "httpx>=0.28.1,<1.0.0",
15
+ ]
16
+
17
+ [dependency-groups]
18
+ dev = [
19
+ "pytest>=9.0.2,<10.0.0",
20
+ "requests>=2.32.5,<3.0.0",
21
+ "ruff>=0.13.1,<0.14.0",
22
+ ]
23
+
24
+ [build-system]
25
+ requires = ["setuptools>=80"]
26
+ build-backend = "setuptools.build_meta"
27
+
28
+ [tool.setuptools]
29
+ package-dir = {"" = "src"}
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [tool.pytest.ini_options]
35
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,253 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import sys
5
+
6
+ from pydantic import ConfigDict, ValidationError
7
+ from rustic_ai.core.guild.agent import AgentSpec
8
+ from rustic_ai.core.guild.dsl import BaseAgentProps, GuildSpec
9
+ from rustic_ai.core.guild.metaprog.constants import MetaclassConstants
10
+ import rustic_ai.core.guild.dsl as guild_dsl
11
+ from rustic_ai.core.guild import Agent
12
+ from rustic_ai.core.guild.metastore import models as metastore_models
13
+ from rustic_ai.core.messaging.core.message import RoutingSlip
14
+ from rustic_ai.core.messaging import MessagingConfig
15
+
16
+ from rustic_ai.forge.agent_wrapper import ForgeAgentWrapper
17
+
18
+ log_level = os.getenv("LOG_LEVEL", "INFO").upper()
19
+
20
+ logging.basicConfig(
21
+ level=getattr(logging, log_level),
22
+ format="[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s",
23
+ handlers=[logging.StreamHandler(sys.stdout)],
24
+ force=True,
25
+ )
26
+
27
+ logger = logging.getLogger("forge.runner")
28
+
29
+
30
+ class _UnresolvedAgent(Agent):
31
+ """
32
+ Placeholder used only for validation fallback when an agent class cannot be imported yet.
33
+ """
34
+
35
+ __annotations__ = {MetaclassConstants.AGENT_PROPS_TYPE: BaseAgentProps}
36
+
37
+ async def run(self):
38
+ return None
39
+
40
+
41
+ class _UnresolvedAgentProps(BaseAgentProps):
42
+ model_config = ConfigDict(extra="allow")
43
+
44
+
45
+ _UnresolvedAgent.__annotations__ = {
46
+ MetaclassConstants.AGENT_PROPS_TYPE: _UnresolvedAgentProps
47
+ }
48
+
49
+
50
+ _ORIGINAL_GET_CLASS_FROM_NAME = guild_dsl.get_class_from_name
51
+ _LENIENT_CLASS_RESOLUTION_ENABLED = False
52
+ _LENIENT_METASTORE_ENABLED = False
53
+
54
+ _ORIGINAL_TO_AGENT_SPEC = metastore_models.AgentModel.to_agent_spec
55
+
56
+
57
+ def _enable_lenient_class_resolution() -> None:
58
+ global _LENIENT_CLASS_RESOLUTION_ENABLED
59
+ if _LENIENT_CLASS_RESOLUTION_ENABLED:
60
+ return
61
+
62
+ def _lenient_get_class_from_name(class_name: str):
63
+ try:
64
+ return _ORIGINAL_GET_CLASS_FROM_NAME(class_name)
65
+ except Exception:
66
+ logger.warning("Using unresolved placeholder for class during validation: %s", class_name)
67
+ return _UnresolvedAgent
68
+
69
+ guild_dsl.get_class_from_name = _lenient_get_class_from_name
70
+ _LENIENT_CLASS_RESOLUTION_ENABLED = True
71
+
72
+
73
+ def _enable_lenient_metastore_conversion() -> None:
74
+ global _LENIENT_METASTORE_ENABLED
75
+ if _LENIENT_METASTORE_ENABLED:
76
+ return
77
+
78
+ def _to_agent_spec_lenient(self):
79
+ try:
80
+ return _ORIGINAL_TO_AGENT_SPEC(self)
81
+ except ValidationError as e:
82
+ logger.warning(
83
+ "AgentModel.to_agent_spec validation failed for %s; using lenient construct fallback: %s",
84
+ self.class_name,
85
+ e,
86
+ )
87
+ return AgentSpec.model_construct(
88
+ id=self.id,
89
+ name=self.name,
90
+ description=self.description,
91
+ class_name=self.class_name,
92
+ properties=self.properties or {},
93
+ additional_topics=self.additional_topics or [],
94
+ listen_to_default_topic=self.listen_to_default_topic,
95
+ act_only_when_tagged=self.act_only_when_tagged,
96
+ dependency_map=self.dependency_map or {},
97
+ additional_dependencies=self.additional_dependencies or [],
98
+ predicates=self.predicates or {},
99
+ resources={},
100
+ qos={},
101
+ )
102
+
103
+ metastore_models.AgentModel.to_agent_spec = _to_agent_spec_lenient
104
+ _LENIENT_METASTORE_ENABLED = True
105
+
106
+
107
+ def _load_guild_spec(guild_spec_json: str) -> GuildSpec:
108
+ """
109
+ Load GuildSpec with a tolerant fallback.
110
+
111
+ Strict validation can fail when guild JSON contains agent classes that are not yet
112
+ importable in this process (for example dynamically downloaded uvx deps). In that
113
+ case we preserve enough structure to start the current agent process.
114
+ """
115
+ try:
116
+ return GuildSpec.model_validate_json(guild_spec_json)
117
+ except ValidationError as e:
118
+ logger.warning(
119
+ "GuildSpec strict validation failed; falling back to lenient parsing for startup: %s",
120
+ e,
121
+ )
122
+ _enable_lenient_class_resolution()
123
+ _enable_lenient_metastore_conversion()
124
+ try:
125
+ return GuildSpec.model_validate_json(guild_spec_json)
126
+ except Exception:
127
+ logger.warning("Lenient GuildSpec re-validation failed; using structural fallback.")
128
+
129
+ raw = json.loads(guild_spec_json)
130
+ if not isinstance(raw, dict):
131
+ raise ValueError("FORGE_GUILD_JSON must decode to a JSON object.")
132
+
133
+ raw_agents = raw.get("agents") or []
134
+ fallback_agents: list[AgentSpec] = []
135
+ for i, raw_agent in enumerate(raw_agents):
136
+ if not isinstance(raw_agent, dict):
137
+ logger.warning("Skipping malformed guild agent entry at index %s", i)
138
+ continue
139
+ fallback_agents.append(
140
+ AgentSpec.model_construct(
141
+ id=str(raw_agent.get("id") or f"agent-{i}"),
142
+ name=str(raw_agent.get("name") or f"agent-{i}"),
143
+ description=str(raw_agent.get("description") or ""),
144
+ class_name=str(raw_agent.get("class_name") or ""),
145
+ additional_topics=list(raw_agent.get("additional_topics") or []),
146
+ properties=raw_agent.get("properties", {}),
147
+ listen_to_default_topic=bool(raw_agent.get("listen_to_default_topic", True)),
148
+ act_only_when_tagged=bool(raw_agent.get("act_only_when_tagged", False)),
149
+ predicates=raw_agent.get("predicates") or {},
150
+ dependency_map=raw_agent.get("dependency_map") or {},
151
+ additional_dependencies=list(raw_agent.get("additional_dependencies") or []),
152
+ resources=raw_agent.get("resources") or {},
153
+ qos=raw_agent.get("qos") or {},
154
+ )
155
+ )
156
+
157
+ routes_raw = raw.get("routes") or {}
158
+ try:
159
+ routes = RoutingSlip.model_validate(routes_raw)
160
+ except Exception:
161
+ routes = RoutingSlip()
162
+
163
+ return GuildSpec.model_construct(
164
+ id=str(raw.get("id") or ""),
165
+ name=str(raw.get("name") or ""),
166
+ description=str(raw.get("description") or ""),
167
+ properties=raw.get("properties") or {},
168
+ agents=fallback_agents,
169
+ dependency_map=raw.get("dependency_map") or {},
170
+ routes=routes,
171
+ gateway=raw.get("gateway"),
172
+ )
173
+
174
+
175
+ def _load_agent_spec(agent_spec_json: str) -> AgentSpec:
176
+ try:
177
+ return AgentSpec.model_validate_json(agent_spec_json)
178
+ except ValidationError as e:
179
+ logger.warning(
180
+ "AgentSpec strict validation failed; retrying with lenient class resolution: %s",
181
+ e,
182
+ )
183
+ _enable_lenient_class_resolution()
184
+ _enable_lenient_metastore_conversion()
185
+ return AgentSpec.model_validate_json(agent_spec_json)
186
+
187
+
188
+ def main():
189
+ try:
190
+ logger.info("Starting Forge Agent Runner...")
191
+
192
+ guild_spec_json = os.getenv("FORGE_GUILD_JSON")
193
+ if not guild_spec_json:
194
+ raise ValueError("FORGE_GUILD_JSON environment variable is missing.")
195
+
196
+ guild_spec = _load_guild_spec(guild_spec_json)
197
+ logger.debug(f"Loaded GuildSpec: {guild_spec.id} ({guild_spec.name})")
198
+
199
+ agent_spec_json = os.getenv("FORGE_AGENT_CONFIG_JSON")
200
+ if not agent_spec_json:
201
+ raise ValueError("FORGE_AGENT_CONFIG_JSON environment variable is missing.")
202
+
203
+ agent_spec = _load_agent_spec(agent_spec_json)
204
+ logger.info(f"Loaded AgentSpec: {agent_spec.id} ({agent_spec.name})")
205
+
206
+ client_type_str = os.getenv("FORGE_CLIENT_TYPE", "InMemoryMessagingBackend")
207
+ client_module_str = os.getenv("FORGE_CLIENT_MODULE", "")
208
+ client_props = json.loads(os.getenv("FORGE_CLIENT_PROPERTIES_JSON", "{}"))
209
+
210
+ backend_config = client_props.get("backend_config", client_props)
211
+
212
+ if (
213
+ client_type_str == "RedisMessagingBackend"
214
+ and "redis_client" not in backend_config
215
+ ):
216
+ backend_config["redis_client"] = {
217
+ "host": os.getenv("REDIS_HOST", "localhost"),
218
+ "port": int(os.getenv("REDIS_PORT", "6379")),
219
+ "db": int(os.getenv("REDIS_DB", "0")),
220
+ }
221
+
222
+ organization_id = client_props.pop("organization_id", None)
223
+
224
+ messaging_config = MessagingConfig(
225
+ backend_module=client_module_str,
226
+ backend_class=client_type_str,
227
+ backend_config=backend_config,
228
+ )
229
+
230
+ logger.info(f"Using Messaging Backend: {client_type_str}")
231
+
232
+ machine_id = hash(f"{guild_spec.id}-{agent_spec.id}") % 256
233
+
234
+ wrapper = ForgeAgentWrapper(
235
+ guild_spec=guild_spec,
236
+ agent_spec=agent_spec,
237
+ messaging_config=messaging_config,
238
+ machine_id=machine_id,
239
+ organization_id=organization_id,
240
+ )
241
+
242
+ wrapper.run()
243
+
244
+ logger.info("Forge Agent Runner exited gracefully.")
245
+ sys.exit(0)
246
+
247
+ except Exception as e:
248
+ logger.critical(f"Forge Agent Runner crashed: {e}", exc_info=True)
249
+ sys.exit(1)
250
+
251
+
252
+ if __name__ == "__main__":
253
+ main()
@@ -0,0 +1,81 @@
1
+ import logging
2
+ import signal
3
+ import threading
4
+ import time
5
+ from typing import Any, Dict, Optional, Type
6
+
7
+ from rustic_ai.core.guild.agent import AgentSpec
8
+ from rustic_ai.core.guild.dsl import GuildSpec
9
+ from rustic_ai.core.messaging import Client, MessageTrackingClient, MessagingConfig
10
+ from rustic_ai.core.guild.execution.agent_wrapper import AgentWrapper
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class ForgeAgentWrapper(AgentWrapper):
16
+ """
17
+ An implementation of AgentWrapper that runs as a standalone Python process
18
+ managed by the Go-based Forge AgentSupervisor.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ guild_spec: GuildSpec,
24
+ agent_spec: AgentSpec,
25
+ messaging_config: MessagingConfig,
26
+ machine_id: int,
27
+ client_type: Type[Client] = MessageTrackingClient,
28
+ client_properties: Optional[Dict[str, Any]] = None,
29
+ organization_id: Optional[str] = None,
30
+ ):
31
+ if client_properties is None:
32
+ client_properties = {}
33
+
34
+ super().__init__(
35
+ guild_spec=guild_spec,
36
+ agent_spec=agent_spec,
37
+ messaging_config=messaging_config,
38
+ machine_id=machine_id,
39
+ client_type=client_type,
40
+ client_properties=client_properties,
41
+ organization_id=organization_id,
42
+ )
43
+
44
+ self.shutdown_event = threading.Event()
45
+ self.is_running = False
46
+
47
+ def run(self) -> None:
48
+ """Initializes the agent and blocks until shutdown is signaled."""
49
+ try:
50
+ self.is_running = True
51
+ logger.info(f"Initializing agent {self.agent_spec.name} in Forge wrapper")
52
+ self.initialize_agent()
53
+ logger.info(f"Agent {self.agent_spec.name} initialized successfully.")
54
+
55
+ def signal_handler(signum: int, frame: Any) -> None:
56
+ logger.info(
57
+ f"Received signal {signum}, initiating shutdown for {self.agent_spec.id}..."
58
+ )
59
+ self.shutdown_event.set()
60
+
61
+ signal.signal(signal.SIGINT, signal_handler)
62
+ signal.signal(signal.SIGTERM, signal_handler)
63
+
64
+ while not self.shutdown_event.is_set():
65
+ time.sleep(0.5)
66
+
67
+ logger.info(f"Agent {self.agent_spec.id} shutting down cleanly.")
68
+ self.shutdown()
69
+
70
+ except Exception as e:
71
+ logger.error(
72
+ f"Error running agent {self.agent_spec.name} in Forge wrapper: {e}",
73
+ exc_info=True,
74
+ )
75
+ self.is_running = False
76
+ raise
77
+
78
+ def shutdown(self) -> None:
79
+ self.shutdown_event.set()
80
+ super().shutdown()
81
+ self.is_running = False