Skip to content

Commit be972e2

Browse files
committed
리액티브 실행 payload에 진단·stale·의존 인접 동봉
KernelReactivePayload에 multipleDefinitions·crossCellMutations·staleBlockIds·dependents 추가 (기본값 비움 = 하위호환). executeKernelReactive가 그래프를 1회만 빌드해 진단을 모으고, 에러로 실행 못 한 다운스트림을 staleBlockIds로 표시. httpPayload·wsCompletePayload 직렬화 통일. remove-cell 엔드포인트 라운드트립 테스트 추가.
1 parent 78a4b45 commit be972e2

3 files changed

Lines changed: 72 additions & 6 deletions

File tree

src/codaro/kernel/executionPayload.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import Any
77

88
from .protocol import ExecutionEvent, ExecutionOutput, WsResultMessage
9-
from .reactive import executeReactive, previewReactiveOrder, reactiveDiagnostics
9+
from .reactive import buildReactiveGraph, diagnosticsFromGraph, executeReactive, previewReactiveOrder
1010
from .session import KernelSession
1111

1212

@@ -28,6 +28,10 @@ class KernelReactivePayload:
2828
executionOrder: tuple[str, ...]
2929
durationMs: float
3030
cycles: tuple[tuple[str, ...], ...] = ()
31+
multipleDefinitions: tuple[tuple[str, tuple[str, ...]], ...] = ()
32+
crossCellMutations: tuple[tuple[str, str, str], ...] = ()
33+
staleBlockIds: tuple[str, ...] = ()
34+
dependents: tuple[tuple[str, tuple[str, ...]], ...] = ()
3135

3236
@property
3337
def resultCount(self) -> int:
@@ -37,11 +41,20 @@ def resultCount(self) -> int:
3741
def executionCount(self) -> int:
3842
return len(self.executionOrder)
3943

44+
def _diagnosticsPayload(self) -> dict[str, Any]:
45+
return {
46+
"cycles": [list(cycle) for cycle in self.cycles],
47+
"multipleDefinitions": [[var, list(blockIds)] for var, blockIds in self.multipleDefinitions],
48+
"crossCellMutations": [list(mutation) for mutation in self.crossCellMutations],
49+
"staleBlockIds": list(self.staleBlockIds),
50+
"dependents": {blockId: list(downstream) for blockId, downstream in self.dependents},
51+
}
52+
4053
def httpPayload(self) -> dict[str, Any]:
4154
return {
4255
"results": [result.model_dump() for result in self.results],
4356
"executionOrder": list(self.executionOrder),
44-
"cycles": [list(cycle) for cycle in self.cycles],
57+
**self._diagnosticsPayload(),
4558
}
4659

4760
def toolPayload(self) -> dict[str, Any]:
@@ -67,7 +80,7 @@ def wsCompletePayload(self, requestId: str) -> dict[str, Any]:
6780
"type": "reactiveComplete",
6881
"requestId": requestId,
6982
"executionOrder": list(self.executionOrder),
70-
"cycles": [list(cycle) for cycle in self.cycles],
83+
**self._diagnosticsPayload(),
7184
}
7285

7386

@@ -93,15 +106,28 @@ async def executeKernelReactive(
93106
) -> KernelReactivePayload:
94107
startedAt = time.perf_counter()
95108
blockList = list(blocks)
96-
cycles = reactiveDiagnostics(blockList)
109+
graph = buildReactiveGraph(blockList)
110+
diagnostics = diagnosticsFromGraph(graph)
97111
results, executionOrder = await executeReactive(
98-
session, blockList, changedBlockId, eventHandler=eventHandler, includeSource=includeSource
112+
session, blockList, changedBlockId, eventHandler=eventHandler, includeSource=includeSource, graph=graph
113+
)
114+
# early-stop(에러로 중단)으로 영향받았지만 실행 못 한 셀 = stale.
115+
executed = {result.blockId for result in results}
116+
staleBlockIds = tuple(blockId for blockId in executionOrder if blockId not in executed)
117+
dependents = tuple(
118+
(blockId, tuple(sorted(graph.dependents[blockId])))
119+
for blockId in graph.blockOrder
120+
if graph.dependents.get(blockId)
99121
)
100122
return KernelReactivePayload(
101123
results=tuple(results),
102124
executionOrder=tuple(executionOrder),
103125
durationMs=_durationMs(startedAt),
104-
cycles=tuple(tuple(cycle) for cycle in cycles),
126+
cycles=diagnostics.cycles,
127+
multipleDefinitions=diagnostics.multipleDefinitions,
128+
crossCellMutations=diagnostics.crossCellMutations,
129+
staleBlockIds=staleBlockIds,
130+
dependents=dependents,
105131
)
106132

