whatap 1.0.14 → 2.0.0-canary.0

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.
@@ -1,7 +1,13 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(grep -n \"ECONNREFUSED\" /Users/seunghunlee/agent_workspace/nodejs_agent/lib/observers/*.js /Users/seunghunlee/agent_workspace/nodejs_agent/lib/core/agent.js 2>/dev/null)"
4
+ "Bash(grep -n \"ECONNREFUSED\" /Users/seunghunlee/agent_workspace/nodejs_agent/lib/observers/*.js /Users/seunghunlee/agent_workspace/nodejs_agent/lib/core/agent.js 2>/dev/null)",
5
+ "WebSearch",
6
+ "Bash(go env *)",
7
+ "Bash(ls -l \"$\\(go env GOPATH\\)/bin/govulncheck\")",
8
+ "Bash(brew list *)",
9
+ "Bash(brew search *)",
10
+ "Bash(brew info *)"
5
11
  ]
6
12
  }
7
13
  }
Binary file
Binary file
Binary file
package/build.txt CHANGED
@@ -1,4 +1,4 @@
1
1
  app = 'NodeJS'
2
2
  name = 'whatap_nodejs'
3
- version = '1.0.13'
4
- build_date = '20260224'
3
+ version = '1.0.15'
4
+ build_date = '20260423'
@@ -285,7 +285,13 @@ var ConfigDefault = {
285
285
  "trace_kafka_enabled": bool("trace_kafka_enabled", false),
286
286
  "trace_kafka_consumer_enabled": bool("trace_kafka_consumer_enabled", false),
287
287
 
288
- "agent_per_instance_enabled": bool("agent_per_instance_enabled", false)
288
+ "agent_per_instance_enabled": bool("agent_per_instance_enabled", false),
289
+
290
+ // LLM monitoring
291
+ "llm_enabled": bool("llm_enabled", false),
292
+ "trace_llm_log_enabled": bool("trace_llm_log_enabled", false),
293
+ "llm_net_udp_port": num("llm_net_udp_port", 0),
294
+ "llm_model_pricing": str("llm_model_pricing", "")
289
295
 
290
296
  };
291
297
 
package/lib/core/agent.js CHANGED
@@ -37,7 +37,8 @@ var Interceptor = require('./interceptor').Interceptor,
37
37
  OracleObserver = require('../observers/oracle-observer').OracleObserver,
38
38
  CustomMethodObserver = require('../observers/custom-method-observer').CustomMethodObserver,
39
39
  CronObserver = require('../observers/cron-observer').CronObserver,
40
- KafkaObserver = require('../observers/kafka-observer').KafkaObserver;
40
+ KafkaObserver = require('../observers/kafka-observer').KafkaObserver,
41
+ OpenAIObserver = require('../observers/openai-observer').OpenAIObserver;
41
42
 
42
43
 
43
44
  var Configuration = require('./../conf/configure'),
@@ -1249,6 +1250,107 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
1249
1250
  }
1250
1251
  };
1251
1252
 
