harness-runtime 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.
cli.py ADDED
@@ -0,0 +1,116 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import NoReturn
6
+
7
+ from dotenv import load_dotenv
8
+
9
+ from core.event_publisher import StdioPublisher
10
+ from core.executor import ExecutionManager
11
+ from core.session import Session
12
+
13
+ env_path = Path(__file__).parent / ".env"
14
+ load_dotenv(dotenv_path=env_path)
15
+
16
+
17
+ def main() -> None:
18
+ database_url = os.getenv("DATABASE_URL")
19
+ if not database_url:
20
+ _error_exit("DATABASE_URL environment variable is required", 2)
21
+
22
+ session: Session | None = None
23
+ execution_manager: ExecutionManager | None = None
24
+ publisher = StdioPublisher()
25
+
26
+ try:
27
+ execution_manager = ExecutionManager(
28
+ postgres_connection_string=database_url,
29
+ publisher=publisher,
30
+ )
31
+
32
+ for line in sys.stdin:
33
+ line = line.strip()
34
+ if not line:
35
+ continue
36
+
37
+ try:
38
+ msg = json.loads(line)
39
+ except json.JSONDecodeError:
40
+ continue
41
+
42
+ msg_type = msg.get("type")
43
+
44
+ if msg_type == "control_request":
45
+ request = msg.get("request", {})
46
+ subtype = request.get("subtype")
47
+ request_id = msg.get("request_id", "")
48
+
49
+ if subtype == "initialize":
50
+ agent_definition = request.get("agent_definition", {})
51
+ input_payload = request.get("input_payload", {})
52
+
53
+ session = Session(
54
+ agent_definition=agent_definition,
55
+ input_payload=input_payload,
56
+ execution_manager=execution_manager,
57
+ publisher=publisher,
58
+ )
59
+ session.initialize()
60
+
61
+ publisher.publish_control_response(
62
+ request_id=request_id,
63
+ session_id=session.session_id,
64
+ )
65
+
66
+ elif subtype == "interrupt":
67
+ publisher.publish_control_response(
68
+ request_id=request_id,
69
+ )
70
+
71
+ elif msg_type == "user":
72
+ if session is None:
73
+ publisher.publish_control_response(
74
+ request_id="",
75
+ subtype="error",
76
+ error="Session not initialized",
77
+ )
78
+ continue
79
+
80
+ try:
81
+ user_content = ""
82
+ user_msg = msg.get("message", {})
83
+ raw_content = user_msg.get("content", "")
84
+ if isinstance(raw_content, str):
85
+ user_content = raw_content
86
+ elif isinstance(raw_content, list):
87
+ texts = [
88
+ b.get("text", "") for b in raw_content
89
+ if isinstance(b, dict) and b.get("type") == "text"
90
+ ]
91
+ user_content = " ".join(texts)
92
+
93
+ session.run_turn(user_content=user_content)
94
+ except Exception as e:
95
+ publisher.publish_result(
96
+ session_id=session.session_id,
97
+ subtype="error_during_execution",
98
+ is_error=True,
99
+ result=str(e),
100
+ )
101
+ sys.exit(1)
102
+
103
+ except KeyboardInterrupt:
104
+ pass
105
+ finally:
106
+ if execution_manager:
107
+ execution_manager.close()
108
+
109
+
110
+ def _error_exit(message: str, code: int = 1) -> NoReturn:
111
+ print(message, file=sys.stderr)
112
+ sys.exit(code)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
core/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ """Core business logic for graph building and execution.
2
+
3
+ This module provides the main entry point for the agent executor service.
4
+ The implementation follows a flat, modular structure:
5
+
6
+ Flat Structure:
7
+ - builder.py: Main GraphBuilder class (entry point)
8
+ - model_identifier.py: Model identifier creation
9
+ - subagent_builder.py: Subagent compilation logic
10
+ - tool_loader.py: Tool loading logic
11
+
12
+ Usage:
13
+ from core import GraphBuilder
14
+
15
+ builder = GraphBuilder()
16
+ agent = builder.build_from_definition(definition)
17
+
18
+ Or use the modular functions directly:
19
+ from core import (
20
+ load_tools_from_definition,
21
+ create_model_identifier,
22
+ build_subagent
23
+ )
24
+ """
25
+
26
+ # Main API
27
+ from core.builder import GraphBuilder, GraphBuilderError
28
+ from core.model_identifier import create_model_identifier
29
+ from core.subagent_builder import SubAgentCompilationError, build_subagent
30
+
31
+ # Modular functions
32
+ from core.tool_loader import ToolLoadingError, load_tools_from_definition
33
+
34
+ __all__ = [
35
+ # Main API
36
+ "GraphBuilder",
37
+ "GraphBuilderError",
38
+
39
+ # Modular functions
40
+ "load_tools_from_definition",
41
+ "create_model_identifier",
42
+ "build_subagent",
43
+
44
+ # Exceptions
45
+ "ToolLoadingError",
46
+ "SubAgentCompilationError"
47
+ ]
core/builder.py ADDED
@@ -0,0 +1,353 @@
1
+ """
2
+ Graph Builder Module for Agent Executor (DEPRECATED).
3
+
4
+ **DEPRECATION NOTICE:**
5
+ This module is DEPRECATED in favor of the new modular architecture.
6
+
7
+ **Use this instead:**
8
+ from deepagents_runtime.core import build_agent_from_definition
9
+ agent = build_agent_from_definition(definition)
10
+
11
+ **Old approach (deprecated):**
12
+ from deepagents_runtime.core import GraphBuilder
13
+ builder = GraphBuilder()
14
+ agent = builder.build_from_definition(definition)
15
+
16
+ This module is kept for backward compatibility but may be removed in future versions.
17
+ The new modular architecture provides:
18
+ - Better separation of concerns
19
+ - Easier testing and maintenance
20
+ - Follows spec-engine pattern
21
+
22
+ New Modular Structure:
23
+ - core/factory.py: Main entry point (build_agent_from_definition)
24
+ - core/tools/: Tool loading logic
25
+ - core/models/: Model identifier creation
26
+ - core/subagents/: Subagent compilation logic
27
+
28
+ Migration Guide:
29
+ Replace GraphBuilder instances with direct factory calls:
30
+
31
+ Before:
32
+ builder = GraphBuilder()
33
+ graph = builder.build_from_definition(definition)
34
+
35
+ After:
36
+ graph = build_agent_from_definition(definition)
37
+
38
+ Classes:
39
+ - GraphBuilder: DEPRECATED - Main class for building LangGraph graphs
40
+
41
+ Security Notes:
42
+ - This module uses exec() to dynamically load tool code. All agent definitions
43
+ MUST come from trusted sources as they involve executing arbitrary Python code.
44
+ - Tools are executed in an isolated namespace, but this does NOT provide
45
+ complete sandboxing. Production deployments should validate all definitions.
46
+
47
+ References:
48
+ - Requirements: Req. 3.1 (Stateful Graph Execution)
49
+ - Design: Section 2.11 (Internal Component Architecture)
50
+ - Tasks: Task 6 (Graph Builder Core Logic)
51
+ """
52
+
53
+ from typing import Any, Dict, List
54
+
55
+ import structlog
56
+ from langchain_core.runnables import Runnable
57
+
58
+ from core.model_identifier import create_model_identifier
59
+ from core.subagent_builder import build_subagent
60
+ from core.tool_loader import load_tools_from_definition
61
+
62
+ # Import deep agents pattern components
63
+ # Note: The spec requires deepagents package with create_deep_agent and CompiledSubAgent
64
+ try:
65
+ from deepagents import create_deep_agent
66
+ except ImportError as e:
67
+ raise ImportError(
68
+ "deepagents package is required but not installed. "
69
+ "Install it with: pip install deepagents>=0.2.0"
70
+ ) from e
71
+
72
+ logger = structlog.get_logger(__name__)
73
+
74
+
75
+ class GraphBuilderError(Exception):
76
+ """Raised when graph building fails."""
77
+ pass
78
+
79
+
80
+ class GraphBuilder:
81
+ """
82
+ Builds LangGraph graphs dynamically from agent definitions.
83
+
84
+ This class is responsible for taking an agent definition (JSON structure)
85
+ and compiling it into a runnable LangGraph graph. The process includes:
86
+ 1. Loading tools from script definitions
87
+ 2. Creating model identifiers for LLM providers
88
+ 3. Compiling sub-agents with their tools and prompts
89
+ 4. Assembling the main orchestrator graph
90
+
91
+ The GraphBuilder reads LLM API keys from environment variables, which are
92
+ populated by Kubernetes Secrets managed by External Secrets Operator.
93
+
94
+ Attributes:
95
+ checkpointer: Optional PostgresSaver instance for checkpoint persistence
96
+
97
+ Example:
98
+ builder = GraphBuilder()
99
+ graph = builder.build_from_definition(agent_definition)
100
+
101
+ # Execute the compiled graph
102
+ result = graph.invoke({"messages": [{"role": "user", "content": "Hello"}]})
103
+ """
104
+
105
+ def __init__(self, checkpointer: Any = None) -> None:
106
+ """
107
+ Initialize GraphBuilder with checkpointer dependency.
108
+
109
+ Args:
110
+ checkpointer: Optional PostgresSaver instance for checkpoint persistence
111
+
112
+ References:
113
+ - Requirements: Req. 3.1, 14.2
114
+ - Design: Section 2.11 (Component Interaction)
115
+ - Tasks: Task 1.1
116
+ """
117
+ self.checkpointer = checkpointer
118
+ logger.info("graph_builder_initialized", has_checkpointer=checkpointer is not None)
119
+
120
+
121
+ def build_from_definition(self, definition: Dict[str, Any]) -> Runnable:
122
+ """
123
+ Build a complete LangGraph graph from an agent definition.
124
+
125
+ This is the main public method of GraphBuilder. It orchestrates the entire
126
+ graph construction process:
127
+ 1. Load all tools from tool definitions
128
+ 2. Parse the graph structure (nodes, edges)
129
+ 3. Identify orchestrator and specialist nodes
130
+ 4. Compile all sub-agents
131
+ 5. Assemble the main orchestrator graph
132
+ 6. Return the compiled runnable
133
+
134
+ The resulting graph can be executed with .invoke() or .stream() methods.
135
+
136
+ Args:
137
+ definition: Complete agent definition dictionary containing:
138
+ - tool_definitions: List of tool script definitions
139
+ - nodes: List of node definitions (orchestrator + specialists)
140
+ - edges: Graph edge definitions
141
+ - initial_state: Optional initial state configuration
142
+
143
+ Returns:
144
+ Compiled Runnable graph ready for execution
145
+
146
+ Raises:
147
+ GraphBuilderError: If graph building fails at any step
148
+
149
+ Example definition structure:
150
+ {
151
+ "tool_definitions": [
152
+ {"name": "web_search", "script": "...", "description": "..."}
153
+ ],
154
+ "nodes": [
155
+ {
156
+ "type": "orchestrator",
157
+ "name": "main_orchestrator",
158
+ "model": {"provider": "openai", "model_name": "gpt-4o"},
159
+ "system_prompt": "You coordinate the specialists...",
160
+ "tools": []
161
+ },
162
+ {
163
+ "type": "specialist",
164
+ "name": "research_specialist",
165
+ "model": {"provider": "openai", "model_name": "gpt-4o"},
166
+ "system_prompt": "You research information...",
167
+ "tools": ["web_search"]
168
+ }
169
+ ],
170
+ "edges": [
171
+ {"from": "orchestrator", "to": "research_specialist"},
172
+ {"from": "research_specialist", "to": "orchestrator"}
173
+ ]
174
+ }
175
+
176
+ References:
177
+ - Requirements: Req. 3.1 (Stateful Graph Execution)
178
+ - Design: Section 3.2 (Core Logic Layer)
179
+ - Tasks: Task 6.5 (Main Graph Builder)
180
+ """
181
+ try:
182
+ logger.info("building_graph_from_definition")
183
+
184
+ # Step 1: Load all tools
185
+ tool_definitions = definition.get("tool_definitions", [])
186
+ available_tools = load_tools_from_definition(tool_definitions)
187
+
188
+ # Step 2: Parse nodes from definition
189
+ nodes = definition.get("nodes", [])
190
+ if not nodes:
191
+ raise GraphBuilderError("Agent definition must contain at least one node")
192
+
193
+ # Step 3: Identify orchestrator and specialist nodes
194
+ orchestrator_config = None
195
+ specialist_configs = []
196
+
197
+ for node in nodes:
198
+ node_type = node.get("type", "specialist").lower()
199
+ if node_type == "orchestrator":
200
+ orchestrator_config = node
201
+ else:
202
+ specialist_configs.append(node)
203
+
204
+ if not orchestrator_config:
205
+ logger.warning("no_orchestrator_found_using_first_node")
206
+ orchestrator_config = nodes[0] if nodes else {}
207
+
208
+ logger.info(
209
+ "graph_structure_parsed",
210
+ total_nodes=len(nodes),
211
+ has_orchestrator=bool(orchestrator_config),
212
+ specialist_count=len(specialist_configs)
213
+ )
214
+
215
+ # Step 4: Build all sub-agents as CompiledSubAgent instances
216
+ compiled_subagents: List[Any] = [] # List[CompiledSubAgent] when deepagents available
217
+
218
+ for specialist_node in specialist_configs:
219
+ # Extract config from node structure
220
+ specialist_config = specialist_node.get("config", {})
221
+ sub_agent = build_subagent(specialist_config, available_tools)
222
+ compiled_subagents.append(sub_agent)
223
+
224
+ logger.info(
225
+ "compiled_subagents",
226
+ count=len(compiled_subagents),
227
+ names=[
228
+ sa.get("name") if isinstance(sa, dict) else getattr(sa, "name", "unknown")
229
+ for sa in compiled_subagents
230
+ ],
231
+ types=[
232
+ "SubAgent_dict" if isinstance(sa, dict) else "CompiledSubAgent"
233
+ for sa in compiled_subagents
234
+ ]
235
+ )
236
+
237
+ # Step 5: Build the main orchestrator agent
238
+ logger.info("building_orchestrator_agent")
239
+
240
+ # Extract orchestrator config from node structure
241
+ orchestrator_actual_config = orchestrator_config.get("config", {})
242
+
243
+ # Extract orchestrator model configuration
244
+ orchestrator_model_config = orchestrator_actual_config.get("model", {})
245
+ orchestrator_provider = orchestrator_model_config.get("provider", "openai")
246
+ # Support both "model_name" and "model" field names
247
+ orchestrator_model_name = orchestrator_model_config.get("model") or orchestrator_model_config.get("model", "gpt-4.1.mini")
248
+ orchestrator_model_identifier = create_model_identifier(
249
+ orchestrator_provider,
250
+ orchestrator_model_name
251
+ )
252
+
253
+ orchestrator_system_prompt = orchestrator_actual_config.get("system_prompt", "")
254
+
255
+ # Extract and resolve orchestrator tools
256
+ orchestrator_tool_names = orchestrator_actual_config.get("tools", [])
257
+ orchestrator_tools = []
258
+
259
+ for tool_name in orchestrator_tool_names:
260
+ if tool_name in available_tools:
261
+ orchestrator_tools.append(available_tools[tool_name])
262
+ else:
263
+ logger.warning(
264
+ "orchestrator_tool_not_found",
265
+ tool_name=tool_name,
266
+ available_tools=list(available_tools.keys())
267
+ )
268
+
269
+ # Log orchestrator configuration for verification
270
+ logger.info(
271
+ "orchestrator_config_extracted",
272
+ orchestrator_name=orchestrator_actual_config.get("name", "unknown"),
273
+ model_identifier=orchestrator_model_identifier,
274
+ system_prompt_length=len(orchestrator_system_prompt),
275
+ system_prompt_preview=orchestrator_system_prompt[:200] if orchestrator_system_prompt else "EMPTY",
276
+ requested_tools=orchestrator_tool_names,
277
+ resolved_tools=len(orchestrator_tools),
278
+ tool_names=[t.name if hasattr(t, 'name') else str(t) for t in orchestrator_tools],
279
+ has_task_tool_instruction="task()" in orchestrator_system_prompt
280
+ )
281
+
282
+ # Step 6: Assemble the main graph using create_deep_agent
283
+ # Use create_deep_agent with the list of CompiledSubAgent instances
284
+ logger.info(
285
+ "creating_deep_agent",
286
+ orchestrator_model=orchestrator_model_identifier,
287
+ subagent_count=len(compiled_subagents),
288
+ subagent_names=[
289
+ sa.get("name") if isinstance(sa, dict) else getattr(sa, "name", "unknown")
290
+ for sa in compiled_subagents
291
+ ],
292
+ subagent_types=[
293
+ type(sa).__name__ if not isinstance(sa, dict) else "dict"
294
+ for sa in compiled_subagents
295
+ ],
296
+ has_checkpointer=self.checkpointer is not None
297
+ )
298
+
299
+ # Log detailed subagent info for debugging
300
+ for i, sa in enumerate(compiled_subagents):
301
+ if isinstance(sa, dict):
302
+ logger.info(
303
+ f"subagent_{i}_details",
304
+ name=sa.get("name"),
305
+ description=sa.get("description", "")[:100],
306
+ has_system_prompt=bool(sa.get("system_prompt")),
307
+ has_tools=len(sa.get("tools", [])),
308
+ model=sa.get("model")
309
+ )
310
+
311
+ # Initialize the model object from the identifier string
312
+ # create_deep_agent expects a model object, not a string
313
+ # Use ModelFactory for clean separation of mock vs real models
314
+ from core.model_factory import ModelFactory
315
+ orchestrator_model = ModelFactory.create_model()
316
+ logger.info("model_created_via_factory", model_type=type(orchestrator_model).__name__)
317
+
318
+ main_runnable = create_deep_agent(
319
+ model=orchestrator_model,
320
+ system_prompt=orchestrator_system_prompt,
321
+ tools=orchestrator_tools, # Pass resolved orchestrator tools
322
+ subagents=compiled_subagents, # List of CompiledSubAgent and SubAgent dict instances
323
+ checkpointer=self.checkpointer, # Pass checkpointer for state persistence
324
+ )
325
+
326
+ # Debug: Check if the graph has the expected structure
327
+ logger.info(
328
+ "create_deep_agent_result",
329
+ runnable_type=type(main_runnable).__name__,
330
+ has_nodes=hasattr(main_runnable, 'nodes'),
331
+ node_count=len(getattr(main_runnable, 'nodes', {})) if hasattr(main_runnable, 'nodes') else 0
332
+ )
333
+
334
+ logger.info(
335
+ "graph_built_successfully",
336
+ orchestrator_name=orchestrator_actual_config.get("name", "main"),
337
+ orchestrator_model=orchestrator_model_identifier,
338
+ sub_agent_count=len(compiled_subagents),
339
+ total_tools=len(available_tools),
340
+ graph_type="deep_agent"
341
+ )
342
+
343
+ return main_runnable
344
+
345
+
346
+
347
+ except Exception as e:
348
+ logger.error(
349
+ "graph_building_failed",
350
+ error=str(e),
351
+ error_type=type(e).__name__
352
+ )
353
+ raise GraphBuilderError(f"Graph building failed: {e}") from e
@@ -0,0 +1,108 @@
1
+ import json
2
+ import sys
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Optional
5
+
6
+ from models.frames import (
7
+ AssistantFrame,
8
+ ControlResponseFrame,
9
+ OutgoingFrame,
10
+ ResultFrame,
11
+ StreamEventFrame,
12
+ SystemInitFrame,
13
+ UserEchoFrame,
14
+ frame_to_dict,
15
+ )
16
+
17
+
18
+ class EventPublisher(ABC):
19
+ @abstractmethod
20
+ def publish_system_init(self, *, session_id: str, model: str,
21
+ tools: Optional[list[dict[str, Any]]] = None) -> None:
22
+ ...
23
+
24
+ @abstractmethod
25
+ def publish_assistant(self, *, session_id: str, model: str,
26
+ content: list[dict[str, Any]],
27
+ parent_tool_use_id: Optional[str] = None) -> None:
28
+ ...
29
+
30
+ @abstractmethod
31
+ def publish_user_echo(self, *, session_id: str,
32
+ content: list[dict[str, Any]]) -> None:
33
+ ...
34
+
35
+ @abstractmethod
36
+ def publish_stream_event_text(self, *, session_id: str,
37
+ text: str, index: int = 0) -> None:
38
+ ...
39
+
40
+ @abstractmethod
41
+ def publish_result(self, *, session_id: str, subtype: str = "success",
42
+ duration_ms: int = 0, is_error: bool = False,
43
+ num_turns: int = 1, result: Optional[str] = None) -> None:
44
+ ...
45
+
46
+ @abstractmethod
47
+ def publish_control_response(self, *, request_id: str,
48
+ subtype: str = "success", **extra: Any) -> None:
49
+ ...
50
+
51
+
52
+ class StdioPublisher(EventPublisher):
53
+ def _write(self, frame: OutgoingFrame) -> None:
54
+ sys.stdout.write(json.dumps(frame_to_dict(frame), default=str) + "\n")
55
+ sys.stdout.flush()
56
+
57
+ def publish_system_init(self, *, session_id: str, model: str,
58
+ tools: Optional[list[dict[str, Any]]] = None) -> None:
59
+ self._write(SystemInitFrame(
60
+ session_id=session_id,
61
+ model=model,
62
+ tools=tools or [],
63
+ ))
64
+
65
+ def publish_assistant(self, *, session_id: str, model: str,
66
+ content: list[dict[str, Any]],
67
+ parent_tool_use_id: Optional[str] = None) -> None:
68
+ self._write(AssistantFrame.build(
69
+ session_id=session_id,
70
+ model=model,
71
+ content=content,
72
+ parent_tool_use_id=parent_tool_use_id,
73
+ ))
74
+
75
+ def publish_user_echo(self, *, session_id: str,
76
+ content: list[dict[str, Any]]) -> None:
77
+ self._write(UserEchoFrame.build(
78
+ session_id=session_id,
79
+ content=content,
80
+ ))
81
+
82
+ def publish_stream_event_text(self, *, session_id: str,
83
+ text: str, index: int = 0) -> None:
84
+ self._write(StreamEventFrame.text_delta(
85
+ session_id=session_id,
86
+ text=text,
87
+ index=index,
88
+ ))
89
+
90
+ def publish_result(self, *, session_id: str, subtype: str = "success",
91
+ duration_ms: int = 0, is_error: bool = False,
92
+ num_turns: int = 1, result: Optional[str] = None) -> None:
93
+ self._write(ResultFrame(
94
+ subtype=subtype,
95
+ session_id=session_id,
96
+ duration_ms=duration_ms,
97
+ duration_api_ms=duration_ms,
98
+ is_error=is_error,
99
+ num_turns=num_turns,
100
+ result=result,
101
+ ))
102
+
103
+ def publish_control_response(self, *, request_id: str,
104
+ subtype: str = "success", **extra: Any) -> None:
105
+ self._write(ControlResponseFrame.success(
106
+ request_id=request_id,
107
+ **extra,
108
+ ))