odl-kernel 0.9.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.
- odl_kernel/__init__.py +46 -0
- odl_kernel/engine/analyzer.py +474 -0
- odl_kernel/engine/logic/expansion/__init__.py +28 -0
- odl_kernel/engine/logic/expansion/base.py +75 -0
- odl_kernel/engine/logic/expansion/iterate.py +183 -0
- odl_kernel/engine/logic/expansion/loop.py +119 -0
- odl_kernel/engine/logic/expansion/parallel.py +88 -0
- odl_kernel/engine/logic/expansion/serial.py +103 -0
- odl_kernel/engine/logic/introspection.py +181 -0
- odl_kernel/engine/logic/node_id_generator.py +74 -0
- odl_kernel/engine/logic/transition_rules.py +334 -0
- odl_kernel/engine/logic/variable_resolver.py +180 -0
- odl_kernel/types/__init__.py +74 -0
- odl_kernel/types/entities.py +103 -0
- odl_kernel/types/enums.py +49 -0
- odl_kernel/types/structs/__init__.py +30 -0
- odl_kernel/types/structs/input_intervention_intent.py +62 -0
- odl_kernel/types/structs/input_job_snapshot.py +32 -0
- odl_kernel/types/structs/input_kernel_event.py +35 -0
- odl_kernel/types/structs/input_kernel_event_type.py +101 -0
- odl_kernel/types/structs/output_analysis_result.py +33 -0
- odl_kernel/types/structs/output_command_type.py +115 -0
- odl_kernel/types/structs/output_job_update.py +27 -0
- odl_kernel/types/structs/output_runtime_command.py +25 -0
- odl_kernel-0.9.0.dist-info/METADATA +107 -0
- odl_kernel-0.9.0.dist-info/RECORD +28 -0
- odl_kernel-0.9.0.dist-info/WHEEL +4 -0
- odl_kernel-0.9.0.dist-info/licenses/LICENSE +62 -0
odl_kernel/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copyright (c) 2026 Centillion System, Inc. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the Business Source License 1.1 (BSL 1.1).
|
|
4
|
+
# For full license terms, see the LICENSE file in the root directory.
|
|
5
|
+
#
|
|
6
|
+
# This software is "Source Available" but contains certain restrictions on
|
|
7
|
+
# commercial use and managed service provision.
|
|
8
|
+
# Usage by organizations with gross revenue exceeding $10M USD requires
|
|
9
|
+
# a commercial license.
|
|
10
|
+
|
|
11
|
+
from .types import (
|
|
12
|
+
JobSnapshot,
|
|
13
|
+
KernelEvent,
|
|
14
|
+
AnalysisResult
|
|
15
|
+
)
|
|
16
|
+
from .engine.analyzer import OdlAnalyzer
|
|
17
|
+
from .engine.logic.introspection import NodeInspector
|
|
18
|
+
|
|
19
|
+
# Main Entry Point
|
|
20
|
+
def analyze(snapshot: JobSnapshot, event: KernelEvent) -> AnalysisResult:
|
|
21
|
+
"""
|
|
22
|
+
ODLカーネルのメインエントリポイント。
|
|
23
|
+
物理法則に基づき、入力されたSnapshotとEventから次のアクションを導出する純粋関数。
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
import odl_kernel
|
|
27
|
+
result = odl_kernel.analyze(snapshot, event)
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
snapshot: 現在のジョブの状態 (DBからロード済み)
|
|
31
|
+
event: トリガーとなった事象 (Tick, ActionCompleted等)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
AnalysisResult: 実行すべきコマンドと、更新されたノード状態
|
|
35
|
+
"""
|
|
36
|
+
# Analyzerはステートレスなので、都度インスタンス化して実行する
|
|
37
|
+
# これにより odl.compile(...) と同じような関数的な使い心地を提供する
|
|
38
|
+
return OdlAnalyzer().analyze(snapshot, event)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"analyze",
|
|
42
|
+
"JobSnapshot",
|
|
43
|
+
"KernelEvent",
|
|
44
|
+
"AnalysisResult",
|
|
45
|
+
"NodeInspector"
|
|
46
|
+
]
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
# Copyright (c) 2026 Centillion System, Inc. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the Business Source License 1.1 (BSL 1.1).
|
|
4
|
+
# For full license terms, see the LICENSE file in the root directory.
|
|
5
|
+
#
|
|
6
|
+
# This software is "Source Available" but contains certain restrictions on
|
|
7
|
+
# commercial use and managed service provision.
|
|
8
|
+
# Usage by organizations with gross revenue exceeding $10M USD requires
|
|
9
|
+
# a commercial license.
|
|
10
|
+
|
|
11
|
+
from typing import List, Dict, Any, Set, Optional
|
|
12
|
+
from odl.types import OpCode, NodeType, WiringObject
|
|
13
|
+
|
|
14
|
+
from odl_kernel.types import (
|
|
15
|
+
JobSnapshot, KernelEvent, AnalysisResult,
|
|
16
|
+
RuntimeCommand, CommandType, ProcessNode,
|
|
17
|
+
LifecycleStatus, JobStatus, JobUpdate,
|
|
18
|
+
ContextSchema, KernelEventType, BusinessResult
|
|
19
|
+
)
|
|
20
|
+
from .logic.node_id_generator import NodeIdGenerator
|
|
21
|
+
from .logic.transition_rules import TransitionRules
|
|
22
|
+
from .logic.variable_resolver import VariableResolver
|
|
23
|
+
|
|
24
|
+
# Strategies
|
|
25
|
+
from .logic.expansion.serial import SerialExpansionStrategy
|
|
26
|
+
from .logic.expansion.parallel import ParallelExpansionStrategy
|
|
27
|
+
from .logic.expansion.loop import LoopExpansionStrategy
|
|
28
|
+
from .logic.expansion.iterate import IterateExpansionStrategy
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OdlAnalyzer:
|
|
32
|
+
"""
|
|
33
|
+
ODL実行の中核となる純粋関数エンジン (The Physics Engine)。
|
|
34
|
+
|
|
35
|
+
[Architecture: Deep Analyze & Signal/State Separation]
|
|
36
|
+
|
|
37
|
+
1. Deep Analyze (Fixed-point Iteration):
|
|
38
|
+
本関数は、入力された状態に対し、物理的に進行可能な限界(Action待ち or 完全停止)まで
|
|
39
|
+
内部でサイクルを回し続け、収束した結果を一括で返却する。
|
|
40
|
+
これにより、Hostは「過渡状態」を意識することなく、常に安定した状態のみを観測できる。
|
|
41
|
+
|
|
42
|
+
2. Signal (Command) vs State (UpdatedNodes):
|
|
43
|
+
- updated_nodes: 状態の「真実(Truth)」。Hostはこれを無条件にDB保存する。
|
|
44
|
+
- commands: 状態変化に伴う「シグナル」。Payloadは最小化され、分岐判断やログ通知に使用される。
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# 無限ループ(物理崩壊)防止用の安全装置
|
|
48
|
+
MAX_ITERATIONS = 100
|
|
49
|
+
|
|
50
|
+
def analyze(self, snapshot: JobSnapshot, event: KernelEvent) -> AnalysisResult:
|
|
51
|
+
"""
|
|
52
|
+
Main Entry Point: 物理法則に基づき、入力されたSnapshotとEventから次のアクションを導出する。
|
|
53
|
+
"""
|
|
54
|
+
final_result = AnalysisResult()
|
|
55
|
+
|
|
56
|
+
# Working Memory (Deep Copy)
|
|
57
|
+
# 計算中の状態変更を即座に反映させるため、ミュータブルな辞書として展開する
|
|
58
|
+
working_nodes: Dict[str, ProcessNode] = {
|
|
59
|
+
k: v.model_copy(deep=True) for k, v in snapshot.nodes.items()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# -----------------------------------------------------------
|
|
63
|
+
# 0. Global Suppression Check
|
|
64
|
+
# -----------------------------------------------------------
|
|
65
|
+
if snapshot.job.status == JobStatus.STOPPING:
|
|
66
|
+
self._evaluate_suppression(snapshot, working_nodes, final_result)
|
|
67
|
+
return final_result
|
|
68
|
+
|
|
69
|
+
# -----------------------------------------------------------
|
|
70
|
+
# 1. Root Bootstrapping (Initial Spawn)
|
|
71
|
+
# -----------------------------------------------------------
|
|
72
|
+
if not working_nodes and snapshot.job.status == JobStatus.RUNNING:
|
|
73
|
+
self._bootstrap_root(snapshot, working_nodes, final_result)
|
|
74
|
+
# Root生成も状態変化の一種なので、ここからメインループに入る
|
|
75
|
+
|
|
76
|
+
# -----------------------------------------------------------
|
|
77
|
+
# 2. Main Deep Loop (Fixed-point Iteration)
|
|
78
|
+
# -----------------------------------------------------------
|
|
79
|
+
# cocrea-2201: Pipelined Analyze (Step A -> B -> C)
|
|
80
|
+
for _ in range(self.MAX_ITERATIONS):
|
|
81
|
+
has_change = False
|
|
82
|
+
|
|
83
|
+
# --- Step A: Transition (State Update) ---
|
|
84
|
+
# 完了判定、タイムアウト、WakeUp(Pending->Running)などを処理
|
|
85
|
+
active_nodes = [
|
|
86
|
+
n for n in working_nodes.values()
|
|
87
|
+
if n.lifecycle_status in (LifecycleStatus.PENDING, LifecycleStatus.RUNNING)
|
|
88
|
+
]
|
|
89
|
+
for node in active_nodes:
|
|
90
|
+
if self._step_a_transition(node, working_nodes, event, final_result):
|
|
91
|
+
has_change = True
|
|
92
|
+
|
|
93
|
+
# --- Step B: Expansion (Structure Growth) ---
|
|
94
|
+
# Controlノードによる子ノード生成
|
|
95
|
+
control_nodes = [
|
|
96
|
+
n for n in working_nodes.values()
|
|
97
|
+
if n.node_type == NodeType.CONTROL
|
|
98
|
+
and n.lifecycle_status == LifecycleStatus.RUNNING
|
|
99
|
+
]
|
|
100
|
+
for node in control_nodes:
|
|
101
|
+
if self._step_b_expansion(node, snapshot.job, working_nodes, final_result):
|
|
102
|
+
has_change = True
|
|
103
|
+
|
|
104
|
+
# --- Step C: Dispatch (External Trigger) ---
|
|
105
|
+
# 新規生成されたActionノードの実行開始
|
|
106
|
+
pending_nodes = [
|
|
107
|
+
n for n in working_nodes.values()
|
|
108
|
+
if n.lifecycle_status == LifecycleStatus.PENDING
|
|
109
|
+
]
|
|
110
|
+
for node in pending_nodes:
|
|
111
|
+
if self._step_c_dispatch(node, working_nodes, final_result):
|
|
112
|
+
has_change = True
|
|
113
|
+
|
|
114
|
+
# --- Convergence Check ---
|
|
115
|
+
# 変化がなくなれば安定状態とみなして終了
|
|
116
|
+
if not has_change:
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
# -----------------------------------------------------------
|
|
120
|
+
# 3. Finalization
|
|
121
|
+
# -----------------------------------------------------------
|
|
122
|
+
self._evaluate_job_status(snapshot.job, working_nodes, final_result)
|
|
123
|
+
|
|
124
|
+
# -----------------------------------------------------------
|
|
125
|
+
# 4. Packaging (Unique Updates)
|
|
126
|
+
# -----------------------------------------------------------
|
|
127
|
+
# working_nodes は常に最新の状態を持っている。
|
|
128
|
+
# final_result.updated_nodes に記録されたID(変更があったもの)について、
|
|
129
|
+
# working_nodes から最新のオブジェクトを取り直して返却する。
|
|
130
|
+
touched_ids = {n.node_id for n in final_result.updated_nodes}
|
|
131
|
+
final_result.updated_nodes = [working_nodes[nid] for nid in touched_ids]
|
|
132
|
+
|
|
133
|
+
return final_result
|
|
134
|
+
|
|
135
|
+
# =========================================================================
|
|
136
|
+
# Internal Steps
|
|
137
|
+
# =========================================================================
|
|
138
|
+
|
|
139
|
+
def _bootstrap_root(self, snapshot: JobSnapshot, nodes: Dict[str, ProcessNode], result: AnalysisResult):
|
|
140
|
+
"""Rootノードの初期生成"""
|
|
141
|
+
ir = snapshot.job.ir_root
|
|
142
|
+
id_gen = NodeIdGenerator(snapshot.job.job_id)
|
|
143
|
+
node_id = id_gen.generate(ir.stack_path)
|
|
144
|
+
|
|
145
|
+
# 1. State Update (Truth)
|
|
146
|
+
root_node = self._simulate_spawn(
|
|
147
|
+
node_id=node_id,
|
|
148
|
+
blueprint=ir,
|
|
149
|
+
resolved_path=ir.stack_path,
|
|
150
|
+
context_vars={},
|
|
151
|
+
parent_context=None
|
|
152
|
+
)
|
|
153
|
+
nodes[node_id] = root_node
|
|
154
|
+
result.updated_nodes.append(root_node)
|
|
155
|
+
|
|
156
|
+
# 2. Command (Signal)
|
|
157
|
+
cmd = RuntimeCommand(
|
|
158
|
+
type=CommandType.SPAWN_CHILD,
|
|
159
|
+
target_node_id=snapshot.job.root_node_id or node_id,
|
|
160
|
+
payload={
|
|
161
|
+
"child_node_id": node_id,
|
|
162
|
+
"is_root": True,
|
|
163
|
+
"child_opcode": ir.opcode,
|
|
164
|
+
"parent_opcode": None
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
result.commands.append(cmd)
|
|
168
|
+
|
|
169
|
+
def _step_a_transition(self, node: ProcessNode, nodes: Dict[str, ProcessNode], event: KernelEvent, result: AnalysisResult) -> bool:
|
|
170
|
+
"""Step A: 状態遷移ロジックの評価と適用"""
|
|
171
|
+
|
|
172
|
+
# 1-A. External Event Handling (Action Completion)
|
|
173
|
+
if event.type == KernelEventType.ACTION_COMPLETED and event.target_node_id == node.node_id:
|
|
174
|
+
# ... (既存コード: ACTION_COMPLETED処理) ...
|
|
175
|
+
if node.lifecycle_status == LifecycleStatus.COMPLETED:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
from_status = node.lifecycle_status
|
|
179
|
+
|
|
180
|
+
node.lifecycle_status = LifecycleStatus.COMPLETED
|
|
181
|
+
node.business_result = event.payload.get("result", BusinessResult.NONE)
|
|
182
|
+
|
|
183
|
+
output_data = event.payload.get("output_data")
|
|
184
|
+
if output_data:
|
|
185
|
+
node.runtime_context.output_aggregation.append(output_data)
|
|
186
|
+
|
|
187
|
+
result.updated_nodes.append(node)
|
|
188
|
+
|
|
189
|
+
signal_cmd = RuntimeCommand(
|
|
190
|
+
type=CommandType.FINALIZE,
|
|
191
|
+
target_node_id=node.node_id,
|
|
192
|
+
payload={
|
|
193
|
+
"from_status": from_status,
|
|
194
|
+
"to_status": LifecycleStatus.COMPLETED,
|
|
195
|
+
"result": node.business_result
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
result.commands.append(signal_cmd)
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
# 1-B. External Event Handling (Data Resolved) [UPDATED]
|
|
202
|
+
if event.type == KernelEventType.DATA_RESOLVED and event.target_node_id == node.node_id:
|
|
203
|
+
if node.lifecycle_status == LifecycleStatus.COMPLETED:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
# [Physics Fix] Idempotency Check
|
|
207
|
+
if node.runtime_context.output_aggregation:
|
|
208
|
+
pass
|
|
209
|
+
else:
|
|
210
|
+
items = event.payload.get("items")
|
|
211
|
+
resolved_id = event.payload.get("resolved_id")
|
|
212
|
+
|
|
213
|
+
updated = False
|
|
214
|
+
if items is not None:
|
|
215
|
+
node.runtime_context.output_aggregation.append(items)
|
|
216
|
+
updated = True
|
|
217
|
+
elif resolved_id is not None:
|
|
218
|
+
node.runtime_context.output_aggregation.append(resolved_id)
|
|
219
|
+
updated = True
|
|
220
|
+
|
|
221
|
+
if updated:
|
|
222
|
+
# [Fix] データ解決したので、待機フラグを下ろす
|
|
223
|
+
node.runtime_context.system_variables.pop("__waiting_for_data", None)
|
|
224
|
+
|
|
225
|
+
result.updated_nodes.append(node)
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
# 2. Internal Rule Evaluation
|
|
229
|
+
children = [nodes[cid] for cid in node.runtime_context.children_ids if cid in nodes]
|
|
230
|
+
logic_cmd = TransitionRules.evaluate(node, children, event.occurred_at)
|
|
231
|
+
|
|
232
|
+
if logic_cmd:
|
|
233
|
+
from_status = node.lifecycle_status
|
|
234
|
+
|
|
235
|
+
# State Update Application
|
|
236
|
+
if logic_cmd.type == CommandType.TRANSITION:
|
|
237
|
+
node.lifecycle_status = logic_cmd.payload["to_status"]
|
|
238
|
+
elif logic_cmd.type == CommandType.FINALIZE:
|
|
239
|
+
node.lifecycle_status = LifecycleStatus.COMPLETED
|
|
240
|
+
node.business_result = logic_cmd.payload["result"]
|
|
241
|
+
|
|
242
|
+
elif logic_cmd.type == CommandType.REQUIRE_DATA:
|
|
243
|
+
# [Fix] 要求を出したので、待機フラグを立てる (State Mutation)
|
|
244
|
+
# これにより、次回のEvaluateで二重発行が抑制され、ループが収束する
|
|
245
|
+
node.runtime_context.system_variables["__waiting_for_data"] = True
|
|
246
|
+
|
|
247
|
+
result.updated_nodes.append(node)
|
|
248
|
+
|
|
249
|
+
# Signal
|
|
250
|
+
signal_payload = {
|
|
251
|
+
"from_status": from_status,
|
|
252
|
+
"to_status": node.lifecycle_status
|
|
253
|
+
}
|
|
254
|
+
if logic_cmd.payload:
|
|
255
|
+
signal_payload.update(logic_cmd.payload)
|
|
256
|
+
|
|
257
|
+
signal_cmd = RuntimeCommand(
|
|
258
|
+
type=logic_cmd.type,
|
|
259
|
+
target_node_id=node.node_id,
|
|
260
|
+
payload=signal_payload
|
|
261
|
+
)
|
|
262
|
+
result.commands.append(signal_cmd)
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
def _step_b_expansion(self, node: ProcessNode, job, nodes: Dict[str, ProcessNode], result: AnalysisResult) -> bool:
|
|
268
|
+
"""Step B: 子ノードの展開ロジック"""
|
|
269
|
+
strategy = self._select_strategy(node.opcode)
|
|
270
|
+
if not strategy: return False
|
|
271
|
+
|
|
272
|
+
children = [nodes[cid] for cid in node.runtime_context.children_ids if cid in nodes]
|
|
273
|
+
plans = strategy.plan_next_nodes(node, children)
|
|
274
|
+
if not plans: return False
|
|
275
|
+
|
|
276
|
+
id_gen = NodeIdGenerator(job.job_id)
|
|
277
|
+
has_spawn = False
|
|
278
|
+
|
|
279
|
+
for plan in plans:
|
|
280
|
+
new_id = id_gen.generate(plan.resolved_path)
|
|
281
|
+
|
|
282
|
+
# 1. State Update (Truth)
|
|
283
|
+
# Strategyの計画に加え、親のContextを継承・シフトさせて生成する
|
|
284
|
+
# [Update] params_override を渡す
|
|
285
|
+
new_node = self._simulate_spawn(
|
|
286
|
+
node_id=new_id,
|
|
287
|
+
blueprint=plan.blueprint,
|
|
288
|
+
resolved_path=plan.resolved_path,
|
|
289
|
+
context_vars=plan.context_vars,
|
|
290
|
+
parent_context=node.runtime_context,
|
|
291
|
+
params_override=plan.params_override # <--- NEW
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Link Parent -> Child
|
|
295
|
+
node.runtime_context.children_ids.append(new_id)
|
|
296
|
+
nodes[new_id] = new_node
|
|
297
|
+
|
|
298
|
+
result.updated_nodes.append(new_node)
|
|
299
|
+
result.updated_nodes.append(node)
|
|
300
|
+
|
|
301
|
+
# 2. Command (Signal)
|
|
302
|
+
cmd = RuntimeCommand(
|
|
303
|
+
type=CommandType.SPAWN_CHILD,
|
|
304
|
+
target_node_id=node.node_id,
|
|
305
|
+
payload={
|
|
306
|
+
"child_node_id": new_id,
|
|
307
|
+
"blueprint_selector": plan.original_index,
|
|
308
|
+
"parent_opcode": node.opcode,
|
|
309
|
+
"child_opcode": plan.blueprint.opcode
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
result.commands.append(cmd)
|
|
313
|
+
has_spawn = True
|
|
314
|
+
|
|
315
|
+
return has_spawn
|
|
316
|
+
|
|
317
|
+
def _step_c_dispatch(self, node: ProcessNode, nodes: Dict[str, ProcessNode], result: AnalysisResult) -> bool:
|
|
318
|
+
"""Step C: 実行指示ロジック"""
|
|
319
|
+
# PENDING以外は無視
|
|
320
|
+
if node.lifecycle_status != LifecycleStatus.PENDING:
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
# Control/Logic Nodeは自己駆動するためDispatch対象外
|
|
324
|
+
if node.node_type in (NodeType.CONTROL, NodeType.LOGIC):
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
# 1. State Update (Truth)
|
|
328
|
+
# DispatchされたノードはHost側で即座にRUNNINGにされる前提
|
|
329
|
+
node.lifecycle_status = LifecycleStatus.RUNNING
|
|
330
|
+
result.updated_nodes.append(node)
|
|
331
|
+
|
|
332
|
+
# 2. Command (Signal)
|
|
333
|
+
cmd = RuntimeCommand(
|
|
334
|
+
type=CommandType.DISPATCH,
|
|
335
|
+
target_node_id=node.node_id,
|
|
336
|
+
payload={}
|
|
337
|
+
)
|
|
338
|
+
result.commands.append(cmd)
|
|
339
|
+
|
|
340
|
+
return True
|
|
341
|
+
|
|
342
|
+
# =========================================================================
|
|
343
|
+
# Helpers
|
|
344
|
+
# =========================================================================
|
|
345
|
+
|
|
346
|
+
def _simulate_spawn(
|
|
347
|
+
self,
|
|
348
|
+
node_id: str,
|
|
349
|
+
blueprint,
|
|
350
|
+
resolved_path: str,
|
|
351
|
+
context_vars: Dict[str, Any] = None,
|
|
352
|
+
parent_context: ContextSchema = None,
|
|
353
|
+
params_override: Dict[str, Any] = None # <--- [NEW] 追加引数
|
|
354
|
+
) -> ProcessNode:
|
|
355
|
+
"""
|
|
356
|
+
[Physics Helper] 完全なProcessNodeオブジェクトをメモリ上に生成する。
|
|
357
|
+
|
|
358
|
+
Physics Rules:
|
|
359
|
+
1. Context Inheritance & Shifting:
|
|
360
|
+
親の変数を継承し、ネストされたループ変数 ($LOOP -> $LOOP^1) をシフトする。
|
|
361
|
+
2. Variable Override:
|
|
362
|
+
Strategyが指定した新しい変数 ($LOOP, $KEY) で上書きする。
|
|
363
|
+
3. Params Override:
|
|
364
|
+
Strategyが指定した動的パラメータ (items等) で静的定義を上書きする。
|
|
365
|
+
4. Wiring Resolution:
|
|
366
|
+
コンテキストに基づき、BlueprintのWiring定義内の変数を解決・フィルタリングする。
|
|
367
|
+
"""
|
|
368
|
+
# 0. Extract Children Blueprints
|
|
369
|
+
children_bps = []
|
|
370
|
+
if blueprint.children:
|
|
371
|
+
children_bps = blueprint.children
|
|
372
|
+
elif blueprint.contents:
|
|
373
|
+
children_bps = [blueprint.contents]
|
|
374
|
+
|
|
375
|
+
# 1. Inherit & Shift System Variables
|
|
376
|
+
inherited_sys = {}
|
|
377
|
+
inherited_user = {}
|
|
378
|
+
|
|
379
|
+
if parent_context:
|
|
380
|
+
inherited_user = parent_context.user_variables.copy()
|
|
381
|
+
|
|
382
|
+
# [Logic] Context Key Shifting
|
|
383
|
+
# 新しいコンテキスト変数が「$LOOP」や「$KEY」を注入しようとしている場合(= 新しいスコープ)、
|
|
384
|
+
# 親から継承する $LOOP 系変数の深度をシフトする。
|
|
385
|
+
should_shift = context_vars and ("$LOOP" in context_vars or "$KEY" in context_vars)
|
|
386
|
+
|
|
387
|
+
for k, v in parent_context.system_variables.items():
|
|
388
|
+
if should_shift and k.startswith("$LOOP"):
|
|
389
|
+
# $LOOP -> $LOOP^1
|
|
390
|
+
if k == "$LOOP":
|
|
391
|
+
inherited_sys["$LOOP^1"] = v
|
|
392
|
+
else:
|
|
393
|
+
# $LOOP^N -> $LOOP^{N+1}
|
|
394
|
+
try:
|
|
395
|
+
parts = k.split("^")
|
|
396
|
+
if len(parts) == 2:
|
|
397
|
+
depth = int(parts[1])
|
|
398
|
+
inherited_sys[f"$LOOP^{depth+1}"] = v
|
|
399
|
+
except ValueError:
|
|
400
|
+
pass # 形式不正な変数は無視
|
|
401
|
+
else:
|
|
402
|
+
# その他の変数はそのまま継承
|
|
403
|
+
inherited_sys[k] = v
|
|
404
|
+
|
|
405
|
+
# 2. Override with Plan (Strategy) vars
|
|
406
|
+
if context_vars:
|
|
407
|
+
inherited_sys.update(context_vars)
|
|
408
|
+
|
|
409
|
+
# Context初期化
|
|
410
|
+
ctx = ContextSchema(
|
|
411
|
+
system_variables=inherited_sys,
|
|
412
|
+
user_variables=inherited_user
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# 3. Parameters Merge [NEW]
|
|
416
|
+
# Blueprintの静的パラメータをベースに、Overrideを適用する。
|
|
417
|
+
# Blueprint自体は変更せず、コピーに対して操作する。
|
|
418
|
+
merged_params = blueprint.params.copy()
|
|
419
|
+
if params_override:
|
|
420
|
+
merged_params.update(params_override)
|
|
421
|
+
|
|
422
|
+
# 4. Resolve Wiring
|
|
423
|
+
# 解決済みのコンテキスト(ctx.system_variables)を使ってWiringを解決する
|
|
424
|
+
# これにより {$LOOP-1} 等が物理的な値に置換され、無効な参照は除外される
|
|
425
|
+
raw_wiring = blueprint.wiring or WiringObject()
|
|
426
|
+
resolved_wiring = VariableResolver.resolve_wiring(raw_wiring, ctx.system_variables)
|
|
427
|
+
|
|
428
|
+
return ProcessNode(
|
|
429
|
+
node_id=node_id,
|
|
430
|
+
stack_path=resolved_path,
|
|
431
|
+
node_type=blueprint.node_type,
|
|
432
|
+
opcode=blueprint.opcode,
|
|
433
|
+
wiring=resolved_wiring, # Resolved
|
|
434
|
+
params=merged_params, # <--- [NEW] Merged Paramsを使用
|
|
435
|
+
children_blueprint=children_bps,
|
|
436
|
+
lifecycle_status=LifecycleStatus.PENDING,
|
|
437
|
+
runtime_context=ctx
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
def _select_strategy(self, opcode: OpCode):
|
|
441
|
+
if opcode == OpCode.SERIAL: return SerialExpansionStrategy()
|
|
442
|
+
elif opcode == OpCode.PARALLEL: return ParallelExpansionStrategy()
|
|
443
|
+
elif opcode == OpCode.LOOP: return LoopExpansionStrategy()
|
|
444
|
+
elif opcode == OpCode.ITERATE: return IterateExpansionStrategy()
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
def _evaluate_suppression(self, snapshot, nodes, result):
|
|
448
|
+
pass # 将来的な実装用
|
|
449
|
+
|
|
450
|
+
def _evaluate_job_status(self, job, nodes, result):
|
|
451
|
+
"""ジョブ全体のステータス遷移判定"""
|
|
452
|
+
|
|
453
|
+
# 1. 鎮火判定 (STOPPING -> CANCELLED)
|
|
454
|
+
if job.status == JobStatus.STOPPING:
|
|
455
|
+
running = [n for n in nodes.values() if n.lifecycle_status == LifecycleStatus.RUNNING]
|
|
456
|
+
if not running:
|
|
457
|
+
result.job_update = JobUpdate(status=JobStatus.CANCELLED)
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
# 2. 完了/失敗判定 (RUNNING -> ALL_DONE / FAILED)
|
|
461
|
+
if job.status == JobStatus.RUNNING:
|
|
462
|
+
# Rootノードが特定できない場合(Bootstrap前など)は何もしない
|
|
463
|
+
if not job.root_node_id or job.root_node_id not in nodes:
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
root_node = nodes[job.root_node_id]
|
|
467
|
+
|
|
468
|
+
# Case A: Rootが失敗 -> Jobも失敗
|
|
469
|
+
if root_node.lifecycle_status == LifecycleStatus.FAILED:
|
|
470
|
+
result.job_update = JobUpdate(status=JobStatus.FAILED)
|
|
471
|
+
|
|
472
|
+
# Case B: Rootが正常完了 -> Jobは全工程完了 (ALL_DONE)
|
|
473
|
+
elif root_node.lifecycle_status == LifecycleStatus.COMPLETED:
|
|
474
|
+
result.job_update = JobUpdate(status=JobStatus.ALL_DONE)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright (c) 2026 Centillion System, Inc. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the Business Source License 1.1 (BSL 1.1).
|
|
4
|
+
# For full license terms, see the LICENSE file in the root directory.
|
|
5
|
+
#
|
|
6
|
+
# This software is "Source Available" but contains certain restrictions on
|
|
7
|
+
# commercial use and managed service provision.
|
|
8
|
+
# Usage by organizations with gross revenue exceeding $10M USD requires
|
|
9
|
+
# a commercial license.
|
|
10
|
+
|
|
11
|
+
from .base import (
|
|
12
|
+
ExpansionPlan,
|
|
13
|
+
ExpansionStrategy
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .serial import SerialExpansionStrategy
|
|
17
|
+
from .parallel import ParallelExpansionStrategy
|
|
18
|
+
from .iterate import IterateExpansionStrategy
|
|
19
|
+
from .loop import LoopExpansionStrategy
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ExpansionPlan",
|
|
23
|
+
"ExpansionStrategy",
|
|
24
|
+
"SerialExpansionStrategy",
|
|
25
|
+
"ParallelExpansionStrategy",
|
|
26
|
+
"IterateExpansionStrategy",
|
|
27
|
+
"LoopExpansionStrategy",
|
|
28
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Copyright (c) 2026 Centillion System, Inc. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the Business Source License 1.1 (BSL 1.1).
|
|
4
|
+
# For full license terms, see the LICENSE file in the root directory.
|
|
5
|
+
#
|
|
6
|
+
# This software is "Source Available" but contains certain restrictions on
|
|
7
|
+
# commercial use and managed service provision.
|
|
8
|
+
# Usage by organizations with gross revenue exceeding $10M USD requires
|
|
9
|
+
# a commercial license.
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from typing import List, Dict, Any, Optional
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from odl.types import IrComponent
|
|
16
|
+
from odl_kernel.types import ProcessNode
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ExpansionPlan:
|
|
20
|
+
"""
|
|
21
|
+
ExpansionStrategyが導出する「次の一手」の定義。
|
|
22
|
+
Analyzeフェーズの出力であり、RuntimeCommand (SPAWN_CHILD) の生成源となる。
|
|
23
|
+
"""
|
|
24
|
+
blueprint: IrComponent # 実体化すべきノードの定義 (IR)
|
|
25
|
+
context_vars: Dict[str, Any] # 注入すべきコンテキスト変数 ($LOOP, $PREV 等)
|
|
26
|
+
resolved_path: str # トークン解決済みの物理パス (ID生成の種)
|
|
27
|
+
original_index: int # 親のchildren_blueprintリストにおけるインデックス (Selector用)
|
|
28
|
+
|
|
29
|
+
# 動的に取得したデータ(items等)を注入するために使用する
|
|
30
|
+
params_override: Optional[Dict[str, Any]] = None
|
|
31
|
+
|
|
32
|
+
class ExpansionStrategy(ABC):
|
|
33
|
+
"""
|
|
34
|
+
Expansion Logic Interface
|
|
35
|
+
|
|
36
|
+
Architecture:
|
|
37
|
+
L3 Mechanism (Logic) - Pure Domain Logic
|
|
38
|
+
|
|
39
|
+
Responsibility:
|
|
40
|
+
親ノードの状態と、現在の子ノード群(Current Children)に基づき、
|
|
41
|
+
「次に生成すべき子ノード」の計画(ExpansionPlan)を立案する。
|
|
42
|
+
|
|
43
|
+
具体的な展開ロジック(順次、一括、反復等)は、継承先の各Strategyクラスで定義される。
|
|
44
|
+
StrategyはDBへの書き込みや副作用を持たず、純粋に計画オブジェクトを返すことに徹する。
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def plan_next_nodes(
|
|
49
|
+
self,
|
|
50
|
+
parent: ProcessNode,
|
|
51
|
+
current_children: List[ProcessNode]
|
|
52
|
+
) -> List[ExpansionPlan]:
|
|
53
|
+
"""
|
|
54
|
+
Args:
|
|
55
|
+
parent: 親ノード(Blueprints, Resolved Path保持)
|
|
56
|
+
current_children: 既に生成されている子ノードのリスト
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List[ExpansionPlan]: 新規に生成すべきノードの計画リスト。
|
|
60
|
+
生成すべきものがなければ空リストを返す。
|
|
61
|
+
"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def _get_node_name(self, blueprint: IrComponent) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Blueprintのパスから、末尾のノード名部分のみを抽出するヘルパーメソッド。
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
Strict Path Resolution (Inheritance) のために使用される。
|
|
70
|
+
Blueprint全体のパス(未解決トークンを含む可能性がある)ではなく、
|
|
71
|
+
このメソッドで得た名前を親パスに結合することで、安全な物理パスを生成する。
|
|
72
|
+
|
|
73
|
+
Example: "root/loop/v{$LOOP}/worker_0" -> "worker_0"
|
|
74
|
+
"""
|
|
75
|
+
return blueprint.stack_path.split("/")[-1]
|