1253
+ /**
1254
+ * Start a separate Go Agent for LLM monitoring (with --llm flag)
1255
+ * Python equivalent: go(llm=True)
1256
+ */
1257
+ NodeAgent.prototype.startLlmGoAgent = function () {
1258
+ var self = this;
1259
+ var whatapHome = process.env.WHATAP_HOME;
1260
+ if (!whatapHome) return;
1261
+
1262
+ try {
1263
+ var platform = process.platform;
1264
+ var agentBinaryName = platform === 'win32' ? AGENT_NAME + '.exe' : AGENT_NAME;
1265
+ var agentPath = path.join(whatapHome, agentBinaryName);
1266
+
1267
+ if (!fs.existsSync(agentPath)) {
1268
+ Logger.print("WHATAP-LLM-020", "Agent binary not found for LLM agent: " + agentPath, false);
1269
+ return;
1270
+ }
1271
+
1272
+ // LLM UDP port
1273
+ var basePort = parseInt(self._conf.getProperty('net_udp_port', 6600)) || 6600;
1274
+ var llmPort = parseInt(self._conf.getProperty('llm_net_udp_port', 0)) || (basePort + 100);
1275
+ self._conf['llm_net_udp_port'] = llmPort;
1276
+
1277
+ // Write to whatap.conf so Go Agent can read it
1278
+ try {
1279
+ var confFile = path.join(whatapHome, 'whatap.conf');
1280
+ if (fs.existsSync(confFile)) {
1281
+ var confContent = fs.readFileSync(confFile, 'utf8');
1282
+ if (confContent.indexOf('llm_net_udp_port') < 0) {
1283
+ fs.appendFileSync(confFile, '\nllm_net_udp_port=' + llmPort + '\n');
1284
+ }
1285
+ }
1286
+ } catch (e) {
1287
+ Logger.printError("WHATAP-LLM-021", "Error writing llm_net_udp_port to whatap.conf", e, false);
1288
+ }
1289
+
1290
+ // Check existing LLM agent
1291
+ var llmPidFile = path.join(whatapHome, AGENT_NAME + '.pid.llm');
1292
+ if (fs.existsSync(llmPidFile)) {
1293
+ try {
1294
+ var existingPid = parseInt(fs.readFileSync(llmPidFile, 'utf8').trim());
1295
+ if (existingPid && self.isGoAgentRunning(existingPid)) {
1296
+ Logger.print("WHATAP-LLM-022", "LLM Go Agent already running (PID: " + existingPid + ")", false);
1297
+ return;
1298
+ }
1299
+ } catch (e) {}
1300
+ }
1301
+
1302
+ // Build environment (python equivalent: WHATAP_NET_UDP_PORT, WHATAP_PID_FILE, *_PARENT_APP_PID)
1303
+ var newEnv = Object.assign({}, process.env);
1304
+ newEnv['WHATAP_NET_UDP_PORT'] = String(llmPort);
1305
+ newEnv['WHATAP_PID_FILE'] = AGENT_NAME + '.pid.llm';
1306
+ newEnv['NODEJS_PARENT_APP_PID'] = process.pid.toString();
1307
+ newEnv['whatap.enabled'] = 'true';
1308
+
1309
+ // Spawn: whatap_nodejs -t 2 -d 1 --llm (normal agent와 동일 -t/-d, --llm만 추가)
1310
+ var spawnArgs = platform === 'win32'
1311
+ ? ['-t', '2', '--llm', 'foreground']
1312
+ : ['-t', '2', '-d', '1', '--llm'];
1313
+
1314
+ var spawnOptions = {
1315
+ cwd: whatapHome,
1316
+ env: newEnv,
1317
+ stdio: ['pipe', 'pipe', 'pipe'],
1318
+ detached: true, // python: start_new_session=True
1319
+ };
1320
+
1321
+ if (platform === 'win32') {
1322
+ spawnOptions.windowsHide = true;
1323
+ }
1324
+
1325
+ var llmProcess = child_process.spawn(agentPath, spawnArgs, spawnOptions);
1326
+
1327
+ llmProcess.on('spawn', function () {
1328
+ Logger.print("WHATAP-LLM-023", "executed LLM golang module in background (PID: " + llmProcess.pid + ")", false);
1329
+ try {
1330
+ fs.writeFileSync(llmPidFile, String(llmProcess.pid));
1331
+ } catch (e) {}
1332
+ Logger.print("WHATAP-LLM-028", "WHATAP: AGENT UP! (process name: " + AGENT_NAME + " --llm)", false);
1333
+ });
1334
+
1335
+ llmProcess.on('error', function (err) {
1336
+ Logger.printError("WHATAP-LLM-024", "Failed to spawn LLM Go Agent", err, false);
1337
+ });
1338
+
1339
+ llmProcess.on('close', function (code) {
1340
+ Logger.print("WHATAP-LLM-025", "LLM Go Agent exited with code: " + code, false);
1341
+ });
1342
+
1343
+ // Capture stderr for logging
1344
+ llmProcess.stderr.on('data', function (data) {
1345
+ var msg = data.toString().trim();
1346
+ if (msg) Logger.print("WHATAP-LLM-026", "[LLM Agent] " + msg, false);
1347
+ });
1348
+
1349
+ } catch (e) {
1350
+ Logger.printError("WHATAP-LLM-027", "Error starting LLM Go Agent", e, false);
1351
+ }
1352
+ };
1353
+
1252
1354
  // 프로세스 종료 시 정리 작업 개선