107133

tests/testKernel.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ def testReactivePayloadOwnsHttpWsAndToolShapes() -> None:
185185
"requestId": "req-r",
186186
"executionOrder": ["b1", "b2"],
187187
"cycles": [],
188+
"multipleDefinitions": [],
189+
"crossCellMutations": [],
190+
"staleBlockIds": [],
191+
"dependents": {"b1": ["b2"]},
188192
}
189193

190194
toolPayload = payload.toolPayload()
@@ -194,6 +198,21 @@ def testReactivePayloadOwnsHttpWsAndToolShapes() -> None:
194198
session.dispose()
195199

196200

201+
def testReactivePayloadMarksUnrunDownstreamStaleOnError() -> None:
202+
session = KernelSession()
203+
blocks = [
204+
{"id": "a", "type": "code", "content": "x = 1"},
205+
{"id": "b", "type": "code", "content": "y = x + missing"}, # NameError → early stop
206+
{"id": "c", "type": "code", "content": "z = y + 1"},
207+
]
208+
payload = _run(executeKernelReactive(session, blocks, "a"))
209+
210+
assert payload.executionOrder == ("a", "b", "c")
211+
# b가 에러로 끊겨 c는 실행 못 함 → stale.
212+
assert payload.staleBlockIds == ("c",)
213+
session.dispose()
214+
215+
197216
def testMultilineCode() -> None:
198217
session = KernelSession()
199218
code = """

tests/testServerApi.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,23 @@ def testKernelStatePersistence() -> None:
134134
client.delete(f"/api/kernel/{sessionId}")
135135

136136

137+
def testKernelRemoveCellEndpointClearsDefinitions() -> None:
138+
client = TestClient(createServerApp())
139+
sessionId = client.post("/api/kernel/create", json={}).json()["sessionId"]
140+
141+
client.post(f"/api/kernel/{sessionId}/execute", json={"code": "x = 10", "blockId": "a"})
142+
removeResponse = client.post(f"/api/kernel/{sessionId}/remove-cell", json={"code": "", "blockId": "a"})
143+
assert removeResponse.status_code == 200
144+
assert removeResponse.json() == {"status": "removed"}
145+
146+
# 삭제 후 x를 참조하면 NameError(zombie 없음).
147+
result = client.post(f"/api/kernel/{sessionId}/execute", json={"code": "x + 1", "blockId": "b"})
148+
assert result.json()["status"] == "error"
149+
assert "NameError" in f"{result.json()['data']} {result.json()['stderr']}"
150+
151+
client.delete(f"/api/kernel/{sessionId}")
152+
153+
137154
def testKernelReset() -> None:
138155
client = TestClient(createServerApp())
139156

@@ -884,6 +901,10 @@ def testKernelWebSocketReactiveAndReset() -> None:
884901
"requestId": "req-reactive",
885902
"executionOrder": ["b1", "b2"],
886903
"cycles": [],
904+
"multipleDefinitions": [],
905+
"crossCellMutations": [],
906+
"staleBlockIds": [],
907+
"dependents": {"b1": ["b2"]},
887908
}
888909
assert websocket.receive_json() == {"type": "status", "engineStatus": "ready"}
889910

0 commit comments

Comments
 (0)