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 +116 -0
- core/__init__.py +47 -0
- core/builder.py +353 -0
- core/event_publisher.py +108 -0
- core/executor.py +227 -0
- core/model_factory.py +88 -0
- core/model_identifier.py +77 -0
- core/session.py +60 -0
- core/state_schema_builder.py +109 -0
- core/subagent_builder.py +264 -0
- core/tool_loader.py +158 -0
- harness_runtime-0.1.0.dist-info/METADATA +136 -0
- harness_runtime-0.1.0.dist-info/RECORD +18 -0
- harness_runtime-0.1.0.dist-info/WHEEL +5 -0
- harness_runtime-0.1.0.dist-info/entry_points.txt +2 -0
- harness_runtime-0.1.0.dist-info/top_level.txt +3 -0
- models/__init__.py +35 -0
- models/frames.py +156 -0
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
|
core/event_publisher.py
ADDED
|
@@ -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
|
+
))
|