1253
1355
  process.on('exit', () => {
1254
1356
  if (!process.env.WHATAP_HOME || !NodeAgent.prototype.getApplicationIdentifier || !NodeAgent.prototype.isPM2ClusterMode) {
@@ -1374,6 +1476,12 @@ NodeAgent.prototype.init = function(cb) {
1374
1476
  // Start Node.js agent with proper port configuration
1375
1477
  WhatapUtil.printWhatap();
1376
1478
  self.startGoAgent();
1479
+
1480
+ // Start LLM Go Agent if llm_enabled
1481
+ if (self._conf.getProperty('llm_enabled', false)) {
1482
+ self.startLlmGoAgent();
1483
+ }
1484
+
1377
1485
  TraceContextManager.initialized = true;
1378
1486
 
1379
1487
  self.initUdp();
@@ -1413,6 +1521,7 @@ NodeAgent.prototype.loadObserves = function() {
1413
1521
  observes.push(OracleObserver);
1414
1522
  observes.push(CronObserver);
1415
1523
  observes.push(KafkaObserver);
1524
+ observes.push(OpenAIObserver);
1416
1525
 
1417
1526
  var packageToObserve = {};
1418
1527
  observes.forEach(function(observeObj) {
@@ -6,7 +6,8 @@
6
6
 
7
7
  var conf = require('../conf/configure'),
8
8
  Logger = require('../logger'),
9
- GCAction = require('../system/gc-action');
9
+ GCAction = require('../system/gc-action'),
10
+ LlmStatTasks = require('./task/llm-stat-tasks');
10
11
 
11
12
  function CounterManager(agent) {
12
13
  this.agent = agent;
@@ -21,6 +22,15 @@ CounterManager.prototype.run = function () {
21
22
  var tasks = [];
22
23
  tasks.push(new GCAction());
23
24
 
25
+ // LLM 메트릭 6종 (5초 주기). llm_meter는 python-apm과 일치 — 제거됨.
26
+ // 호출 카운트는 ACTIVE_STATS 꼬리의 "LLM:count=N"으로 전송 (_llmMeterCount).
27
+ tasks.push(new LlmStatTasks.LLMActiveStatTask());
28
+ tasks.push(new LlmStatTasks.LLMApiStatusTask());
29
+ tasks.push(new LlmStatTasks.LLMErrorStatTask());
30
+ tasks.push(new LlmStatTasks.LLMFeatureStatTask());
31
+ tasks.push(new LlmStatTasks.LLMPerfStatTask());
32
+ tasks.push(new LlmStatTasks.LLMTokenUsageTask());
33
+
24
34
  self.intervalIndex = setInterval(function(){
25
35
  self.process(tasks);
26
36
  },5000);
@@ -0,0 +1,270 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * LLM Metric counter tasks (7 categories):
5
+ * - llm_active_stat
6
+ * - llm_api_status
7
+ * - llm_error_stat
8
+ * - llm_feature_stat
9
+ * - llm_perf_stat (sketch fields omitted; Node has no datasketches)
10
+ * - llm_token_usage
11
+ * - meter (LLM_TRANSACTION)
12
+ *
13
+ * Each task exposes a process() method invoked by counter-manager.
14
+ * Packs are sent via async_sender.send_llm_relaypack to the LLM Go agent.
15
+ */
16
+
17
+ const TagCountPack = require('../../pack/tagcount-pack');
18
+ const DataOutputX = require('../../io/data-outputx');
19
+ const AsyncSender = require('../../udp/async_sender');
20
+ const conf = require('../../conf/configure');
21
+ const Logger = require('../../logger');
22
+ const HashUtil = require('../../util/hashutil');
23
+ const collector = require('../../llm/llm-stat-collector');
24
+ const { getLlmPcode } = require('../../llm/llm-pcode');
25
+
26
+ const _PID = process.pid;
27
+
28
+ function _enabled() {
29
+ // python-apm과 일치: llm_enabled 단일 gate
30
+ return conf.getProperty('llm_enabled', false);
31
+ }
32
+
33
+ function _bucketTime(ms) {
34
+ return Math.floor(ms / 5000) * 5000;
35
+ }
36
+
37
+ function _send(p) {
38
+ try {
39
+ p.pcode = getLlmPcode();
40
+ const bout = new DataOutputX();
41
+ bout.writePack(p, null);
42
+ AsyncSender.send_llm_relaypack(bout.toByteArray());
43
+ } catch (e) {
44
+ Logger.printError('WHATAP-LLM-STAT-001', 'send pack failed', e, false);
45
+ }
46
+ }
47
+
48
+ function _newPack(category, model, provider, opType, url) {
49
+ const p = new TagCountPack();
50
+ p.time = _bucketTime(Date.now());
51
+ p.category = category;
52
+ p.tags.putLong('pid', _PID);
53
+ p.tags.putString('model', model || 'unknown');
54
+ p.tags.putString('provider', provider || '');
55
+ p.tags.putString('operation_type', opType || 'unknown');
56
+ p.tags.putString('url', url || '');
57
+ return p;
58
+ }
59
+
60
+ // ---------------------------------------------------------
61
+ // 1) llm_active_stat
62
+ // ---------------------------------------------------------
63
+ function LLMActiveStatTask() {}
64
+ LLMActiveStatTask.prototype.process = function () {
65
+ if (!_enabled()) return;
66
+ // 매 tick에 model cache TTL 만료 entry 제거 (10분/500개 cap)
67
+ try { collector.pruneModelCache(); } catch (e) {}
68
+
69
+ const snap = collector.snapshotAndResetActive();
70
+ if (snap.active.size === 0) return;
71
+ snap.active.forEach((count, k) => {
72
+ try {
73
+ const parsed = collector.parseKey(k);
74
+ const info = snap.modelInfo.get(parsed.model);
75
+ const provider = info ? info.provider : '';
76
+ const url = info ? info.url : '';
77
+ const p = _newPack('llm_active_stat', parsed.model, provider, parsed.op_type, url);
78
+ p.fields.putLong('count', count);
79
+ _send(p);
80
+ } catch (e) {
81
+ Logger.printError('WHATAP-LLM-STAT-010', 'active stat error', e, false);
82
+ }
83
+ });
84
+ };
85
+
86
+ // ---------------------------------------------------------
87
+ // 2) llm_api_status
88
+ // ---------------------------------------------------------
89
+ function LLMApiStatusTask() {}
90
+ LLMApiStatusTask.prototype.process = function () {
91
+ if (!_enabled()) return;
92
+ const snap = collector.snapshotAndResetApiStatus();
93
+ if (snap.x4.size === 0 && snap.x5.size === 0) return;
94
+
95
+ // Set spread → Map iteration으로 GC 압박 감소 (intermediate array 제거)
96
+ const seen = new Set();
97
+ const emit = (k) => {
98
+ if (seen.has(k)) return;
99
+ seen.add(k);
100
+ try {
101
+ const parsed = collector.parseKey(k);
102
+ const p = _newPack('llm_api_status', parsed.model, parsed.provider, parsed.op_type, parsed.url);
103
+ p.fields.putLong('4xx_total_count', snap.x4.get(k) || 0);
104
+ p.fields.putLong('5xx_total_count', snap.x5.get(k) || 0);
105
+ _send(p);
106
+ } catch (e) {
107
+ Logger.printError('WHATAP-LLM-STAT-020', 'api status error', e, false);
108
+ }
109
+ };
110
+ snap.x4.forEach((_, k) => emit(k));
111
+ snap.x5.forEach((_, k) => emit(k));
112
+ };
113
+
114
+ // ---------------------------------------------------------
115
+ // 3) llm_error_stat
116
+ // ---------------------------------------------------------
117
+ function LLMErrorStatTask() {}
118
+ LLMErrorStatTask.prototype.process = function () {
119
+ if (!_enabled()) return;
120
+ const snap = collector.snapshotAndResetErrors();
121
+ if (snap.api.size === 0 && snap.program.size === 0 && snap.lastApi.size === 0) return;
122
+
123
+ const seen = new Set();
124
+ const emit = (k) => {
125
+ if (seen.has(k)) return;
126
+ seen.add(k);
127
+ try {
128
+ const parsed = collector.parseKey(k);
129
+ const apiE = snap.api.get(k) || 0;
130
+ const progE = snap.program.get(k) || 0;
131
+ const p = _newPack('llm_error_stat', parsed.model, parsed.provider, parsed.op_type, parsed.url);
132
+ p.fields.putLong('error_count', apiE + progE);
133
+ p.fields.putLong('api_error_count', apiE);
134
+ p.fields.putLong('program_error_count', progE);
135
+ p.fields.putLong('last_api_error_count', snap.lastApi.get(k) || 0);
136
+ _send(p);
137
+ } catch (e) {
138
+ Logger.printError('WHATAP-LLM-STAT-030', 'error stat error', e, false);
139
+ }
140
+ };
141
+ snap.api.forEach((_, k) => emit(k));
142
+ snap.program.forEach((_, k) => emit(k));
143
+ snap.lastApi.forEach((_, k) => emit(k));
144
+ };
145
+
146
+ // ---------------------------------------------------------
147
+ // 4) llm_feature_stat (리스트형 → !rectype=2)
148
+ // ---------------------------------------------------------
149
+ function LLMFeatureStatTask() {}
150
+ LLMFeatureStatTask.prototype.process = function () {
151
+ if (!_enabled()) return;
152
+ const snap = collector.snapshotAndResetFeatures();
153
+ if (snap.calls.size === 0) return;
154
+ snap.calls.forEach((callCount, k) => {
155
+ try {
156
+ const parsed = collector.parseKey(k);
157
+ const p = _newPack('llm_feature_stat', parsed.model, parsed.provider, parsed.op_type, parsed.url);
158
+ p.tags.putLong('!rectype', 2);
159
+ p.fields.putLong('call_count', callCount);
160
+
161
+ const idList = p.fields.newList('@id');
162
+ const fList = p.fields.newList('features');
163
+ const fCntList = p.fields.newList('features_count');
164
+ const counts = snap.counts.get(k) || {};
165
+ for (const feat of collector.FEATURES) {
166
+ const c = counts[feat] || 0;
167
+ if (c > 0) {
168
+ idList.addLong(HashUtil.hashFromString(`${parsed.model}:${parsed.provider}:${parsed.op_type}:${feat}`));
169
+ fList.addString(feat);
170
+ fCntList.addLong(c);
171
+ }
172
+ }
173
+ _send(p);
174
+ } catch (e) {
175
+ Logger.printError('WHATAP-LLM-STAT-040', 'feature stat error', e, false);
176
+ }
177
+ });
178
+ };
179
+
180
+ // ---------------------------------------------------------
181
+ // 5) llm_perf_stat (sketch 미지원 — sum/count만 전송)
182
+ // ---------------------------------------------------------
183
+ function LLMPerfStatTask() {}
184
+ LLMPerfStatTask.prototype.process = function () {
185
+ if (!_enabled()) return;
186
+ const snap = collector.snapshotAndResetPerf();
187
+ if (snap.callCount.size === 0) return;
188
+ snap.callCount.forEach((call, k) => {
189
+ try {
190
+ const parsed = collector.parseKey(k);
191
+ const p = _newPack('llm_perf_stat', parsed.model, parsed.provider, parsed.op_type, parsed.url);
192
+ p.fields.putLong('call_count', call);
193
+ p.fields.putFloat('latency_sum', snap.latencySum.get(k) || 0);
194
+ p.fields.putFloat('ttft_sum', snap.ttftSum.get(k) || 0);
195
+ p.fields.putLong('ttft_count', snap.ttftCount.get(k) || 0);
196
+ p.fields.putFloat('tpot_sum', snap.tpotSum.get(k) || 0);
197
+ p.fields.putLong('tpot_count', snap.tpotCount.get(k) || 0);
198
+ // sketch 필드는 datasketches 미지원으로 omit
199
+ _send(p);
200
+ } catch (e) {
201
+ Logger.printError('WHATAP-LLM-STAT-050', 'perf stat error', e, false);
202
+ }
203
+ });
204
+ };
205
+
206
+ // ---------------------------------------------------------
207
+ // 6) llm_token_usage (리스트형 → !rectype=2)
208
+ // ---------------------------------------------------------
209
+ const _TOKEN_TYPES = ['input_tokens', 'output_tokens', 'cached_tokens', 'reasoning_tokens',
210
+ 'cache_creation_input_tokens', 'cache_read_input_tokens'];
211
+ const _TOKEN_COST_MAP = {
212
+ 'input_tokens': 'input_cost',
213
+ 'output_tokens': 'output_cost',
214
+ 'cached_tokens': 'cached_cost',
215
+ };
216
+
217
+ function LLMTokenUsageTask() {}
218
+ LLMTokenUsageTask.prototype.process = function () {
219
+ if (!_enabled()) return;
220
+ const snap = collector.snapshotAndResetTokens();
221
+ if (snap.calls.size === 0) return;
222
+ snap.calls.forEach((call, k) => {
223
+ try {
224
+ const parsed = collector.parseKey(k);
225
+ const counts = snap.counts.get(k) || {};
226
+ const costs = snap.costs.get(k) || {};
227
+ const totalTokens = (counts.input_tokens || 0) + (counts.output_tokens || 0);
228
+ const totalCost = (costs.input_cost || 0) + (costs.output_cost || 0);
229
+
230
+ const p = _newPack('llm_token_usage', parsed.model, parsed.provider, parsed.op_type, parsed.url);
231
+ p.tags.putLong('!rectype', 2);
232
+ p.fields.putLong('call_count', call);
233
+ p.fields.putLong('total_tokens', totalTokens);
234
+ p.fields.putFloat('total_cost', +totalCost.toFixed(6));
235
+ p.fields.putLong('error_count', snap.error.get(k) || 0);
236
+ p.fields.putLong('stream_count', snap.stream.get(k) || 0);
237
+
238
+ const idList = p.fields.newList('@id');
239
+ const tokList = p.fields.newList('tokens');
240
+ const cntList = p.fields.newList('tokens_count');
241
+ const costList = p.fields.newList('tokens_cost');
242
+ for (const tt of _TOKEN_TYPES) {
243
+ const c = counts[tt] || 0;
244
+ if (c > 0) {
245
+ idList.addLong(HashUtil.hashFromString(`${parsed.model}:${parsed.provider}:${parsed.op_type}:${tt}`));
246
+ tokList.addString(tt);
247
+ cntList.addLong(c);
248
+ const costKey = _TOKEN_COST_MAP[tt];
249
+ costList.addFloat(costKey ? +(costs[costKey] || 0).toFixed(6) : 0);
250
+ }
251
+ }
252
+ _send(p);
253
+ } catch (e) {
254
+ Logger.printError('WHATAP-LLM-STAT-060', 'token usage error', e, false);
255
+ }
256
+ });
257
+ };
258
+
259
+ // NOTE: llm_meter TagCountPack은 python-apm에서도 제거됨.
260
+ // LLM 호출 카운트는 _llmMeterCount로 유지되어 ACTIVE_STATS 패킷 꼬리에
261
+ // "LLM:count=N" 형태로 append 송신됨 (udp_session.js:161).
262
+
263
+ module.exports = {
264
+ LLMActiveStatTask,
265
+ LLMApiStatusTask,
266
+ LLMErrorStatTask,
267
+ LLMFeatureStatTask,
268
+ LLMPerfStatTask,
269
+ LLMTokenUsageTask,
270
+ };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * BigInt 변환 캐싱 유틸.
5
+ * JS Number의 53bit 초과 정수 정밀도 손실 방지용 BigInt 변환은 비용이 있는 작업이므로
6
+ * 한 라이프사이클 내 동일 값에 대해 1회만 수행.
7
+ */
8
+
9
+ function bigStr(v) {
10
+ if (v == null) return '';
11
+ try {
12
+ return BigInt(v).toString();
13
+ } catch (e) {
14
+ return String(v);
15
+ }
16
+ }
17
+
18
+ /**
19
+ * ctx.id를 BigInt-precise string으로 반환. ctx에 _txidStr로 캐싱.
20
+ */
21
+ function ctxTxidStr(ctx) {
22
+ if (!ctx) return '';
23
+ if (ctx._txidStr) return ctx._txidStr;
24
+ ctx._txidStr = bigStr(ctx.id);
25
+ return ctx._txidStr;
26
+ }
27
+
28
+ /**
29
+ * 임의 step_id를 BigInt-precise string으로 반환. 라이프사이클 짧으면 캐싱 안 함.
30
+ */
31
+ function stepIdStr(stepIdRaw) {
32
+ return bigStr(stepIdRaw);
33
+ }
34
+
35
+ module.exports = { bigStr, ctxTxidStr, stepIdStr };