@goondan/runtime 0.0.3-alpha1

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.
Files changed (137) hide show
  1. package/dist/config/bundle-loader.d.ts +30 -0
  2. package/dist/config/bundle-loader.d.ts.map +1 -0
  3. package/dist/config/bundle-loader.js +504 -0
  4. package/dist/config/bundle-loader.js.map +1 -0
  5. package/dist/config/object-ref.d.ts +8 -0
  6. package/dist/config/object-ref.d.ts.map +1 -0
  7. package/dist/config/object-ref.js +60 -0
  8. package/dist/config/object-ref.js.map +1 -0
  9. package/dist/config/resources.d.ts +18 -0
  10. package/dist/config/resources.d.ts.map +1 -0
  11. package/dist/config/resources.js +575 -0
  12. package/dist/config/resources.js.map +1 -0
  13. package/dist/config/simple-yaml.d.ts +5 -0
  14. package/dist/config/simple-yaml.d.ts.map +1 -0
  15. package/dist/config/simple-yaml.js +250 -0
  16. package/dist/config/simple-yaml.js.map +1 -0
  17. package/dist/conversation/state.d.ts +38 -0
  18. package/dist/conversation/state.d.ts.map +1 -0
  19. package/dist/conversation/state.js +157 -0
  20. package/dist/conversation/state.js.map +1 -0
  21. package/dist/events/runtime-events.d.ts +87 -0
  22. package/dist/events/runtime-events.d.ts.map +1 -0
  23. package/dist/events/runtime-events.js +32 -0
  24. package/dist/events/runtime-events.js.map +1 -0
  25. package/dist/extension/api-impl.d.ts +28 -0
  26. package/dist/extension/api-impl.d.ts.map +1 -0
  27. package/dist/extension/api-impl.js +59 -0
  28. package/dist/extension/api-impl.js.map +1 -0
  29. package/dist/extension/api-impl.test.d.ts +2 -0
  30. package/dist/extension/api-impl.test.d.ts.map +1 -0
  31. package/dist/extension/api-impl.test.js +97 -0
  32. package/dist/extension/api-impl.test.js.map +1 -0
  33. package/dist/extension/index.d.ts +4 -0
  34. package/dist/extension/index.d.ts.map +1 -0
  35. package/dist/extension/index.js +4 -0
  36. package/dist/extension/index.js.map +1 -0
  37. package/dist/extension/integration.test.d.ts +2 -0
  38. package/dist/extension/integration.test.d.ts.map +1 -0
  39. package/dist/extension/integration.test.js +224 -0
  40. package/dist/extension/integration.test.js.map +1 -0
  41. package/dist/extension/loader.d.ts +17 -0
  42. package/dist/extension/loader.d.ts.map +1 -0
  43. package/dist/extension/loader.js +50 -0
  44. package/dist/extension/loader.js.map +1 -0
  45. package/dist/extension/loader.test.d.ts +2 -0
  46. package/dist/extension/loader.test.d.ts.map +1 -0
  47. package/dist/extension/loader.test.js +180 -0
  48. package/dist/extension/loader.test.js.map +1 -0
  49. package/dist/extension/state-manager.d.ts +21 -0
  50. package/dist/extension/state-manager.d.ts.map +1 -0
  51. package/dist/extension/state-manager.js +43 -0
  52. package/dist/extension/state-manager.js.map +1 -0
  53. package/dist/extension/state-manager.test.d.ts +2 -0
  54. package/dist/extension/state-manager.test.d.ts.map +1 -0
  55. package/dist/extension/state-manager.test.js +100 -0
  56. package/dist/extension/state-manager.test.js.map +1 -0
  57. package/dist/index.d.ts +19 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +19 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/orchestrator/event-queue.d.ts +15 -0
  62. package/dist/orchestrator/event-queue.d.ts.map +1 -0
  63. package/dist/orchestrator/event-queue.js +20 -0
  64. package/dist/orchestrator/event-queue.js.map +1 -0
  65. package/dist/orchestrator/orchestrator.d.ts +41 -0
  66. package/dist/orchestrator/orchestrator.d.ts.map +1 -0
  67. package/dist/orchestrator/orchestrator.js +409 -0
  68. package/dist/orchestrator/orchestrator.js.map +1 -0
  69. package/dist/orchestrator/types.d.ts +72 -0
  70. package/dist/orchestrator/types.d.ts.map +1 -0
  71. package/dist/orchestrator/types.js +2 -0
  72. package/dist/orchestrator/types.js.map +1 -0
  73. package/dist/pipeline/registry.d.ts +60 -0
  74. package/dist/pipeline/registry.d.ts.map +1 -0
  75. package/dist/pipeline/registry.js +362 -0
  76. package/dist/pipeline/registry.js.map +1 -0
  77. package/dist/runner/conversation-state.d.ts +8 -0
  78. package/dist/runner/conversation-state.d.ts.map +1 -0
  79. package/dist/runner/conversation-state.js +52 -0
  80. package/dist/runner/conversation-state.js.map +1 -0
  81. package/dist/runner/index.d.ts +4 -0
  82. package/dist/runner/index.d.ts.map +1 -0
  83. package/dist/runner/index.js +18 -0
  84. package/dist/runner/index.js.map +1 -0
  85. package/dist/runner/runtime-restart-signal.d.ts +6 -0
  86. package/dist/runner/runtime-restart-signal.d.ts.map +1 -0
  87. package/dist/runner/runtime-restart-signal.js +18 -0
  88. package/dist/runner/runtime-restart-signal.js.map +1 -0
  89. package/dist/runner/runtime-routing.d.ts +41 -0
  90. package/dist/runner/runtime-routing.d.ts.map +1 -0
  91. package/dist/runner/runtime-routing.js +154 -0
  92. package/dist/runner/runtime-routing.js.map +1 -0
  93. package/dist/runner/runtime-runner-connector-child.d.ts +2 -0
  94. package/dist/runner/runtime-runner-connector-child.d.ts.map +1 -0
  95. package/dist/runner/runtime-runner-connector-child.js +188 -0
  96. package/dist/runner/runtime-runner-connector-child.js.map +1 -0
  97. package/dist/runner/runtime-runner-protocol.d.ts +13 -0
  98. package/dist/runner/runtime-runner-protocol.d.ts.map +1 -0
  99. package/dist/runner/runtime-runner-protocol.js +14 -0
  100. package/dist/runner/runtime-runner-protocol.js.map +1 -0
  101. package/dist/runner/runtime-runner.d.ts +2 -0
  102. package/dist/runner/runtime-runner.d.ts.map +1 -0
  103. package/dist/runner/runtime-runner.js +2738 -0
  104. package/dist/runner/runtime-runner.js.map +1 -0
  105. package/dist/runner/turn-policy.d.ts +8 -0
  106. package/dist/runner/turn-policy.d.ts.map +1 -0
  107. package/dist/runner/turn-policy.js +13 -0
  108. package/dist/runner/turn-policy.js.map +1 -0
  109. package/dist/tools/executor.d.ts +29 -0
  110. package/dist/tools/executor.d.ts.map +1 -0
  111. package/dist/tools/executor.js +124 -0
  112. package/dist/tools/executor.js.map +1 -0
  113. package/dist/tools/naming.d.ts +7 -0
  114. package/dist/tools/naming.d.ts.map +1 -0
  115. package/dist/tools/naming.js +31 -0
  116. package/dist/tools/naming.js.map +1 -0
  117. package/dist/tools/registry.d.ts +18 -0
  118. package/dist/tools/registry.d.ts.map +1 -0
  119. package/dist/tools/registry.js +66 -0
  120. package/dist/tools/registry.js.map +1 -0
  121. package/dist/types.d.ts +388 -0
  122. package/dist/types.d.ts.map +1 -0
  123. package/dist/types.js +38 -0
  124. package/dist/types.js.map +1 -0
  125. package/dist/workspace/instance-manager.d.ts +19 -0
  126. package/dist/workspace/instance-manager.d.ts.map +1 -0
  127. package/dist/workspace/instance-manager.js +72 -0
  128. package/dist/workspace/instance-manager.js.map +1 -0
  129. package/dist/workspace/paths.d.ts +28 -0
  130. package/dist/workspace/paths.d.ts.map +1 -0
  131. package/dist/workspace/paths.js +108 -0
  132. package/dist/workspace/paths.js.map +1 -0
  133. package/dist/workspace/storage.d.ts +45 -0
  134. package/dist/workspace/storage.d.ts.map +1 -0
  135. package/dist/workspace/storage.js +377 -0
  136. package/dist/workspace/storage.js.map +1 -0
  137. package/package.json +32 -0
@@ -0,0 +1,2738 @@
1
+ import path from 'node:path';
2
+ import { Console } from 'node:console';
3
+ import { fork } from 'node:child_process';
4
+ import { EventEmitter } from 'node:events';
5
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
6
+ import { closeSync, constants as fsConstants, existsSync, openSync, watch as watchFs } from 'node:fs';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
+ import { generateText, jsonSchema, stepCountIs, tool, } from 'ai';
9
+ import { createAnthropic } from '@ai-sdk/anthropic';
10
+ import { createGoogleGenerativeAI } from '@ai-sdk/google';
11
+ import { createOpenAI } from '@ai-sdk/openai';
12
+ import { BundleLoader, ConversationStateImpl, ExtensionApiImpl, ExtensionStateManagerImpl, PipelineRegistryImpl, buildToolName, createMinimalToolContext, isJsonObject, loadExtensions, normalizeObjectRef, FileWorkspaceStorage, ToolExecutor, ToolRegistryImpl, WorkspacePaths, } from '../index.js';
13
+ import { formatRuntimeInboundUserText, parseAgentToolEventPayload, parseConnectorEventPayload, selectMatchingIngressRule, resolveInboundInstanceKey, resolveRuntimeWorkdir, } from './runtime-routing.js';
14
+ import { buildStepLimitResponse } from './turn-policy.js';
15
+ import { toConversationTurns, toPersistentMessages, } from './conversation-state.js';
16
+ const ORCHESTRATOR_PROCESS_NAME = 'orchestrator';
17
+ const REPLACEMENT_STARTUP_TIMEOUT_MS = 5000;
18
+ const CONNECTOR_CHILD_STARTUP_TIMEOUT_MS = 5000;
19
+ const CONNECTOR_CHILD_SHUTDOWN_TIMEOUT_MS = 2000;
20
+ const RESTART_FLAG_KEYS = ['restartRequested', 'runtimeRestart', '__goondanRestart'];
21
+ function isRunnerReadyMessage(message) {
22
+ if (!isJsonObject(message)) {
23
+ return false;
24
+ }
25
+ return message.type === 'ready' && typeof message.instanceKey === 'string' && typeof message.pid === 'number';
26
+ }
27
+ function isRunnerStartErrorMessage(message) {
28
+ if (!isJsonObject(message)) {
29
+ return false;
30
+ }
31
+ return message.type === 'start_error' && typeof message.message === 'string';
32
+ }
33
+ function isConnectorChildStartedMessage(message) {
34
+ if (!isJsonObject(message)) {
35
+ return false;
36
+ }
37
+ return message.type === 'connector_started';
38
+ }
39
+ function isConnectorChildStartErrorMessage(message) {
40
+ if (!isJsonObject(message)) {
41
+ return false;
42
+ }
43
+ return message.type === 'connector_start_error' && typeof message.message === 'string';
44
+ }
45
+ function isConnectorChildEventMessage(message) {
46
+ if (!isJsonObject(message)) {
47
+ return false;
48
+ }
49
+ return message.type === 'connector_event' && Object.hasOwn(message, 'event');
50
+ }
51
+ function readRuntimeRestartSignal(value) {
52
+ if (!isJsonObject(value)) {
53
+ return undefined;
54
+ }
55
+ for (const key of RESTART_FLAG_KEYS) {
56
+ if (value[key] === true) {
57
+ const reasonValue = value.restartReason;
58
+ return {
59
+ requested: true,
60
+ reason: typeof reasonValue === 'string' && reasonValue.trim().length > 0 ? reasonValue.trim() : undefined,
61
+ };
62
+ }
63
+ }
64
+ return undefined;
65
+ }
66
+ function resolveProcessLogPaths(stateRoot, instanceKey, processName) {
67
+ const logDir = path.join(stateRoot, 'runtime', 'logs', instanceKey);
68
+ return {
69
+ stdoutPath: path.join(logDir, `${processName}.stdout.log`),
70
+ stderrPath: path.join(logDir, `${processName}.stderr.log`),
71
+ };
72
+ }
73
+ function closeFd(fd) {
74
+ try {
75
+ closeSync(fd);
76
+ }
77
+ catch {
78
+ // 이미 닫힌 fd는 무시한다.
79
+ }
80
+ }
81
+ function killIfRunning(pid) {
82
+ if (!pid || pid <= 0) {
83
+ return;
84
+ }
85
+ try {
86
+ process.kill(pid, 'SIGKILL');
87
+ }
88
+ catch {
89
+ // 이미 종료된 프로세스는 무시한다.
90
+ }
91
+ }
92
+ async function waitForReplacementRunnerReady(child, instanceKey, startupTimeoutMs, logPaths) {
93
+ return new Promise((resolve, reject) => {
94
+ let settled = false;
95
+ const cleanup = () => {
96
+ clearTimeout(timeout);
97
+ child.off('message', onMessage);
98
+ child.off('error', onError);
99
+ child.off('exit', onExit);
100
+ };
101
+ const fail = (message) => {
102
+ if (settled) {
103
+ return;
104
+ }
105
+ settled = true;
106
+ cleanup();
107
+ reject(new Error(`${message} (logs: ${logPaths.stdoutPath}, ${logPaths.stderrPath})`));
108
+ };
109
+ const succeed = (pid) => {
110
+ if (settled) {
111
+ return;
112
+ }
113
+ settled = true;
114
+ cleanup();
115
+ resolve(pid);
116
+ };
117
+ const timeout = setTimeout(() => {
118
+ killIfRunning(child.pid);
119
+ fail('replacement Orchestrator 시작 확인이 시간 내에 완료되지 않았습니다.');
120
+ }, startupTimeoutMs);
121
+ const onMessage = (message) => {
122
+ if (isRunnerReadyMessage(message)) {
123
+ if (message.instanceKey !== instanceKey) {
124
+ fail(`replacement Orchestrator instanceKey 불일치: expected=${instanceKey}, actual=${message.instanceKey}`);
125
+ return;
126
+ }
127
+ succeed(message.pid);
128
+ return;
129
+ }
130
+ if (isRunnerStartErrorMessage(message)) {
131
+ fail(`replacement Orchestrator 시작 실패: ${message.message}`);
132
+ }
133
+ };
134
+ const onError = (error) => {
135
+ fail(`replacement Orchestrator 프로세스 오류: ${error.message}`);
136
+ };
137
+ const onExit = (code, signal) => {
138
+ const cause = code !== null ? `exit code ${code}` : signal ? `signal ${signal}` : 'unknown reason';
139
+ fail(`replacement Orchestrator가 초기화 중 종료되었습니다 (${cause}).`);
140
+ };
141
+ child.on('message', onMessage);
142
+ child.on('error', onError);
143
+ child.on('exit', onExit);
144
+ });
145
+ }
146
+ async function writeActiveRuntimeState(input) {
147
+ const runtimeDir = path.join(input.stateRoot, 'runtime');
148
+ await mkdir(runtimeDir, { recursive: true });
149
+ const state = {
150
+ instanceKey: input.instanceKey,
151
+ bundlePath: input.bundlePath,
152
+ startedAt: new Date().toISOString(),
153
+ watch: input.watch,
154
+ swarm: input.swarmName,
155
+ pid: input.pid,
156
+ logs: [
157
+ {
158
+ process: ORCHESTRATOR_PROCESS_NAME,
159
+ stdout: input.logPaths.stdoutPath,
160
+ stderr: input.logPaths.stderrPath,
161
+ },
162
+ ],
163
+ };
164
+ await writeFile(path.join(runtimeDir, 'active.json'), JSON.stringify(state, null, 2), 'utf8');
165
+ }
166
+ async function spawnReplacementRunner(input) {
167
+ const logPaths = resolveProcessLogPaths(input.stateRoot, input.instanceKey, ORCHESTRATOR_PROCESS_NAME);
168
+ await mkdir(path.dirname(logPaths.stdoutPath), { recursive: true });
169
+ const stdoutFd = openSync(logPaths.stdoutPath, 'a');
170
+ const stderrFd = openSync(logPaths.stderrPath, 'a');
171
+ let child;
172
+ try {
173
+ child = fork(input.runnerModulePath, input.runnerArgs, {
174
+ cwd: path.dirname(input.bundlePath),
175
+ detached: true,
176
+ env: {
177
+ ...input.env,
178
+ GOONDAN_STATE_ROOT: input.stateRoot,
179
+ },
180
+ stdio: ['ignore', stdoutFd, stderrFd, 'ipc'],
181
+ });
182
+ }
183
+ finally {
184
+ closeFd(stdoutFd);
185
+ closeFd(stderrFd);
186
+ }
187
+ if (!child.pid || child.pid <= 0) {
188
+ throw new Error('replacement Orchestrator 프로세스를 시작하지 못했습니다.');
189
+ }
190
+ const pid = await waitForReplacementRunnerReady(child, input.instanceKey, input.startupTimeoutMs ?? REPLACEMENT_STARTUP_TIMEOUT_MS, logPaths).catch((error) => {
191
+ killIfRunning(child.pid);
192
+ throw error;
193
+ });
194
+ if (child.connected) {
195
+ child.disconnect();
196
+ }
197
+ child.unref();
198
+ await writeActiveRuntimeState({
199
+ stateRoot: input.stateRoot,
200
+ instanceKey: input.instanceKey,
201
+ bundlePath: input.bundlePath,
202
+ watch: input.watch,
203
+ swarmName: input.swarmName,
204
+ pid,
205
+ logPaths,
206
+ });
207
+ return pid;
208
+ }
209
+ function parseRunnerArguments(argv) {
210
+ let bundlePath;
211
+ let instanceKey;
212
+ let stateRoot;
213
+ let swarmName;
214
+ let watch = false;
215
+ for (let index = 0; index < argv.length; index += 1) {
216
+ const arg = argv[index];
217
+ if (!arg) {
218
+ continue;
219
+ }
220
+ if (arg === '--bundle-path') {
221
+ const value = argv[index + 1];
222
+ if (!value) {
223
+ throw new Error('--bundle-path 옵션 값이 필요합니다.');
224
+ }
225
+ bundlePath = value;
226
+ index += 1;
227
+ continue;
228
+ }
229
+ if (arg === '--instance-key') {
230
+ const value = argv[index + 1];
231
+ if (!value) {
232
+ throw new Error('--instance-key 옵션 값이 필요합니다.');
233
+ }
234
+ instanceKey = value;
235
+ index += 1;
236
+ continue;
237
+ }
238
+ if (arg === '--state-root') {
239
+ const value = argv[index + 1];
240
+ if (!value) {
241
+ throw new Error('--state-root 옵션 값이 필요합니다.');
242
+ }
243
+ stateRoot = value;
244
+ index += 1;
245
+ continue;
246
+ }
247
+ if (arg === '--swarm') {
248
+ const value = argv[index + 1];
249
+ if (!value) {
250
+ throw new Error('--swarm 옵션 값이 필요합니다.');
251
+ }
252
+ swarmName = value;
253
+ index += 1;
254
+ continue;
255
+ }
256
+ if (arg === '--watch') {
257
+ watch = true;
258
+ continue;
259
+ }
260
+ throw new Error(`지원하지 않는 runtime-runner 옵션입니다: ${arg}`);
261
+ }
262
+ if (!bundlePath) {
263
+ throw new Error('bundlePath가 필요합니다.');
264
+ }
265
+ if (!instanceKey) {
266
+ throw new Error('instanceKey가 필요합니다.');
267
+ }
268
+ if (!stateRoot) {
269
+ throw new Error('stateRoot가 필요합니다.');
270
+ }
271
+ return {
272
+ bundlePath: path.resolve(bundlePath),
273
+ instanceKey,
274
+ stateRoot: path.resolve(stateRoot),
275
+ swarmName,
276
+ watch,
277
+ };
278
+ }
279
+ function sendMessage(message) {
280
+ if (typeof process.send === 'function') {
281
+ process.send(message);
282
+ }
283
+ }
284
+ async function existsFile(filePath) {
285
+ try {
286
+ await access(filePath, fsConstants.R_OK);
287
+ return true;
288
+ }
289
+ catch {
290
+ return false;
291
+ }
292
+ }
293
+ function readSpecRecord(resource) {
294
+ if (!isJsonObject(resource.spec)) {
295
+ throw new Error(`${resource.kind}/${resource.metadata.name} spec 형식이 잘못되었습니다.`);
296
+ }
297
+ return resource.spec;
298
+ }
299
+ function collectEnvRequirements(resources) {
300
+ const requirements = [];
301
+ const seen = new Set();
302
+ const visit = (value, resourceId) => {
303
+ if (Array.isArray(value)) {
304
+ for (const item of value) {
305
+ visit(item, resourceId);
306
+ }
307
+ return;
308
+ }
309
+ if (!isJsonObject(value)) {
310
+ return;
311
+ }
312
+ const valueFrom = value.valueFrom;
313
+ if (isJsonObject(valueFrom) && typeof valueFrom.env === 'string' && valueFrom.env.trim().length > 0) {
314
+ const envName = valueFrom.env.trim();
315
+ const identity = `${resourceId}:${envName}`;
316
+ if (!seen.has(identity)) {
317
+ seen.add(identity);
318
+ requirements.push({
319
+ envName,
320
+ resourceId,
321
+ });
322
+ }
323
+ }
324
+ for (const nested of Object.values(value)) {
325
+ visit(nested, resourceId);
326
+ }
327
+ };
328
+ for (const resource of resources) {
329
+ const resourceId = `${resource.kind}/${resource.metadata.name}`;
330
+ visit(resource.spec, resourceId);
331
+ }
332
+ return requirements;
333
+ }
334
+ function summarizeMissingEnv(requirements, env) {
335
+ const missingByEnv = new Map();
336
+ for (const requirement of requirements) {
337
+ const value = env[requirement.envName];
338
+ if (typeof value === 'string' && value.trim().length > 0) {
339
+ continue;
340
+ }
341
+ if (!missingByEnv.has(requirement.envName)) {
342
+ missingByEnv.set(requirement.envName, requirement);
343
+ }
344
+ }
345
+ if (missingByEnv.size === 0) {
346
+ return undefined;
347
+ }
348
+ const detail = [...missingByEnv.values()]
349
+ .map((item) => `${item.envName} (${item.resourceId})`)
350
+ .join(', ');
351
+ return `필수 환경 변수가 없습니다: ${detail}`;
352
+ }
353
+ function formatValidationErrors(errors) {
354
+ return errors
355
+ .map((error) => {
356
+ const parts = [error.code, error.path, error.message].filter((part) => part.trim().length > 0);
357
+ return parts.join(' | ');
358
+ })
359
+ .join('\n');
360
+ }
361
+ function isObjectRefLike(value) {
362
+ if (typeof value === 'string') {
363
+ return true;
364
+ }
365
+ if (!isJsonObject(value)) {
366
+ return false;
367
+ }
368
+ if (typeof value.kind !== 'string' || typeof value.name !== 'string') {
369
+ return false;
370
+ }
371
+ if ('package' in value && value.package !== undefined && typeof value.package !== 'string') {
372
+ return false;
373
+ }
374
+ if ('apiVersion' in value && value.apiVersion !== undefined && typeof value.apiVersion !== 'string') {
375
+ return false;
376
+ }
377
+ return true;
378
+ }
379
+ function extractRefLike(value) {
380
+ if (isObjectRefLike(value)) {
381
+ return value;
382
+ }
383
+ if (!isJsonObject(value)) {
384
+ return undefined;
385
+ }
386
+ const ref = value.ref;
387
+ if (isObjectRefLike(ref)) {
388
+ return ref;
389
+ }
390
+ return undefined;
391
+ }
392
+ function resolveConnectorCandidates(baseDir, entry) {
393
+ const normalizedEntry = entry.startsWith('./') ? entry.slice(2) : entry;
394
+ const directPath = path.resolve(baseDir, normalizedEntry);
395
+ const candidates = [];
396
+ if (directPath.endsWith('.ts')) {
397
+ if (normalizedEntry.startsWith('src/')) {
398
+ const distRelative = normalizedEntry.replace(/^src\//, 'dist/src/').replace(/\.ts$/, '.js');
399
+ candidates.push(path.resolve(baseDir, distRelative));
400
+ }
401
+ const directJs = directPath.slice(0, -3) + '.js';
402
+ candidates.push(directJs);
403
+ candidates.push(directPath);
404
+ }
405
+ else {
406
+ candidates.push(directPath);
407
+ }
408
+ const deduped = new Set();
409
+ const result = [];
410
+ for (const candidate of candidates) {
411
+ if (deduped.has(candidate)) {
412
+ continue;
413
+ }
414
+ deduped.add(candidate);
415
+ result.push(candidate);
416
+ }
417
+ return result;
418
+ }
419
+ async function resolveEntryPath(resource, fieldName) {
420
+ const spec = readSpecRecord(resource);
421
+ const entryValue = spec.entry;
422
+ if (typeof entryValue !== 'string' || entryValue.trim().length === 0) {
423
+ throw new Error(`${resource.kind}/${resource.metadata.name} spec.${fieldName}가 비어 있습니다.`);
424
+ }
425
+ const rootDir = resource.__rootDir ? path.resolve(resource.__rootDir) : process.cwd();
426
+ const candidates = resolveConnectorCandidates(rootDir, entryValue.trim());
427
+ for (const candidate of candidates) {
428
+ if (await existsFile(candidate)) {
429
+ return candidate;
430
+ }
431
+ }
432
+ const listed = candidates.join(', ');
433
+ throw new Error(`${resource.kind}/${resource.metadata.name} entry 파일을 찾을 수 없습니다: ${listed}`);
434
+ }
435
+ function selectReferencedResource(resources, ref, expectedKind, ownerPackageName, selectedSwarmPackage) {
436
+ const normalized = normalizeObjectRef(ref);
437
+ if (normalized.kind !== expectedKind) {
438
+ throw new Error(`참조 kind 불일치: expected=${expectedKind}, actual=${normalized.kind}`);
439
+ }
440
+ const candidates = resources.filter((resource) => resource.kind === expectedKind && resource.metadata.name === normalized.name);
441
+ if (candidates.length === 0) {
442
+ throw new Error(`${expectedKind}/${normalized.name} 리소스를 찾지 못했습니다.`);
443
+ }
444
+ if (normalized.package) {
445
+ const byPackage = candidates.find((resource) => resource.__package === normalized.package);
446
+ if (!byPackage) {
447
+ throw new Error(`${expectedKind}/${normalized.name} (${normalized.package}) 리소스를 찾지 못했습니다.`);
448
+ }
449
+ return byPackage;
450
+ }
451
+ if (ownerPackageName) {
452
+ const byOwnerPackage = candidates.find((resource) => resource.__package === ownerPackageName);
453
+ if (byOwnerPackage) {
454
+ return byOwnerPackage;
455
+ }
456
+ }
457
+ if (selectedSwarmPackage) {
458
+ const bySwarmPackage = candidates.find((resource) => resource.__package === selectedSwarmPackage);
459
+ if (bySwarmPackage) {
460
+ return bySwarmPackage;
461
+ }
462
+ }
463
+ if (candidates.length === 1) {
464
+ const single = candidates[0];
465
+ if (!single) {
466
+ throw new Error(`${expectedKind}/${normalized.name} 단일 후보를 선택할 수 없습니다.`);
467
+ }
468
+ return single;
469
+ }
470
+ const packageNames = candidates.map((candidate) => candidate.__package ?? '<local>').join(', ');
471
+ throw new Error(`${expectedKind}/${normalized.name} 후보가 여러 개여서 선택할 수 없습니다. package를 명시하세요. candidates: ${packageNames}`);
472
+ }
473
+ function parseSwarmAgentRef(value) {
474
+ const ref = extractRefLike(value);
475
+ if (!ref) {
476
+ throw new Error('Swarm Agent ref 형식이 올바르지 않습니다.');
477
+ }
478
+ const normalized = normalizeObjectRef(ref);
479
+ if (normalized.kind !== 'Agent') {
480
+ throw new Error(`Swarm agents는 Agent를 가리켜야 합니다: ${normalized.kind}/${normalized.name}`);
481
+ }
482
+ return {
483
+ name: normalized.name,
484
+ packageName: normalized.package,
485
+ };
486
+ }
487
+ function parseSwarmSelection(resources, requestedName) {
488
+ const swarms = resources.filter((resource) => resource.kind === 'Swarm');
489
+ if (swarms.length === 0) {
490
+ throw new Error('Swarm 리소스를 찾지 못했습니다.');
491
+ }
492
+ let swarmResource;
493
+ if (requestedName) {
494
+ swarmResource = swarms.find((swarm) => swarm.metadata.name === requestedName);
495
+ if (!swarmResource) {
496
+ throw new Error(`Swarm '${requestedName}'을(를) 찾지 못했습니다.`);
497
+ }
498
+ }
499
+ else {
500
+ const defaultSwarm = swarms.find((swarm) => swarm.metadata.name === 'default');
501
+ if (defaultSwarm) {
502
+ swarmResource = defaultSwarm;
503
+ }
504
+ else if (swarms.length === 1) {
505
+ swarmResource = swarms[0];
506
+ }
507
+ else {
508
+ const names = swarms.map((swarm) => swarm.metadata.name).join(', ');
509
+ throw new Error(`실행할 Swarm을 선택할 수 없습니다. --swarm 옵션을 지정하세요. candidates: ${names}`);
510
+ }
511
+ }
512
+ if (!swarmResource) {
513
+ throw new Error('Swarm을 선택할 수 없습니다.');
514
+ }
515
+ const spec = readSpecRecord(swarmResource);
516
+ const entryAgent = parseSwarmAgentRef(spec.entryAgent);
517
+ const rawAgents = spec.agents;
518
+ if (!Array.isArray(rawAgents) || rawAgents.length === 0) {
519
+ throw new Error(`Swarm/${swarmResource.metadata.name} spec.agents가 비어 있습니다.`);
520
+ }
521
+ const agents = rawAgents.map((item) => parseSwarmAgentRef(item));
522
+ if (!agents.some((agent) => agent.name === entryAgent.name && agent.packageName === entryAgent.packageName)) {
523
+ agents.push(entryAgent);
524
+ }
525
+ return {
526
+ swarmResource,
527
+ selectedSwarm: {
528
+ name: swarmResource.metadata.name,
529
+ packageName: swarmResource.__package,
530
+ entryAgent,
531
+ agents,
532
+ },
533
+ };
534
+ }
535
+ function hasMatchingSwarm(resource, selectedSwarm) {
536
+ const spec = readSpecRecord(resource);
537
+ const swarmRef = spec.swarmRef;
538
+ if (swarmRef === undefined) {
539
+ return true;
540
+ }
541
+ if (!isObjectRefLike(swarmRef)) {
542
+ throw new Error(`Connection/${resource.metadata.name} spec.swarmRef 형식이 올바르지 않습니다.`);
543
+ }
544
+ const normalized = normalizeObjectRef(swarmRef);
545
+ if (normalized.kind !== 'Swarm') {
546
+ throw new Error(`Connection/${resource.metadata.name} spec.swarmRef는 Swarm을 가리켜야 합니다.`);
547
+ }
548
+ if (normalized.package && selectedSwarm.packageName && normalized.package !== selectedSwarm.packageName) {
549
+ return false;
550
+ }
551
+ return normalized.name === selectedSwarm.name;
552
+ }
553
+ function normalizeEnvToken(value) {
554
+ return value
555
+ .trim()
556
+ .replace(/[^A-Za-z0-9]+/g, '_')
557
+ .replace(/^_+/, '')
558
+ .replace(/_+$/, '')
559
+ .toUpperCase();
560
+ }
561
+ function parseSecretRefName(refValue) {
562
+ const trimmedRef = refValue.trim();
563
+ const match = /^Secret\/([^/]+)$/.exec(trimmedRef);
564
+ if (!match) {
565
+ return undefined;
566
+ }
567
+ const secretName = match[1];
568
+ if (!secretName) {
569
+ return undefined;
570
+ }
571
+ const trimmedName = secretName.trim();
572
+ if (trimmedName.length === 0) {
573
+ return undefined;
574
+ }
575
+ return trimmedName;
576
+ }
577
+ function buildSecretRefEnvCandidates(secretName, fieldName, keyName) {
578
+ const secretToken = normalizeEnvToken(secretName);
579
+ const fieldToken = normalizeEnvToken(fieldName);
580
+ const keyToken = normalizeEnvToken(keyName);
581
+ const candidates = [
582
+ `GOONDAN_SECRET_${secretToken}_${keyToken}`,
583
+ `GOONDAN_SECRET_${secretToken}_${fieldToken}`,
584
+ `GOONDAN_SECRET_${secretToken}`,
585
+ `SECRET_${secretToken}_${keyToken}`,
586
+ `SECRET_${secretToken}_${fieldToken}`,
587
+ `SECRET_${secretToken}`,
588
+ ];
589
+ const seen = new Set();
590
+ const deduped = [];
591
+ for (const candidate of candidates) {
592
+ if (seen.has(candidate)) {
593
+ continue;
594
+ }
595
+ seen.add(candidate);
596
+ deduped.push(candidate);
597
+ }
598
+ return deduped;
599
+ }
600
+ function resolveSecretRefValue(connectionName, sectionName, fieldName, secretRef, env) {
601
+ if (!isJsonObject(secretRef)) {
602
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}' valueFrom.secretRef 형식이 올바르지 않습니다.`);
603
+ }
604
+ const refValue = secretRef.ref;
605
+ if (typeof refValue !== 'string' || refValue.trim().length === 0) {
606
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}' valueFrom.secretRef.ref 형식이 올바르지 않습니다.`);
607
+ }
608
+ const secretName = parseSecretRefName(refValue);
609
+ if (!secretName) {
610
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}' valueFrom.secretRef.ref(${refValue})는 Secret/<name> 형식이어야 합니다.`);
611
+ }
612
+ const keyValue = secretRef.key;
613
+ let keyName = fieldName;
614
+ if (keyValue !== undefined) {
615
+ if (typeof keyValue !== 'string' || keyValue.trim().length === 0) {
616
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}' valueFrom.secretRef.key 형식이 올바르지 않습니다.`);
617
+ }
618
+ keyName = keyValue.trim();
619
+ }
620
+ const envCandidates = buildSecretRefEnvCandidates(secretName, fieldName, keyName);
621
+ if (envCandidates.length === 0) {
622
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}' secretRef(ref=${refValue}, key=${keyName}) env 후보를 생성할 수 없습니다.`);
623
+ }
624
+ for (const envName of envCandidates) {
625
+ const envValue = env[envName];
626
+ if (typeof envValue === 'string' && envValue.trim().length > 0) {
627
+ return envValue;
628
+ }
629
+ }
630
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}' secretRef(ref=${refValue}, key=${keyName}) 값을 해석할 수 없습니다. env candidates: ${envCandidates.join(', ')}`);
631
+ }
632
+ function resolveConnectionValue(connectionName, sectionName, fieldName, source, env) {
633
+ if (!isJsonObject(source)) {
634
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}' 형식이 올바르지 않습니다.`);
635
+ }
636
+ const inlineValue = source.value;
637
+ const valueFrom = source.valueFrom;
638
+ const hasInlineValue = typeof inlineValue === 'string';
639
+ const hasValueFrom = isJsonObject(valueFrom);
640
+ if (hasInlineValue && hasValueFrom) {
641
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}'는 value와 valueFrom을 동시에 가질 수 없습니다.`);
642
+ }
643
+ if (hasInlineValue) {
644
+ return inlineValue;
645
+ }
646
+ if (!hasValueFrom) {
647
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}'는 value 또는 valueFrom이 필요합니다.`);
648
+ }
649
+ const envNameValue = valueFrom.env;
650
+ const secretRefValue = valueFrom.secretRef;
651
+ const hasEnv = typeof envNameValue === 'string';
652
+ const hasSecretRef = secretRefValue !== undefined;
653
+ if (hasEnv && hasSecretRef) {
654
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}'는 valueFrom.env와 valueFrom.secretRef를 동시에 가질 수 없습니다.`);
655
+ }
656
+ if (hasEnv) {
657
+ const envName = envNameValue.trim();
658
+ if (envName.length === 0) {
659
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}' valueFrom.env 형식이 올바르지 않습니다.`);
660
+ }
661
+ const envValue = env[envName];
662
+ if (typeof envValue !== 'string' || envValue.trim().length === 0) {
663
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}' env(${envName}) 값을 찾을 수 없습니다.`);
664
+ }
665
+ return envValue;
666
+ }
667
+ if (hasSecretRef) {
668
+ return resolveSecretRefValue(connectionName, sectionName, fieldName, secretRefValue, env);
669
+ }
670
+ throw new Error(`Connection/${connectionName} ${sectionName} '${fieldName}'는 valueFrom.env 또는 valueFrom.secretRef가 필요합니다.`);
671
+ }
672
+ function resolveConnectionSecrets(resource, env) {
673
+ const spec = readSpecRecord(resource);
674
+ const value = spec.secrets;
675
+ if (value === undefined) {
676
+ return {};
677
+ }
678
+ if (!isJsonObject(value)) {
679
+ throw new Error(`Connection/${resource.metadata.name} spec.secrets 형식이 올바르지 않습니다.`);
680
+ }
681
+ const secrets = {};
682
+ for (const [secretName, source] of Object.entries(value)) {
683
+ secrets[secretName] = resolveConnectionValue(resource.metadata.name, 'secret', secretName, source, env);
684
+ }
685
+ return secrets;
686
+ }
687
+ function resolveConnectionConfig(resource, env) {
688
+ const spec = readSpecRecord(resource);
689
+ const value = spec.config;
690
+ if (value === undefined) {
691
+ return {};
692
+ }
693
+ if (!isJsonObject(value)) {
694
+ throw new Error(`Connection/${resource.metadata.name} spec.config 형식이 올바르지 않습니다.`);
695
+ }
696
+ const config = {};
697
+ for (const [secretName, source] of Object.entries(value)) {
698
+ config[secretName] = resolveConnectionValue(resource.metadata.name, 'config', secretName, source, env);
699
+ }
700
+ return config;
701
+ }
702
+ function parseIngressRouteRules(connection, selectedSwarm) {
703
+ const spec = readSpecRecord(connection);
704
+ const ingress = spec.ingress;
705
+ if (ingress === undefined) {
706
+ return [];
707
+ }
708
+ if (!isJsonObject(ingress)) {
709
+ throw new Error(`Connection/${connection.metadata.name} spec.ingress 형식이 올바르지 않습니다.`);
710
+ }
711
+ const rulesValue = ingress.rules;
712
+ if (rulesValue === undefined) {
713
+ return [];
714
+ }
715
+ if (!Array.isArray(rulesValue)) {
716
+ throw new Error(`Connection/${connection.metadata.name} spec.ingress.rules 형식이 올바르지 않습니다.`);
717
+ }
718
+ const rules = [];
719
+ for (const ruleValue of rulesValue) {
720
+ if (!isJsonObject(ruleValue)) {
721
+ throw new Error(`Connection/${connection.metadata.name} ingress rule 형식이 올바르지 않습니다.`);
722
+ }
723
+ const matchValue = ruleValue.match;
724
+ let eventName;
725
+ let properties;
726
+ if (matchValue !== undefined) {
727
+ if (!isJsonObject(matchValue)) {
728
+ throw new Error(`Connection/${connection.metadata.name} ingress.match 형식이 올바르지 않습니다.`);
729
+ }
730
+ if (typeof matchValue.event === 'string' && matchValue.event.trim().length > 0) {
731
+ eventName = matchValue.event;
732
+ }
733
+ if (isJsonObject(matchValue.properties)) {
734
+ const normalized = {};
735
+ for (const [key, value] of Object.entries(matchValue.properties)) {
736
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
737
+ normalized[key] = String(value);
738
+ }
739
+ }
740
+ properties = Object.keys(normalized).length > 0 ? normalized : undefined;
741
+ }
742
+ }
743
+ const routeValue = ruleValue.route;
744
+ if (!isJsonObject(routeValue)) {
745
+ throw new Error(`Connection/${connection.metadata.name} ingress.route 형식이 올바르지 않습니다.`);
746
+ }
747
+ let agent;
748
+ if (routeValue.agentRef !== undefined) {
749
+ const agentRef = extractRefLike(routeValue.agentRef);
750
+ if (!agentRef) {
751
+ throw new Error(`Connection/${connection.metadata.name} ingress.route.agentRef 형식이 올바르지 않습니다.`);
752
+ }
753
+ const normalized = normalizeObjectRef(agentRef);
754
+ if (normalized.kind !== 'Agent') {
755
+ throw new Error(`Connection/${connection.metadata.name} ingress.route.agentRef는 Agent를 가리켜야 합니다.`);
756
+ }
757
+ const found = selectedSwarm.agents.find((item) => item.name === normalized.name && (normalized.package ? item.packageName === normalized.package : true));
758
+ if (!found) {
759
+ throw new Error(`Connection/${connection.metadata.name} ingress.route.agentRef(${normalized.name})가 Swarm agents에 없습니다.`);
760
+ }
761
+ agent = {
762
+ name: normalized.name,
763
+ packageName: normalized.package,
764
+ };
765
+ }
766
+ let instanceKey;
767
+ if (routeValue.instanceKey !== undefined) {
768
+ const value = readStringValue(routeValue, 'instanceKey');
769
+ if (!value || value.trim().length === 0) {
770
+ throw new Error(`Connection/${connection.metadata.name} ingress.route.instanceKey 형식이 올바르지 않습니다.`);
771
+ }
772
+ instanceKey = value.trim();
773
+ }
774
+ let instanceKeyProperty;
775
+ if (routeValue.instanceKeyProperty !== undefined) {
776
+ const value = readStringValue(routeValue, 'instanceKeyProperty');
777
+ if (!value || value.trim().length === 0) {
778
+ throw new Error(`Connection/${connection.metadata.name} ingress.route.instanceKeyProperty 형식이 올바르지 않습니다.`);
779
+ }
780
+ instanceKeyProperty = value.trim();
781
+ }
782
+ let instanceKeyPrefix;
783
+ if (routeValue.instanceKeyPrefix !== undefined) {
784
+ const value = readStringValue(routeValue, 'instanceKeyPrefix');
785
+ if (value === undefined) {
786
+ throw new Error(`Connection/${connection.metadata.name} ingress.route.instanceKeyPrefix 형식이 올바르지 않습니다.`);
787
+ }
788
+ instanceKeyPrefix = value;
789
+ }
790
+ if (instanceKey && instanceKeyProperty) {
791
+ throw new Error(`Connection/${connection.metadata.name} ingress.route.instanceKey와 instanceKeyProperty는 동시에 지정할 수 없습니다.`);
792
+ }
793
+ rules.push({
794
+ eventName,
795
+ properties,
796
+ agent,
797
+ instanceKey,
798
+ instanceKeyProperty,
799
+ instanceKeyPrefix,
800
+ });
801
+ }
802
+ return rules;
803
+ }
804
+ function readStringValue(record, key) {
805
+ const value = record[key];
806
+ if (typeof value === 'string') {
807
+ return value;
808
+ }
809
+ return undefined;
810
+ }
811
+ function readNumberValue(record, key) {
812
+ const value = record[key];
813
+ if (typeof value === 'number' && Number.isFinite(value)) {
814
+ return value;
815
+ }
816
+ return undefined;
817
+ }
818
+ function resolveModelApiKey(modelSpec, env, modelName) {
819
+ const apiKey = modelSpec.apiKey;
820
+ if (!isJsonObject(apiKey)) {
821
+ throw new Error(`Model/${modelName} spec.apiKey 형식이 올바르지 않습니다.`);
822
+ }
823
+ if (typeof apiKey.value === 'string' && apiKey.value.trim().length > 0) {
824
+ return apiKey.value;
825
+ }
826
+ const valueFrom = apiKey.valueFrom;
827
+ if (!isJsonObject(valueFrom) || typeof valueFrom.env !== 'string' || valueFrom.env.trim().length === 0) {
828
+ throw new Error(`Model/${modelName} spec.apiKey.valueFrom.env가 필요합니다.`);
829
+ }
830
+ const envValue = env[valueFrom.env];
831
+ if (typeof envValue !== 'string' || envValue.trim().length === 0) {
832
+ throw new Error(`Model/${modelName} API key env(${valueFrom.env}) 값을 찾을 수 없습니다.`);
833
+ }
834
+ return envValue;
835
+ }
836
+ function isJsonLiteral(value) {
837
+ return value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
838
+ }
839
+ function parseJsonSchemaProperty(value) {
840
+ if (!isJsonObject(value)) {
841
+ return undefined;
842
+ }
843
+ const result = {};
844
+ if (typeof value.type === 'string') {
845
+ result.type = value.type;
846
+ }
847
+ else if (Array.isArray(value.type) && value.type.every((item) => typeof item === 'string')) {
848
+ result.type = [...value.type];
849
+ }
850
+ if (typeof value.description === 'string') {
851
+ result.description = value.description;
852
+ }
853
+ if (Array.isArray(value.enum)) {
854
+ const enumValues = [];
855
+ for (const item of value.enum) {
856
+ if (isJsonLiteral(item)) {
857
+ enumValues.push(item);
858
+ }
859
+ }
860
+ if (enumValues.length > 0) {
861
+ result.enum = enumValues;
862
+ }
863
+ }
864
+ if (isJsonObject(value.properties)) {
865
+ const properties = {};
866
+ for (const [key, child] of Object.entries(value.properties)) {
867
+ const parsedChild = parseJsonSchemaProperty(child);
868
+ if (parsedChild) {
869
+ properties[key] = parsedChild;
870
+ }
871
+ }
872
+ if (Object.keys(properties).length > 0) {
873
+ result.properties = properties;
874
+ }
875
+ }
876
+ if (value.items !== undefined) {
877
+ const parsedItems = parseJsonSchemaProperty(value.items);
878
+ if (parsedItems) {
879
+ result.items = parsedItems;
880
+ }
881
+ }
882
+ return Object.keys(result).length > 0 ? result : undefined;
883
+ }
884
+ function parseJsonSchemaObject(value) {
885
+ if (!isJsonObject(value)) {
886
+ return undefined;
887
+ }
888
+ const type = value.type;
889
+ if (type !== 'object') {
890
+ return undefined;
891
+ }
892
+ const result = {
893
+ type: 'object',
894
+ };
895
+ if (isJsonObject(value.properties)) {
896
+ const parsedProps = {};
897
+ for (const [key, rawProp] of Object.entries(value.properties)) {
898
+ const parsedProp = parseJsonSchemaProperty(rawProp);
899
+ if (parsedProp) {
900
+ parsedProps[key] = parsedProp;
901
+ }
902
+ }
903
+ if (Object.keys(parsedProps).length > 0) {
904
+ result.properties = parsedProps;
905
+ }
906
+ }
907
+ if (Array.isArray(value.required)) {
908
+ const required = value.required.filter((item) => typeof item === 'string');
909
+ if (required.length > 0) {
910
+ result.required = required;
911
+ }
912
+ }
913
+ if (typeof value.additionalProperties === 'boolean') {
914
+ result.additionalProperties = value.additionalProperties;
915
+ }
916
+ return result;
917
+ }
918
+ function createDefaultObjectSchema() {
919
+ return {
920
+ type: 'object',
921
+ properties: {},
922
+ };
923
+ }
924
+ function isFunction(value) {
925
+ return typeof value === 'function';
926
+ }
927
+ function readToolHandlers(module) {
928
+ if (!isJsonObject(module)) {
929
+ return undefined;
930
+ }
931
+ const handlersValue = module.handlers;
932
+ if (!isJsonObject(handlersValue)) {
933
+ return undefined;
934
+ }
935
+ const handlers = {};
936
+ for (const [name, maybeHandler] of Object.entries(handlersValue)) {
937
+ if (!isFunction(maybeHandler)) {
938
+ continue;
939
+ }
940
+ handlers[name] = (ctx, input) => maybeHandler(ctx, input);
941
+ }
942
+ return Object.keys(handlers).length > 0 ? handlers : undefined;
943
+ }
944
+ function toJsonValue(value) {
945
+ if (value === null) {
946
+ return null;
947
+ }
948
+ if (typeof value === 'string') {
949
+ return value;
950
+ }
951
+ if (typeof value === 'number') {
952
+ return Number.isFinite(value) ? value : String(value);
953
+ }
954
+ if (typeof value === 'boolean') {
955
+ return value;
956
+ }
957
+ if (Array.isArray(value)) {
958
+ return value.map((item) => toJsonValue(item));
959
+ }
960
+ if (isJsonObject(value)) {
961
+ const converted = {};
962
+ for (const [key, nested] of Object.entries(value)) {
963
+ converted[key] = toJsonValue(nested);
964
+ }
965
+ return converted;
966
+ }
967
+ if (value === undefined) {
968
+ return null;
969
+ }
970
+ return String(value);
971
+ }
972
+ function ensureJsonObject(value) {
973
+ if (!isJsonObject(value)) {
974
+ return {};
975
+ }
976
+ const converted = {};
977
+ for (const [key, nested] of Object.entries(value)) {
978
+ converted[key] = toJsonValue(nested);
979
+ }
980
+ return converted;
981
+ }
982
+ function parsePositiveInteger(value, fallback) {
983
+ if (typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value) && value > 0) {
984
+ return value;
985
+ }
986
+ return fallback;
987
+ }
988
+ async function readAgentSystemPrompt(agent) {
989
+ const spec = readSpecRecord(agent);
990
+ const prompts = spec.prompts;
991
+ if (!isJsonObject(prompts)) {
992
+ return '';
993
+ }
994
+ const inlineSystem = prompts.system;
995
+ if (typeof inlineSystem === 'string') {
996
+ return inlineSystem;
997
+ }
998
+ const legacySystemPrompt = prompts.systemPrompt;
999
+ if (typeof legacySystemPrompt === 'string') {
1000
+ return legacySystemPrompt;
1001
+ }
1002
+ const systemRef = prompts.systemRef;
1003
+ if (typeof systemRef !== 'string' || systemRef.trim().length === 0) {
1004
+ return '';
1005
+ }
1006
+ const rootDir = agent.__rootDir ? path.resolve(agent.__rootDir) : process.cwd();
1007
+ const promptPath = path.resolve(rootDir, systemRef);
1008
+ const promptExists = await existsFile(promptPath);
1009
+ if (!promptExists) {
1010
+ throw new Error(`Agent/${agent.metadata.name} system prompt 파일을 찾을 수 없습니다: ${promptPath}`);
1011
+ }
1012
+ return await readFile(promptPath, 'utf8');
1013
+ }
1014
+ function parseAgentToolRefs(agent) {
1015
+ const spec = readSpecRecord(agent);
1016
+ const toolsValue = spec.tools;
1017
+ if (toolsValue === undefined) {
1018
+ return [];
1019
+ }
1020
+ if (!Array.isArray(toolsValue)) {
1021
+ throw new Error(`Agent/${agent.metadata.name} spec.tools 형식이 올바르지 않습니다.`);
1022
+ }
1023
+ const refs = [];
1024
+ for (const item of toolsValue) {
1025
+ const ref = extractRefLike(item);
1026
+ if (!ref) {
1027
+ throw new Error(`Agent/${agent.metadata.name} tool ref 형식이 올바르지 않습니다.`);
1028
+ }
1029
+ refs.push(ref);
1030
+ }
1031
+ return refs;
1032
+ }
1033
+ function parseAgentExtensionRefs(agent) {
1034
+ const spec = readSpecRecord(agent);
1035
+ const extensionsValue = spec.extensions;
1036
+ if (extensionsValue === undefined) {
1037
+ return [];
1038
+ }
1039
+ if (!Array.isArray(extensionsValue)) {
1040
+ throw new Error(`Agent/${agent.metadata.name} spec.extensions 형식이 올바르지 않습니다.`);
1041
+ }
1042
+ const refs = [];
1043
+ for (const item of extensionsValue) {
1044
+ const ref = extractRefLike(item);
1045
+ if (!ref) {
1046
+ throw new Error(`Agent/${agent.metadata.name} extension ref 형식이 올바르지 않습니다.`);
1047
+ }
1048
+ refs.push(ref);
1049
+ }
1050
+ return refs;
1051
+ }
1052
+ function toExtensionResource(resource) {
1053
+ const spec = readSpecRecord(resource);
1054
+ const entry = readStringValue(spec, 'entry');
1055
+ if (!entry || entry.trim().length === 0) {
1056
+ throw new Error(`Extension/${resource.metadata.name} spec.entry 형식이 올바르지 않습니다.`);
1057
+ }
1058
+ let config;
1059
+ const configValue = spec.config;
1060
+ if (configValue !== undefined) {
1061
+ if (!isJsonObject(configValue)) {
1062
+ throw new Error(`Extension/${resource.metadata.name} spec.config 형식이 올바르지 않습니다.`);
1063
+ }
1064
+ config = { ...configValue };
1065
+ }
1066
+ return {
1067
+ ...resource,
1068
+ spec: {
1069
+ entry,
1070
+ config,
1071
+ },
1072
+ };
1073
+ }
1074
+ function parseAgentRequiredTools(agent) {
1075
+ const spec = readSpecRecord(agent);
1076
+ const requiredToolsValue = spec.requiredTools;
1077
+ if (requiredToolsValue === undefined) {
1078
+ return [];
1079
+ }
1080
+ if (!Array.isArray(requiredToolsValue)) {
1081
+ throw new Error(`Agent/${agent.metadata.name} spec.requiredTools 형식이 올바르지 않습니다.`);
1082
+ }
1083
+ const names = [];
1084
+ for (const value of requiredToolsValue) {
1085
+ if (typeof value !== 'string' || value.trim().length === 0) {
1086
+ throw new Error(`Agent/${agent.metadata.name} spec.requiredTools에는 비어있지 않은 문자열만 허용됩니다.`);
1087
+ }
1088
+ names.push(value.trim());
1089
+ }
1090
+ return [...new Set(names)];
1091
+ }
1092
+ async function registerToolResource(resource, toolRegistry) {
1093
+ const entryPath = await resolveEntryPath(resource, 'entry');
1094
+ const moduleValue = await import(pathToFileURL(entryPath).href);
1095
+ const handlers = readToolHandlers(moduleValue);
1096
+ if (!handlers) {
1097
+ throw new Error(`Tool/${resource.metadata.name} 모듈에 handlers export가 없습니다: ${entryPath}`);
1098
+ }
1099
+ const spec = readSpecRecord(resource);
1100
+ const exportsValue = spec.exports;
1101
+ if (!Array.isArray(exportsValue) || exportsValue.length === 0) {
1102
+ throw new Error(`Tool/${resource.metadata.name} spec.exports가 비어 있습니다.`);
1103
+ }
1104
+ const catalogItems = [];
1105
+ for (const exportItem of exportsValue) {
1106
+ if (!isJsonObject(exportItem)) {
1107
+ continue;
1108
+ }
1109
+ const exportName = readStringValue(exportItem, 'name');
1110
+ if (!exportName) {
1111
+ continue;
1112
+ }
1113
+ const handler = handlers[exportName];
1114
+ if (!handler) {
1115
+ throw new Error(`Tool/${resource.metadata.name} handlers.${exportName}를 찾을 수 없습니다.`);
1116
+ }
1117
+ const toolName = buildToolName(resource.metadata.name, exportName);
1118
+ const description = readStringValue(exportItem, 'description');
1119
+ const parameters = parseJsonSchemaObject(exportItem.parameters) ?? createDefaultObjectSchema();
1120
+ const catalogItem = {
1121
+ name: toolName,
1122
+ description,
1123
+ parameters,
1124
+ source: {
1125
+ type: 'config',
1126
+ name: resource.metadata.name,
1127
+ },
1128
+ };
1129
+ toolRegistry.register(catalogItem, async (ctx, input) => {
1130
+ const output = await Promise.resolve(handler(ctx, input));
1131
+ return toJsonValue(output);
1132
+ });
1133
+ catalogItems.push(catalogItem);
1134
+ }
1135
+ return {
1136
+ entryPath,
1137
+ catalogItems,
1138
+ };
1139
+ }
1140
+ function parseConnectionConnectorRef(connection) {
1141
+ const spec = readSpecRecord(connection);
1142
+ const connectorRef = spec.connectorRef;
1143
+ if (!isObjectRefLike(connectorRef)) {
1144
+ throw new Error(`Connection/${connection.metadata.name} spec.connectorRef 형식이 올바르지 않습니다.`);
1145
+ }
1146
+ return connectorRef;
1147
+ }
1148
+ function parseAgentModelRef(agent) {
1149
+ const spec = readSpecRecord(agent);
1150
+ const modelConfig = spec.modelConfig;
1151
+ if (!isJsonObject(modelConfig) || !isObjectRefLike(modelConfig.modelRef)) {
1152
+ throw new Error(`Agent/${agent.metadata.name} spec.modelConfig.modelRef 형식이 올바르지 않습니다.`);
1153
+ }
1154
+ return modelConfig.modelRef;
1155
+ }
1156
+ function parseAgentModelParams(agent) {
1157
+ const spec = readSpecRecord(agent);
1158
+ const modelConfig = spec.modelConfig;
1159
+ if (!isJsonObject(modelConfig)) {
1160
+ return {
1161
+ maxTokens: 1000,
1162
+ temperature: 0.2,
1163
+ };
1164
+ }
1165
+ const params = modelConfig.params;
1166
+ if (!isJsonObject(params)) {
1167
+ return {
1168
+ maxTokens: 1000,
1169
+ temperature: 0.2,
1170
+ };
1171
+ }
1172
+ const maxTokens = parsePositiveInteger(readNumberValue(params, 'maxTokens'), 1000);
1173
+ const temperatureRaw = readNumberValue(params, 'temperature');
1174
+ const temperature = typeof temperatureRaw === 'number' && Number.isFinite(temperatureRaw) ? temperatureRaw : 0.2;
1175
+ return {
1176
+ maxTokens,
1177
+ temperature,
1178
+ };
1179
+ }
1180
+ function parseSwarmMaxStepsPerTurn(swarm) {
1181
+ const spec = readSpecRecord(swarm);
1182
+ const policy = spec.policy;
1183
+ if (!isJsonObject(policy)) {
1184
+ return 24;
1185
+ }
1186
+ const maxStepsPerTurn = readNumberValue(policy, 'maxStepsPerTurn');
1187
+ if (typeof maxStepsPerTurn === 'number' && Number.isFinite(maxStepsPerTurn) && maxStepsPerTurn > 0) {
1188
+ return Math.floor(maxStepsPerTurn);
1189
+ }
1190
+ return 24;
1191
+ }
1192
+ function buildRuntimeAgentCatalog(runnerPlan, selfAgent) {
1193
+ const availableAgents = [...runnerPlan.agents.keys()].sort((left, right) => left.localeCompare(right));
1194
+ const callableAgents = availableAgents.filter((agentName) => agentName !== selfAgent);
1195
+ return {
1196
+ swarmName: runnerPlan.selectedSwarm.name,
1197
+ entryAgent: runnerPlan.selectedSwarm.entryAgent.name,
1198
+ selfAgent,
1199
+ availableAgents,
1200
+ callableAgents,
1201
+ };
1202
+ }
1203
+ function mergeToolCatalog(baseCatalog, extensionCatalog) {
1204
+ const merged = [];
1205
+ for (const item of baseCatalog) {
1206
+ if (!merged.some((candidate) => candidate.name === item.name)) {
1207
+ merged.push(item);
1208
+ }
1209
+ }
1210
+ for (const item of extensionCatalog) {
1211
+ if (!merged.some((candidate) => candidate.name === item.name)) {
1212
+ merged.push(item);
1213
+ }
1214
+ }
1215
+ return merged;
1216
+ }
1217
+ function isPathInside(baseDir, targetPath) {
1218
+ const relative = path.relative(baseDir, targetPath);
1219
+ if (relative.length === 0) {
1220
+ return true;
1221
+ }
1222
+ return !relative.startsWith('..') && !path.isAbsolute(relative);
1223
+ }
1224
+ async function buildRunnerPlan(args) {
1225
+ const bundleDir = path.resolve(path.dirname(args.bundlePath));
1226
+ const loader = new BundleLoader({
1227
+ stateRoot: args.stateRoot,
1228
+ });
1229
+ const loaded = await loader.load(bundleDir);
1230
+ if (loaded.errors.length > 0) {
1231
+ throw new Error(formatValidationErrors(loaded.errors));
1232
+ }
1233
+ const localPackageName = loaded.resources.find((resource) => resource.kind === 'Package' && resource.__file === 'goondan.yaml')?.metadata.name;
1234
+ const { swarmResource, selectedSwarm } = parseSwarmSelection(loaded.resources, args.swarmName);
1235
+ const requirements = collectEnvRequirements(loaded.resources);
1236
+ const missingSummary = summarizeMissingEnv(requirements, process.env);
1237
+ if (missingSummary) {
1238
+ throw new Error(missingSummary);
1239
+ }
1240
+ const maxStepsPerTurn = parseSwarmMaxStepsPerTurn(swarmResource);
1241
+ const watchTargets = new Set();
1242
+ watchTargets.add(path.resolve(args.bundlePath));
1243
+ for (const scannedFile of loaded.scannedFiles) {
1244
+ const absolutePath = path.resolve(scannedFile);
1245
+ if (isPathInside(bundleDir, absolutePath)) {
1246
+ watchTargets.add(absolutePath);
1247
+ }
1248
+ }
1249
+ const toolRegistry = new ToolRegistryImpl();
1250
+ const toolExecutor = new ToolExecutor(toolRegistry);
1251
+ const registeredToolResources = new Map();
1252
+ const registeredToolEntryPaths = new Map();
1253
+ const agentPlans = new Map();
1254
+ for (const agentRef of selectedSwarm.agents) {
1255
+ const agentResource = selectReferencedResource(loaded.resources, {
1256
+ kind: 'Agent',
1257
+ name: agentRef.name,
1258
+ package: agentRef.packageName,
1259
+ }, 'Agent', swarmResource.__package, selectedSwarm.packageName);
1260
+ const modelRef = parseAgentModelRef(agentResource);
1261
+ const modelResource = selectReferencedResource(loaded.resources, modelRef, 'Model', agentResource.__package, selectedSwarm.packageName);
1262
+ const modelSpec = readSpecRecord(modelResource);
1263
+ const provider = readStringValue(modelSpec, 'provider');
1264
+ const modelName = readStringValue(modelSpec, 'model');
1265
+ if (!provider || !modelName) {
1266
+ throw new Error(`Model/${modelResource.metadata.name} spec.provider/spec.model이 필요합니다.`);
1267
+ }
1268
+ const apiKey = resolveModelApiKey(modelSpec, process.env, modelResource.metadata.name);
1269
+ const prompt = await readAgentSystemPrompt(agentResource);
1270
+ const modelParams = parseAgentModelParams(agentResource);
1271
+ const toolRefs = parseAgentToolRefs(agentResource);
1272
+ const extensionRefs = parseAgentExtensionRefs(agentResource);
1273
+ const requiredToolNames = parseAgentRequiredTools(agentResource);
1274
+ const agentToolCatalog = [];
1275
+ for (const toolRef of toolRefs) {
1276
+ const toolResource = selectReferencedResource(loaded.resources, toolRef, 'Tool', agentResource.__package, selectedSwarm.packageName);
1277
+ const identity = `${toolResource.__package ?? '__local__'}|${toolResource.metadata.name}`;
1278
+ let catalogForResource = registeredToolResources.get(identity);
1279
+ if (!catalogForResource) {
1280
+ const registration = await registerToolResource(toolResource, toolRegistry);
1281
+ catalogForResource = registration.catalogItems;
1282
+ registeredToolResources.set(identity, catalogForResource);
1283
+ registeredToolEntryPaths.set(identity, registration.entryPath);
1284
+ }
1285
+ const registeredToolEntryPath = registeredToolEntryPaths.get(identity);
1286
+ if (registeredToolEntryPath) {
1287
+ watchTargets.add(registeredToolEntryPath);
1288
+ }
1289
+ for (const item of catalogForResource) {
1290
+ if (!agentToolCatalog.some((candidate) => candidate.name === item.name)) {
1291
+ agentToolCatalog.push(item);
1292
+ }
1293
+ }
1294
+ }
1295
+ const extensionResources = [];
1296
+ for (const extensionRef of extensionRefs) {
1297
+ const rawExtensionResource = selectReferencedResource(loaded.resources, extensionRef, 'Extension', agentResource.__package, selectedSwarm.packageName);
1298
+ const extensionResource = toExtensionResource(rawExtensionResource);
1299
+ const extensionEntryPath = await resolveEntryPath(extensionResource, 'entry');
1300
+ watchTargets.add(extensionEntryPath);
1301
+ extensionResources.push(extensionResource);
1302
+ }
1303
+ const plan = {
1304
+ name: agentResource.metadata.name,
1305
+ modelName,
1306
+ provider,
1307
+ apiKey,
1308
+ systemPrompt: prompt,
1309
+ maxTokens: modelParams.maxTokens,
1310
+ temperature: modelParams.temperature,
1311
+ maxSteps: maxStepsPerTurn,
1312
+ requiredToolNames,
1313
+ toolCatalog: agentToolCatalog,
1314
+ extensionResources,
1315
+ };
1316
+ for (const requiredToolName of requiredToolNames) {
1317
+ const existsInCatalog = agentToolCatalog.some((item) => item.name === requiredToolName);
1318
+ if (!existsInCatalog) {
1319
+ throw new Error(`Agent/${agentResource.metadata.name} spec.requiredTools(${requiredToolName})가 toolCatalog에 없습니다.`);
1320
+ }
1321
+ }
1322
+ agentPlans.set(agentResource.metadata.name, plan);
1323
+ }
1324
+ const connections = loaded.resources.filter((resource) => resource.kind === 'Connection' && hasMatchingSwarm(resource, selectedSwarm));
1325
+ const connectors = [];
1326
+ for (const connection of connections) {
1327
+ const connectorRef = parseConnectionConnectorRef(connection);
1328
+ const connectorResource = selectReferencedResource(loaded.resources, connectorRef, 'Connector', connection.__package, selectedSwarm.packageName);
1329
+ const connectorEntryPath = await resolveEntryPath(connectorResource, 'entry');
1330
+ const config = resolveConnectionConfig(connection, process.env);
1331
+ const secrets = resolveConnectionSecrets(connection, process.env);
1332
+ const routeRules = parseIngressRouteRules(connection, selectedSwarm);
1333
+ watchTargets.add(connectorEntryPath);
1334
+ connectors.push({
1335
+ swarmName: selectedSwarm.name,
1336
+ connectionName: connection.metadata.name,
1337
+ connectorName: connectorResource.metadata.name,
1338
+ connectorEntryPath,
1339
+ config,
1340
+ secrets,
1341
+ routeRules,
1342
+ defaultAgent: selectedSwarm.entryAgent,
1343
+ });
1344
+ }
1345
+ return {
1346
+ selectedSwarm,
1347
+ connectors,
1348
+ agents: agentPlans,
1349
+ toolExecutor,
1350
+ watchTargets: [...watchTargets].sort((left, right) => left.localeCompare(right)),
1351
+ localPackageName,
1352
+ };
1353
+ }
1354
+ function createConnectorLogger(plan) {
1355
+ const prefix = `[goondan-runtime][${plan.connectionName}/${plan.connectorName}]`;
1356
+ const logger = new Console({ stdout: process.stdout, stderr: process.stderr });
1357
+ logger.debug = (...args) => {
1358
+ console.debug(prefix, ...args);
1359
+ };
1360
+ logger.info = (...args) => {
1361
+ console.info(prefix, ...args);
1362
+ };
1363
+ logger.warn = (...args) => {
1364
+ console.warn(prefix, ...args);
1365
+ };
1366
+ logger.error = (...args) => {
1367
+ console.error(prefix, ...args);
1368
+ };
1369
+ return logger;
1370
+ }
1371
+ function queueAgentEvent(state, key, run) {
1372
+ const previous = state.executionQueue.get(key) ?? Promise.resolve();
1373
+ const next = previous
1374
+ .catch(() => {
1375
+ // 이전 턴 실패는 다음 턴 큐 진행을 막지 않는다.
1376
+ })
1377
+ .then(run)
1378
+ .finally(() => {
1379
+ const current = state.executionQueue.get(key);
1380
+ if (current === next) {
1381
+ state.executionQueue.delete(key);
1382
+ }
1383
+ });
1384
+ state.executionQueue.set(key, next);
1385
+ return next;
1386
+ }
1387
+ function createConversationKey(agentName, instanceKey) {
1388
+ return `${agentName}:${instanceKey}`;
1389
+ }
1390
+ function createSpawnOwnerKey(agentName, instanceKey) {
1391
+ return `${agentName}:${instanceKey}`;
1392
+ }
1393
+ function trackSpawnedAgent(runtime, ownerAgent, ownerInstanceKey, record) {
1394
+ const ownerKey = createSpawnOwnerKey(ownerAgent, ownerInstanceKey);
1395
+ let registry = runtime.spawnedAgentsByOwner.get(ownerKey);
1396
+ if (!registry) {
1397
+ registry = new Map();
1398
+ runtime.spawnedAgentsByOwner.set(ownerKey, registry);
1399
+ }
1400
+ const spawnKey = createConversationKey(record.target, record.instanceKey);
1401
+ registry.set(spawnKey, record);
1402
+ }
1403
+ function listSpawnedAgents(runtime, ownerAgent, ownerInstanceKey, includeAll) {
1404
+ if (includeAll) {
1405
+ const records = [];
1406
+ for (const registry of runtime.spawnedAgentsByOwner.values()) {
1407
+ for (const record of registry.values()) {
1408
+ records.push(record);
1409
+ }
1410
+ }
1411
+ return records.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
1412
+ }
1413
+ const ownerKey = createSpawnOwnerKey(ownerAgent, ownerInstanceKey);
1414
+ const registry = runtime.spawnedAgentsByOwner.get(ownerKey);
1415
+ if (!registry) {
1416
+ return [];
1417
+ }
1418
+ return [...registry.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
1419
+ }
1420
+ function resolveAgentWorkdir(runtime, queueKey) {
1421
+ const workdir = runtime.instanceWorkdirs.get(queueKey);
1422
+ if (workdir) {
1423
+ return workdir;
1424
+ }
1425
+ return runtime.workdir;
1426
+ }
1427
+ async function ensureInstanceStorage(runtime, queueKey, agentName) {
1428
+ if (runtime.initializedInstances.has(queueKey)) {
1429
+ return;
1430
+ }
1431
+ await runtime.storage.initializeSystemRoot();
1432
+ const metadata = await runtime.storage.readMetadata(queueKey);
1433
+ if (!metadata) {
1434
+ await runtime.storage.initializeInstanceState(queueKey, agentName);
1435
+ }
1436
+ runtime.initializedInstances.add(queueKey);
1437
+ }
1438
+ async function loadConversationFromStorage(runtime, queueKey, agentName) {
1439
+ await ensureInstanceStorage(runtime, queueKey, agentName);
1440
+ const loaded = await runtime.storage.loadConversation(queueKey);
1441
+ return toConversationTurns(loaded.nextMessages);
1442
+ }
1443
+ async function persistConversationToStorage(runtime, queueKey, agentName, turns) {
1444
+ await ensureInstanceStorage(runtime, queueKey, agentName);
1445
+ const messages = toPersistentMessages(turns);
1446
+ await runtime.storage.writeBaseMessages(queueKey, messages);
1447
+ await runtime.storage.clearEvents(queueKey);
1448
+ }
1449
+ function createId(prefix) {
1450
+ const random = Math.random().toString(36).slice(2, 10);
1451
+ return `${prefix}-${Date.now()}-${random}`;
1452
+ }
1453
+ function createToolContextMessage(content) {
1454
+ return {
1455
+ id: createId('message'),
1456
+ data: {
1457
+ role: 'user',
1458
+ content,
1459
+ },
1460
+ metadata: {},
1461
+ createdAt: new Date(),
1462
+ source: {
1463
+ type: 'user',
1464
+ },
1465
+ };
1466
+ }
1467
+ function createConversationUserMessage(content) {
1468
+ return {
1469
+ id: createId('message'),
1470
+ data: {
1471
+ role: 'user',
1472
+ content,
1473
+ },
1474
+ metadata: {},
1475
+ createdAt: new Date(),
1476
+ source: {
1477
+ type: 'user',
1478
+ },
1479
+ };
1480
+ }
1481
+ function createConversationAssistantMessage(content, stepId) {
1482
+ return {
1483
+ id: createId('message'),
1484
+ data: {
1485
+ role: 'assistant',
1486
+ content,
1487
+ },
1488
+ metadata: {},
1489
+ createdAt: new Date(),
1490
+ source: {
1491
+ type: 'assistant',
1492
+ stepId,
1493
+ },
1494
+ };
1495
+ }
1496
+ function createConversationStateFromTurns(turns) {
1497
+ const messages = [];
1498
+ for (const turn of turns) {
1499
+ if (turn.role === 'assistant') {
1500
+ messages.push(createConversationAssistantMessage(turn.content, createId('seed-step')));
1501
+ continue;
1502
+ }
1503
+ messages.push(createConversationUserMessage(turn.content));
1504
+ }
1505
+ return new ConversationStateImpl(messages);
1506
+ }
1507
+ function safeJsonStringify(value) {
1508
+ try {
1509
+ return JSON.stringify(value);
1510
+ }
1511
+ catch {
1512
+ return String(value);
1513
+ }
1514
+ }
1515
+ function createLanguageModel(provider, model, apiKey) {
1516
+ const normalizedProvider = provider.trim().toLowerCase();
1517
+ if (normalizedProvider === 'anthropic') {
1518
+ return createAnthropic({ apiKey }).languageModel(model);
1519
+ }
1520
+ if (normalizedProvider === 'openai') {
1521
+ return createOpenAI({ apiKey }).languageModel(model);
1522
+ }
1523
+ if (normalizedProvider === 'google') {
1524
+ return createGoogleGenerativeAI({ apiKey }).languageModel(model);
1525
+ }
1526
+ throw new Error(`지원하지 않는 model provider입니다: ${provider}`);
1527
+ }
1528
+ function toToolResultOutput(value) {
1529
+ if (isJsonObject(value) && value.type === 'text' && typeof value.value === 'string') {
1530
+ return {
1531
+ type: 'text',
1532
+ value: value.value,
1533
+ };
1534
+ }
1535
+ if (isJsonObject(value) && value.type === 'json' && Object.hasOwn(value, 'value')) {
1536
+ return {
1537
+ type: 'json',
1538
+ value: toJsonValue(value.value),
1539
+ };
1540
+ }
1541
+ if (typeof value === 'string') {
1542
+ return {
1543
+ type: 'text',
1544
+ value,
1545
+ };
1546
+ }
1547
+ return {
1548
+ type: 'json',
1549
+ value: toJsonValue(value),
1550
+ };
1551
+ }
1552
+ function parseToolCallPart(block) {
1553
+ const type = typeof block.type === 'string' ? block.type : '';
1554
+ if (type !== 'tool-call' && type !== 'tool_call' && type !== 'tool_use') {
1555
+ return undefined;
1556
+ }
1557
+ const toolCallId = readStringValue(block, 'toolCallId') ?? readStringValue(block, 'id');
1558
+ const toolName = readStringValue(block, 'toolName') ?? readStringValue(block, 'name');
1559
+ if (!toolCallId || !toolName) {
1560
+ return undefined;
1561
+ }
1562
+ return {
1563
+ toolCallId,
1564
+ toolName,
1565
+ input: ensureJsonObject(block.input),
1566
+ };
1567
+ }
1568
+ function parseToolResultPart(block) {
1569
+ const type = typeof block.type === 'string' ? block.type : '';
1570
+ if (type !== 'tool-result' && type !== 'tool_result') {
1571
+ return undefined;
1572
+ }
1573
+ const toolCallId = readStringValue(block, 'toolCallId') ?? readStringValue(block, 'tool_use_id');
1574
+ if (!toolCallId) {
1575
+ return undefined;
1576
+ }
1577
+ const toolName = readStringValue(block, 'toolName') ?? readStringValue(block, 'tool_name') ?? 'unknown-tool';
1578
+ const hasOutput = Object.hasOwn(block, 'output');
1579
+ const rawOutput = hasOutput ? block.output : block.content;
1580
+ const isError = block.is_error === true;
1581
+ if (isError) {
1582
+ return {
1583
+ toolCallId,
1584
+ toolName,
1585
+ output: {
1586
+ type: 'text',
1587
+ value: `ERROR: ${typeof rawOutput === 'string' ? rawOutput : safeJsonStringify(rawOutput)}`,
1588
+ },
1589
+ };
1590
+ }
1591
+ return {
1592
+ toolCallId,
1593
+ toolName,
1594
+ output: toToolResultOutput(rawOutput),
1595
+ };
1596
+ }
1597
+ function normalizeAssistantContent(content) {
1598
+ if (typeof content === 'string') {
1599
+ return content;
1600
+ }
1601
+ if (!Array.isArray(content)) {
1602
+ return safeJsonStringify(content);
1603
+ }
1604
+ const parts = [];
1605
+ for (const item of content) {
1606
+ if (isJsonObject(item)) {
1607
+ const toolCallPart = parseToolCallPart(item);
1608
+ if (toolCallPart) {
1609
+ parts.push({
1610
+ type: 'tool-call',
1611
+ toolCallId: toolCallPart.toolCallId,
1612
+ toolName: toolCallPart.toolName,
1613
+ input: toolCallPart.input,
1614
+ });
1615
+ continue;
1616
+ }
1617
+ const toolResultPart = parseToolResultPart(item);
1618
+ if (toolResultPart) {
1619
+ parts.push({
1620
+ type: 'tool-result',
1621
+ toolCallId: toolResultPart.toolCallId,
1622
+ toolName: toolResultPart.toolName,
1623
+ output: toolResultPart.output,
1624
+ });
1625
+ continue;
1626
+ }
1627
+ if (item.type === 'text' && typeof item.text === 'string') {
1628
+ parts.push({ type: 'text', text: item.text });
1629
+ continue;
1630
+ }
1631
+ }
1632
+ parts.push({
1633
+ type: 'text',
1634
+ text: safeJsonStringify(item),
1635
+ });
1636
+ }
1637
+ return parts;
1638
+ }
1639
+ function normalizeUserMessages(content) {
1640
+ if (typeof content === 'string') {
1641
+ return [{ role: 'user', content }];
1642
+ }
1643
+ if (!Array.isArray(content)) {
1644
+ return [{ role: 'user', content: safeJsonStringify(content) }];
1645
+ }
1646
+ const textParts = [];
1647
+ const toolParts = [];
1648
+ for (const item of content) {
1649
+ if (isJsonObject(item)) {
1650
+ const toolPart = parseToolResultPart(item);
1651
+ if (toolPart) {
1652
+ toolParts.push({
1653
+ type: 'tool-result',
1654
+ toolCallId: toolPart.toolCallId,
1655
+ toolName: toolPart.toolName,
1656
+ output: toolPart.output,
1657
+ });
1658
+ continue;
1659
+ }
1660
+ if (item.type === 'text' && typeof item.text === 'string') {
1661
+ textParts.push({ type: 'text', text: item.text });
1662
+ continue;
1663
+ }
1664
+ }
1665
+ textParts.push({
1666
+ type: 'text',
1667
+ text: safeJsonStringify(item),
1668
+ });
1669
+ }
1670
+ const messages = [];
1671
+ if (toolParts.length > 0) {
1672
+ messages.push({
1673
+ role: 'tool',
1674
+ content: toolParts,
1675
+ });
1676
+ }
1677
+ if (textParts.length === 1) {
1678
+ const onlyText = textParts[0];
1679
+ if (onlyText) {
1680
+ messages.push({
1681
+ role: 'user',
1682
+ content: onlyText.text,
1683
+ });
1684
+ }
1685
+ }
1686
+ else if (textParts.length > 1) {
1687
+ messages.push({
1688
+ role: 'user',
1689
+ content: textParts,
1690
+ });
1691
+ }
1692
+ return messages;
1693
+ }
1694
+ function toModelMessages(turns) {
1695
+ const messages = [];
1696
+ for (const turn of turns) {
1697
+ if (turn.role === 'assistant') {
1698
+ messages.push({
1699
+ role: 'assistant',
1700
+ content: normalizeAssistantContent(turn.content),
1701
+ });
1702
+ continue;
1703
+ }
1704
+ messages.push(...normalizeUserMessages(turn.content));
1705
+ }
1706
+ return messages;
1707
+ }
1708
+ function toRunnerToolSet(catalog) {
1709
+ const tools = {};
1710
+ for (const item of catalog) {
1711
+ tools[item.name] = tool({
1712
+ description: item.description,
1713
+ inputSchema: jsonSchema(item.parameters ?? createDefaultObjectSchema()),
1714
+ });
1715
+ }
1716
+ return tools;
1717
+ }
1718
+ function toAssistantContent(messages, fallbackText) {
1719
+ const assistantMessage = messages.find((message) => message.role === 'assistant');
1720
+ if (!assistantMessage) {
1721
+ return fallbackText.trim().length > 0 ? [{ type: 'text', text: fallbackText }] : [];
1722
+ }
1723
+ if (typeof assistantMessage.content === 'string') {
1724
+ return [{ type: 'text', text: assistantMessage.content }];
1725
+ }
1726
+ return assistantMessage.content;
1727
+ }
1728
+ async function requestModelMessage(input) {
1729
+ const model = createLanguageModel(input.provider, input.model, input.apiKey);
1730
+ const result = await generateText({
1731
+ model,
1732
+ system: input.systemPrompt,
1733
+ messages: toModelMessages(input.turns),
1734
+ tools: toRunnerToolSet(input.toolCatalog),
1735
+ stopWhen: stepCountIs(1),
1736
+ temperature: input.temperature,
1737
+ maxOutputTokens: input.maxTokens,
1738
+ });
1739
+ const toolUseBlocks = [];
1740
+ for (const toolCall of result.toolCalls) {
1741
+ toolUseBlocks.push({
1742
+ id: toolCall.toolCallId,
1743
+ name: toolCall.toolName,
1744
+ input: ensureJsonObject(toolCall.input),
1745
+ });
1746
+ }
1747
+ const textBlocks = result.text.trim().length > 0 ? [result.text.trim()] : [];
1748
+ return {
1749
+ assistantContent: toAssistantContent(result.response.messages, result.text),
1750
+ textBlocks,
1751
+ toolUseBlocks,
1752
+ };
1753
+ }
1754
+ async function withOptionalTimeout(promise, timeoutMs, timeoutMessage) {
1755
+ if (timeoutMs === undefined || timeoutMs <= 0) {
1756
+ return promise;
1757
+ }
1758
+ let timer;
1759
+ try {
1760
+ return await Promise.race([
1761
+ promise,
1762
+ new Promise((_, reject) => {
1763
+ timer = setTimeout(() => {
1764
+ reject(new Error(timeoutMessage));
1765
+ }, timeoutMs);
1766
+ }),
1767
+ ]);
1768
+ }
1769
+ finally {
1770
+ if (timer) {
1771
+ clearTimeout(timer);
1772
+ }
1773
+ }
1774
+ }
1775
+ async function executeInboundTurn(input) {
1776
+ const agentPlan = input.runnerPlan.agents.get(input.targetAgentName);
1777
+ if (!agentPlan) {
1778
+ throw new Error(`target agent not found: ${input.targetAgentName}`);
1779
+ }
1780
+ const queueKey = createConversationKey(input.targetAgentName, input.event.instanceKey);
1781
+ const userInputText = formatRuntimeInboundUserText({
1782
+ sourceKind: input.event.sourceKind,
1783
+ sourceName: input.event.sourceName,
1784
+ eventName: input.event.eventName,
1785
+ instanceKey: input.event.instanceKey,
1786
+ messageText: input.event.messageText,
1787
+ properties: input.event.properties,
1788
+ metadata: input.event.metadata,
1789
+ });
1790
+ let turnResult;
1791
+ await queueAgentEvent(input.runtime, queueKey, async () => {
1792
+ await ensureInstanceStorage(input.runtime, queueKey, input.targetAgentName);
1793
+ await input.runtime.storage.updateMetadataStatus(queueKey, 'processing');
1794
+ try {
1795
+ const turnId = createId('turn');
1796
+ const traceId = createId('trace');
1797
+ const history = await loadConversationFromStorage(input.runtime, queueKey, input.targetAgentName);
1798
+ try {
1799
+ const inputAgentEvent = {
1800
+ id: createId('inbound_event'),
1801
+ type: input.event.eventName,
1802
+ createdAt: new Date(),
1803
+ traceId,
1804
+ source: {
1805
+ kind: input.event.sourceKind,
1806
+ name: input.event.sourceName,
1807
+ },
1808
+ input: input.event.messageText,
1809
+ instanceKey: input.event.instanceKey,
1810
+ metadata: input.event.metadata,
1811
+ };
1812
+ turnResult = await runAgentTurn({
1813
+ plan: agentPlan,
1814
+ inputEvent: inputAgentEvent,
1815
+ userInputText,
1816
+ instanceKey: input.event.instanceKey,
1817
+ turnId,
1818
+ traceId,
1819
+ conversation: history,
1820
+ toolExecutor: input.runnerPlan.toolExecutor,
1821
+ runtime: input.runtime,
1822
+ runnerPlan: input.runnerPlan,
1823
+ workdir: resolveAgentWorkdir(input.runtime, queueKey),
1824
+ logger: input.logger,
1825
+ });
1826
+ }
1827
+ catch (error) {
1828
+ const message = unknownToErrorMessage(error);
1829
+ const responseText = `오류: ${message}`;
1830
+ turnResult = {
1831
+ responseText,
1832
+ restartRequested: false,
1833
+ nextConversation: history.concat([
1834
+ {
1835
+ role: 'user',
1836
+ content: userInputText,
1837
+ },
1838
+ {
1839
+ role: 'assistant',
1840
+ content: responseText,
1841
+ },
1842
+ ]),
1843
+ };
1844
+ }
1845
+ await persistConversationToStorage(input.runtime, queueKey, input.targetAgentName, turnResult.nextConversation);
1846
+ if (turnResult.restartRequested) {
1847
+ const reason = turnResult.restartReason ?? 'tool:restart-signal';
1848
+ await requestRuntimeRestart(input.runtime, reason);
1849
+ }
1850
+ }
1851
+ finally {
1852
+ await input.runtime.storage.updateMetadataStatus(queueKey, 'idle');
1853
+ }
1854
+ });
1855
+ if (!turnResult) {
1856
+ throw new Error('turn result was not produced');
1857
+ }
1858
+ return turnResult;
1859
+ }
1860
+ function createAgentToolRuntime(input) {
1861
+ const resolveTargetPlan = (target) => {
1862
+ const targetPlan = input.runnerPlan.agents.get(target);
1863
+ if (!targetPlan) {
1864
+ throw new Error(`target agent not found in selected swarm: ${target}`);
1865
+ }
1866
+ return targetPlan;
1867
+ };
1868
+ return {
1869
+ request: async (target, event, options) => {
1870
+ resolveTargetPlan(target);
1871
+ const parsed = parseAgentToolEventPayload(event, input.callerInstanceKey, input.callerAgentName);
1872
+ if (!parsed) {
1873
+ throw new Error('agents__request 입력 이벤트 형식이 올바르지 않습니다.');
1874
+ }
1875
+ const inboundEvent = {
1876
+ sourceKind: 'agent',
1877
+ sourceName: parsed.sourceName,
1878
+ eventName: parsed.type,
1879
+ instanceKey: parsed.instanceKey,
1880
+ messageText: parsed.messageText,
1881
+ properties: {
1882
+ from_agent: input.callerAgentName,
1883
+ from_instance: input.callerInstanceKey,
1884
+ },
1885
+ metadata: parsed.metadata,
1886
+ };
1887
+ if (target === input.callerAgentName && inboundEvent.instanceKey === input.callerInstanceKey) {
1888
+ throw new Error('agents__request는 동일 agent+instance를 대상으로 호출할 수 없습니다.');
1889
+ }
1890
+ const timeoutMs = options?.timeoutMs;
1891
+ const turnResult = await withOptionalTimeout(executeInboundTurn({
1892
+ runtime: input.runtime,
1893
+ runnerPlan: input.runnerPlan,
1894
+ targetAgentName: target,
1895
+ event: inboundEvent,
1896
+ logger: input.logger,
1897
+ }), timeoutMs, `agent request timeout (${timeoutMs}ms): target=${target}`);
1898
+ return {
1899
+ eventId: parsed.id,
1900
+ target,
1901
+ response: turnResult.responseText,
1902
+ correlationId: parsed.correlationId ?? createId('corr'),
1903
+ };
1904
+ },
1905
+ send: async (target, event) => {
1906
+ resolveTargetPlan(target);
1907
+ const parsed = parseAgentToolEventPayload(event, input.callerInstanceKey, input.callerAgentName);
1908
+ if (!parsed) {
1909
+ throw new Error('agents__send 입력 이벤트 형식이 올바르지 않습니다.');
1910
+ }
1911
+ const inboundEvent = {
1912
+ sourceKind: 'agent',
1913
+ sourceName: parsed.sourceName,
1914
+ eventName: parsed.type,
1915
+ instanceKey: parsed.instanceKey,
1916
+ messageText: parsed.messageText,
1917
+ properties: {
1918
+ from_agent: input.callerAgentName,
1919
+ from_instance: input.callerInstanceKey,
1920
+ },
1921
+ metadata: parsed.metadata,
1922
+ };
1923
+ void executeInboundTurn({
1924
+ runtime: input.runtime,
1925
+ runnerPlan: input.runnerPlan,
1926
+ targetAgentName: target,
1927
+ event: inboundEvent,
1928
+ logger: input.logger,
1929
+ }).catch((error) => {
1930
+ input.logger.warn(`agents__send target=${target} failed: ${unknownToErrorMessage(error)}`);
1931
+ });
1932
+ return {
1933
+ eventId: parsed.id,
1934
+ target,
1935
+ accepted: true,
1936
+ };
1937
+ },
1938
+ spawn: async (target, options) => {
1939
+ resolveTargetPlan(target);
1940
+ const instanceKey = options?.instanceKey && options.instanceKey.trim().length > 0
1941
+ ? options.instanceKey.trim()
1942
+ : createId(`${target}-instance`);
1943
+ const queueKey = createConversationKey(target, instanceKey);
1944
+ await ensureInstanceStorage(input.runtime, queueKey, target);
1945
+ await input.runtime.storage.updateMetadataStatus(queueKey, 'idle');
1946
+ const ownerKey = createSpawnOwnerKey(input.callerAgentName, input.callerInstanceKey);
1947
+ const registry = input.runtime.spawnedAgentsByOwner.get(ownerKey);
1948
+ const existing = registry?.get(queueKey);
1949
+ const resolvedCwd = options?.cwd ? resolveRuntimeWorkdir(input.runtime.workdir, options.cwd) : existing?.cwd;
1950
+ if (resolvedCwd) {
1951
+ input.runtime.instanceWorkdirs.set(queueKey, resolvedCwd);
1952
+ }
1953
+ const createdAt = existing?.createdAt ?? new Date().toISOString();
1954
+ const record = {
1955
+ target,
1956
+ instanceKey,
1957
+ ownerAgent: input.callerAgentName,
1958
+ ownerInstanceKey: input.callerInstanceKey,
1959
+ createdAt,
1960
+ cwd: resolvedCwd,
1961
+ };
1962
+ trackSpawnedAgent(input.runtime, input.callerAgentName, input.callerInstanceKey, record);
1963
+ return {
1964
+ target,
1965
+ instanceKey,
1966
+ spawned: existing === undefined,
1967
+ cwd: resolvedCwd,
1968
+ };
1969
+ },
1970
+ list: async (options) => {
1971
+ const includeAll = options?.includeAll === true;
1972
+ const records = listSpawnedAgents(input.runtime, input.callerAgentName, input.callerInstanceKey, includeAll);
1973
+ return {
1974
+ agents: records.map((record) => ({
1975
+ target: record.target,
1976
+ instanceKey: record.instanceKey,
1977
+ ownerAgent: record.ownerAgent,
1978
+ ownerInstanceKey: record.ownerInstanceKey,
1979
+ createdAt: record.createdAt,
1980
+ cwd: record.cwd,
1981
+ })),
1982
+ };
1983
+ },
1984
+ catalog: async () => {
1985
+ const selfAgent = input.callerAgentName;
1986
+ return {
1987
+ ...buildRuntimeAgentCatalog(input.runnerPlan, selfAgent),
1988
+ };
1989
+ },
1990
+ };
1991
+ }
1992
+ async function getOrCreateAgentExtensionEnvironment(input) {
1993
+ const queueKey = createConversationKey(input.agentName, input.instanceKey);
1994
+ const existing = input.runtime.extensionEnvironments.get(queueKey);
1995
+ if (existing) {
1996
+ return existing;
1997
+ }
1998
+ const extensionNames = input.plan.extensionResources.map((resource) => resource.metadata.name);
1999
+ const extensionStateManager = new ExtensionStateManagerImpl(input.runtime.storage, queueKey, extensionNames);
2000
+ await extensionStateManager.loadAll();
2001
+ const extensionToolRegistry = new ToolRegistryImpl();
2002
+ const environment = {
2003
+ pipelineRegistry: new PipelineRegistryImpl(),
2004
+ extensionToolRegistry,
2005
+ extensionToolExecutor: new ToolExecutor(extensionToolRegistry),
2006
+ extensionStateManager,
2007
+ initialized: false,
2008
+ };
2009
+ if (input.plan.extensionResources.length > 0) {
2010
+ const extensionEventBus = new EventEmitter();
2011
+ await loadExtensions(input.plan.extensionResources, (extensionName) => new ExtensionApiImpl(extensionName, environment.pipelineRegistry, environment.extensionToolRegistry, environment.extensionStateManager, extensionEventBus, input.logger), input.runtime.workdir, input.logger);
2012
+ }
2013
+ environment.initialized = true;
2014
+ input.runtime.extensionEnvironments.set(queueKey, environment);
2015
+ return environment;
2016
+ }
2017
+ async function runAgentTurn(input) {
2018
+ const conversationState = createConversationStateFromTurns(input.conversation);
2019
+ conversationState.emitMessageEvent({
2020
+ type: 'append',
2021
+ message: createConversationUserMessage(input.userInputText),
2022
+ });
2023
+ let lastText = '';
2024
+ let finalResponseText = '응답 텍스트를 생성하지 못했습니다.';
2025
+ let restartRequested = false;
2026
+ let restartReason;
2027
+ const calledToolNames = new Set();
2028
+ const requiredToolNames = input.plan.requiredToolNames;
2029
+ const enforceRequiredTools = requiredToolNames.length > 0;
2030
+ const agentRuntime = createAgentToolRuntime({
2031
+ runtime: input.runtime,
2032
+ runnerPlan: input.runnerPlan,
2033
+ callerAgentName: input.plan.name,
2034
+ callerInstanceKey: input.instanceKey,
2035
+ logger: input.logger,
2036
+ });
2037
+ const extensionEnvironment = await getOrCreateAgentExtensionEnvironment({
2038
+ runtime: input.runtime,
2039
+ plan: input.plan,
2040
+ agentName: input.plan.name,
2041
+ instanceKey: input.instanceKey,
2042
+ logger: input.logger,
2043
+ });
2044
+ const turnMetadata = {
2045
+ runtimeCatalog: buildRuntimeAgentCatalog(input.runnerPlan, input.plan.name),
2046
+ };
2047
+ let step = 0;
2048
+ let turnResult;
2049
+ try {
2050
+ turnResult = await extensionEnvironment.pipelineRegistry.runTurn({
2051
+ agentName: input.plan.name,
2052
+ instanceKey: input.instanceKey,
2053
+ turnId: input.turnId,
2054
+ traceId: input.traceId,
2055
+ inputEvent: input.inputEvent,
2056
+ conversationState,
2057
+ emitMessageEvent(event) {
2058
+ conversationState.emitMessageEvent(event);
2059
+ },
2060
+ metadata: turnMetadata,
2061
+ }, async () => {
2062
+ while (true) {
2063
+ if (step >= input.plan.maxSteps) {
2064
+ const responseText = buildStepLimitResponse({
2065
+ maxSteps: input.plan.maxSteps,
2066
+ requiredToolNames,
2067
+ calledToolNames,
2068
+ lastText,
2069
+ });
2070
+ if (responseText !== lastText) {
2071
+ conversationState.emitMessageEvent({
2072
+ type: 'append',
2073
+ message: createConversationAssistantMessage(responseText, `${input.turnId}-step-limit`),
2074
+ });
2075
+ }
2076
+ finalResponseText = responseText;
2077
+ return {
2078
+ turnId: input.turnId,
2079
+ finishReason: 'max_steps',
2080
+ responseMessage: createConversationAssistantMessage(responseText, `${input.turnId}-step-limit`),
2081
+ };
2082
+ }
2083
+ step += 1;
2084
+ const baseToolCatalog = mergeToolCatalog(input.plan.toolCatalog, extensionEnvironment.extensionToolRegistry.getCatalog());
2085
+ const stepMetadata = {};
2086
+ const turnSnapshot = {
2087
+ id: input.turnId,
2088
+ agentName: input.plan.name,
2089
+ inputEvent: input.inputEvent,
2090
+ messages: conversationState.nextMessages,
2091
+ steps: [],
2092
+ status: 'running',
2093
+ metadata: {},
2094
+ };
2095
+ const stepResult = await extensionEnvironment.pipelineRegistry.runStep({
2096
+ agentName: input.plan.name,
2097
+ instanceKey: input.instanceKey,
2098
+ turnId: input.turnId,
2099
+ traceId: input.traceId,
2100
+ turn: turnSnapshot,
2101
+ stepIndex: step,
2102
+ conversationState,
2103
+ emitMessageEvent(event) {
2104
+ conversationState.emitMessageEvent(event);
2105
+ },
2106
+ toolCatalog: baseToolCatalog,
2107
+ metadata: stepMetadata,
2108
+ }, async (stepCtx) => {
2109
+ const response = await requestModelMessage({
2110
+ provider: input.plan.provider,
2111
+ apiKey: input.plan.apiKey,
2112
+ model: input.plan.modelName,
2113
+ systemPrompt: input.plan.systemPrompt,
2114
+ temperature: input.plan.temperature,
2115
+ maxTokens: input.plan.maxTokens,
2116
+ toolCatalog: stepCtx.toolCatalog,
2117
+ turns: toConversationTurns(conversationState.nextMessages),
2118
+ });
2119
+ conversationState.emitMessageEvent({
2120
+ type: 'append',
2121
+ message: createConversationAssistantMessage(response.assistantContent, `${input.turnId}-step-${step}`),
2122
+ });
2123
+ if (response.textBlocks.length > 0) {
2124
+ lastText = response.textBlocks.join('\n').trim();
2125
+ }
2126
+ if (response.toolUseBlocks.length === 0) {
2127
+ return {
2128
+ status: 'completed',
2129
+ hasToolCalls: false,
2130
+ toolCalls: [],
2131
+ toolResults: [],
2132
+ metadata: {},
2133
+ };
2134
+ }
2135
+ const toolCalls = [];
2136
+ const toolResults = [];
2137
+ const toolResultBlocks = [];
2138
+ for (const toolUse of response.toolUseBlocks) {
2139
+ const toolArgs = ensureJsonObject(toolUse.input);
2140
+ toolCalls.push({
2141
+ id: toolUse.id,
2142
+ name: toolUse.name,
2143
+ args: toolArgs,
2144
+ });
2145
+ const toolResult = await extensionEnvironment.pipelineRegistry.runToolCall({
2146
+ agentName: input.plan.name,
2147
+ instanceKey: input.instanceKey,
2148
+ turnId: input.turnId,
2149
+ traceId: input.traceId,
2150
+ stepIndex: step,
2151
+ toolName: toolUse.name,
2152
+ toolCallId: toolUse.id,
2153
+ args: toolArgs,
2154
+ metadata: {},
2155
+ }, async (toolCallCtx) => {
2156
+ const toolContext = createMinimalToolContext({
2157
+ agentName: input.plan.name,
2158
+ instanceKey: input.instanceKey,
2159
+ turnId: input.turnId,
2160
+ traceId: input.traceId,
2161
+ toolCallId: toolCallCtx.toolCallId,
2162
+ message: createToolContextMessage(input.userInputText),
2163
+ workdir: input.workdir,
2164
+ logger: input.logger,
2165
+ runtime: agentRuntime,
2166
+ });
2167
+ const executor = extensionEnvironment.extensionToolRegistry.has(toolCallCtx.toolName)
2168
+ ? extensionEnvironment.extensionToolExecutor
2169
+ : input.toolExecutor;
2170
+ return executor.execute({
2171
+ toolCallId: toolCallCtx.toolCallId,
2172
+ toolName: toolCallCtx.toolName,
2173
+ args: toolCallCtx.args,
2174
+ catalog: stepCtx.toolCatalog,
2175
+ context: toolContext,
2176
+ });
2177
+ });
2178
+ toolResults.push(toolResult);
2179
+ if (toolResult.status === 'ok') {
2180
+ calledToolNames.add(toolUse.name);
2181
+ const restartSignal = readRuntimeRestartSignal(toolResult.output);
2182
+ if (restartSignal?.requested) {
2183
+ restartRequested = true;
2184
+ if (!restartReason && restartSignal.reason) {
2185
+ restartReason = restartSignal.reason;
2186
+ }
2187
+ }
2188
+ toolResultBlocks.push({
2189
+ type: 'tool-result',
2190
+ toolCallId: toolUse.id,
2191
+ toolName: toolUse.name,
2192
+ output: toToolResultOutput(toolResult.output),
2193
+ });
2194
+ continue;
2195
+ }
2196
+ const errorMessage = toolResult.error?.message ?? 'unknown tool error';
2197
+ toolResultBlocks.push({
2198
+ type: 'tool-result',
2199
+ toolCallId: toolUse.id,
2200
+ toolName: toolUse.name,
2201
+ output: {
2202
+ type: 'text',
2203
+ value: `ERROR: ${errorMessage}`,
2204
+ },
2205
+ });
2206
+ }
2207
+ conversationState.emitMessageEvent({
2208
+ type: 'append',
2209
+ message: createConversationUserMessage(toolResultBlocks),
2210
+ });
2211
+ return {
2212
+ status: 'completed',
2213
+ hasToolCalls: true,
2214
+ toolCalls,
2215
+ toolResults,
2216
+ metadata: {},
2217
+ };
2218
+ });
2219
+ if (stepResult.hasToolCalls) {
2220
+ continue;
2221
+ }
2222
+ if (enforceRequiredTools) {
2223
+ const missingRequiredTools = requiredToolNames.filter((name) => !calledToolNames.has(name));
2224
+ if (missingRequiredTools.length > 0) {
2225
+ const enforcementMessage = [
2226
+ '필수 도구 호출 규칙 위반: 최종 답변 전에 아래 도구 중 최소 하나를 반드시 호출해야 합니다.',
2227
+ ...missingRequiredTools.map((name) => `- ${name}`),
2228
+ '텍스트 답변을 종료하지 말고, 지금 즉시 위 도구 중 하나를 tool call로 호출하세요.',
2229
+ ].join('\n');
2230
+ conversationState.emitMessageEvent({
2231
+ type: 'append',
2232
+ message: createConversationUserMessage(enforcementMessage),
2233
+ });
2234
+ continue;
2235
+ }
2236
+ }
2237
+ const responseText = lastText.length > 0 ? lastText : '응답 텍스트를 생성하지 못했습니다.';
2238
+ if (lastText.length === 0) {
2239
+ conversationState.emitMessageEvent({
2240
+ type: 'append',
2241
+ message: createConversationAssistantMessage(responseText, `${input.turnId}-final`),
2242
+ });
2243
+ }
2244
+ finalResponseText = responseText;
2245
+ return {
2246
+ turnId: input.turnId,
2247
+ finishReason: 'text_response',
2248
+ responseMessage: createConversationAssistantMessage(responseText, `${input.turnId}-final`),
2249
+ };
2250
+ }
2251
+ });
2252
+ }
2253
+ finally {
2254
+ await extensionEnvironment.extensionStateManager.saveAll();
2255
+ }
2256
+ if (turnResult.responseMessage) {
2257
+ const responseContent = turnResult.responseMessage.data.content;
2258
+ if (typeof responseContent === 'string' && responseContent.trim().length > 0) {
2259
+ finalResponseText = responseContent.trim();
2260
+ }
2261
+ }
2262
+ else if (turnResult.finishReason === 'error' && turnResult.error?.message) {
2263
+ finalResponseText = `오류: ${turnResult.error.message}`;
2264
+ }
2265
+ return {
2266
+ responseText: finalResponseText,
2267
+ restartRequested,
2268
+ restartReason,
2269
+ nextConversation: toConversationTurns(conversationState.nextMessages),
2270
+ };
2271
+ }
2272
+ function logConnectorEvent(plan, event) {
2273
+ console.info(`[goondan-runtime][${plan.connectionName}/${plan.connectorName}] emitted event name=${event.name} instanceKey=${event.instanceKey}`);
2274
+ }
2275
+ function buildRuntimeEngine(args, plan) {
2276
+ const workspacePaths = new WorkspacePaths({
2277
+ stateRoot: args.stateRoot,
2278
+ projectRoot: path.dirname(args.bundlePath),
2279
+ packageName: plan.localPackageName,
2280
+ });
2281
+ return {
2282
+ executionQueue: new Map(),
2283
+ initializedInstances: new Set(),
2284
+ storage: new FileWorkspaceStorage(workspacePaths),
2285
+ workdir: path.dirname(args.bundlePath),
2286
+ runnerArgs: args,
2287
+ spawnedAgentsByOwner: new Map(),
2288
+ instanceWorkdirs: new Map(),
2289
+ extensionEnvironments: new Map(),
2290
+ };
2291
+ }
2292
+ async function requestRuntimeRestart(runtime, reason) {
2293
+ if (runtime.restartRequestedReason !== undefined) {
2294
+ return;
2295
+ }
2296
+ runtime.restartRequestedReason = reason;
2297
+ console.info(`[goondan-runtime] restart requested reason=${reason}`);
2298
+ try {
2299
+ process.kill(process.pid, 'SIGTERM');
2300
+ }
2301
+ catch (error) {
2302
+ runtime.restartRequestedReason = undefined;
2303
+ throw new Error(`restart 신호 전송 실패: ${unknownToErrorMessage(error)}`);
2304
+ }
2305
+ }
2306
+ async function handleConnectorEvent(runtime, runnerPlan, connectorPlan, rawEvent) {
2307
+ const event = parseConnectorEventPayload(rawEvent);
2308
+ if (!event) {
2309
+ console.warn(`[goondan-runtime][${connectorPlan.connectionName}/${connectorPlan.connectorName}] invalid event payload received from connector.`);
2310
+ return;
2311
+ }
2312
+ logConnectorEvent(connectorPlan, event);
2313
+ const routingRules = connectorPlan.routeRules.map((rule) => ({
2314
+ eventName: rule.eventName,
2315
+ properties: rule.properties,
2316
+ agentName: rule.agent?.name,
2317
+ instanceKey: rule.instanceKey,
2318
+ instanceKeyProperty: rule.instanceKeyProperty,
2319
+ instanceKeyPrefix: rule.instanceKeyPrefix,
2320
+ }));
2321
+ const matchedRule = selectMatchingIngressRule(routingRules, event);
2322
+ const targetAgentName = matchedRule?.agentName ?? connectorPlan.defaultAgent.name;
2323
+ const logger = createConnectorLogger(connectorPlan);
2324
+ const routedInstanceKey = resolveInboundInstanceKey(matchedRule, event);
2325
+ const inboundEvent = {
2326
+ sourceKind: 'connector',
2327
+ sourceName: connectorPlan.connectorName,
2328
+ eventName: event.name,
2329
+ instanceKey: routedInstanceKey,
2330
+ messageText: event.messageText,
2331
+ properties: {
2332
+ ...event.properties,
2333
+ connection_name: connectorPlan.connectionName,
2334
+ connector_name: connectorPlan.connectorName,
2335
+ },
2336
+ };
2337
+ try {
2338
+ await executeInboundTurn({
2339
+ runtime,
2340
+ runnerPlan,
2341
+ targetAgentName,
2342
+ event: inboundEvent,
2343
+ logger,
2344
+ });
2345
+ }
2346
+ catch (error) {
2347
+ console.warn(`[goondan-runtime][${connectorPlan.connectionName}/${connectorPlan.connectorName}] event turn failed: ${unknownToErrorMessage(error)}`);
2348
+ return;
2349
+ }
2350
+ }
2351
+ function connectorChildRunnerPath() {
2352
+ const jsPath = fileURLToPath(new URL('./runtime-runner-connector-child.js', import.meta.url));
2353
+ if (existsSync(jsPath)) {
2354
+ return jsPath;
2355
+ }
2356
+ return fileURLToPath(new URL('./runtime-runner-connector-child.ts', import.meta.url));
2357
+ }
2358
+ function sendConnectorChildCommand(child, message) {
2359
+ if (!child.connected || typeof child.send !== 'function') {
2360
+ throw new Error('Connector child IPC 채널이 연결되어 있지 않습니다.');
2361
+ }
2362
+ child.send(message);
2363
+ }
2364
+ async function startConnector(plan, runnerPlan, runtime) {
2365
+ const logger = createConnectorLogger(plan);
2366
+ const child = fork(connectorChildRunnerPath(), [], {
2367
+ cwd: path.dirname(plan.connectorEntryPath),
2368
+ env: process.env,
2369
+ stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
2370
+ });
2371
+ if (!child.pid || child.pid <= 0) {
2372
+ throw new Error(`Connector/${plan.connectorName} child process를 시작하지 못했습니다.`);
2373
+ }
2374
+ let startupSettled = false;
2375
+ let exitSettled = false;
2376
+ let expectedTermination = false;
2377
+ let terminatePromise;
2378
+ let resolveStartup;
2379
+ let rejectStartup;
2380
+ const startupPromise = new Promise((resolve, reject) => {
2381
+ resolveStartup = resolve;
2382
+ rejectStartup = reject;
2383
+ });
2384
+ let resolveExit;
2385
+ let rejectExit;
2386
+ const exitPromise = new Promise((resolve, reject) => {
2387
+ resolveExit = resolve;
2388
+ rejectExit = reject;
2389
+ });
2390
+ const startupTimeout = setTimeout(() => {
2391
+ if (startupSettled) {
2392
+ return;
2393
+ }
2394
+ startupSettled = true;
2395
+ rejectStartup?.(new Error(`Connector/${plan.connectorName} 시작 확인이 시간 내에 완료되지 않았습니다.`));
2396
+ }, CONNECTOR_CHILD_STARTUP_TIMEOUT_MS);
2397
+ const cleanup = () => {
2398
+ clearTimeout(startupTimeout);
2399
+ child.off('message', onMessage);
2400
+ child.off('error', onError);
2401
+ child.off('exit', onExit);
2402
+ };
2403
+ const settleStartupSuccess = () => {
2404
+ if (startupSettled) {
2405
+ return;
2406
+ }
2407
+ startupSettled = true;
2408
+ clearTimeout(startupTimeout);
2409
+ resolveStartup?.();
2410
+ };
2411
+ const settleStartupFailure = (error) => {
2412
+ if (startupSettled) {
2413
+ return;
2414
+ }
2415
+ startupSettled = true;
2416
+ clearTimeout(startupTimeout);
2417
+ rejectStartup?.(error);
2418
+ };
2419
+ const settleExitSuccess = () => {
2420
+ if (exitSettled) {
2421
+ return;
2422
+ }
2423
+ exitSettled = true;
2424
+ cleanup();
2425
+ resolveExit?.();
2426
+ };
2427
+ const settleExitFailure = (error) => {
2428
+ if (exitSettled) {
2429
+ return;
2430
+ }
2431
+ exitSettled = true;
2432
+ cleanup();
2433
+ rejectExit?.(error);
2434
+ };
2435
+ const onMessage = (message) => {
2436
+ if (isConnectorChildEventMessage(message)) {
2437
+ void handleConnectorEvent(runtime, runnerPlan, plan, message.event).catch((error) => {
2438
+ logger.warn(`event handling failed: ${unknownToErrorMessage(error)}`);
2439
+ });
2440
+ return;
2441
+ }
2442
+ if (isConnectorChildStartedMessage(message)) {
2443
+ settleStartupSuccess();
2444
+ return;
2445
+ }
2446
+ if (isConnectorChildStartErrorMessage(message)) {
2447
+ settleStartupFailure(new Error(`Connector/${plan.connectorName} 시작 실패: ${message.message}`));
2448
+ }
2449
+ };
2450
+ const onError = (error) => {
2451
+ const wrapped = new Error(`Connector/${plan.connectorName} child process 오류: ${error.message}`);
2452
+ settleStartupFailure(wrapped);
2453
+ if (expectedTermination) {
2454
+ settleExitSuccess();
2455
+ return;
2456
+ }
2457
+ settleExitFailure(wrapped);
2458
+ };
2459
+ const onExit = (code, signal) => {
2460
+ const cause = code !== null ? `exit code ${code}` : signal ? `signal ${signal}` : 'unknown reason';
2461
+ if (!startupSettled) {
2462
+ settleStartupFailure(new Error(`Connector/${plan.connectorName} 초기화 중 종료되었습니다 (${cause}).`));
2463
+ }
2464
+ if (expectedTermination) {
2465
+ settleExitSuccess();
2466
+ return;
2467
+ }
2468
+ if (code === 0) {
2469
+ settleExitFailure(new Error(`Connector/${plan.connectorName} (connection=${plan.connectionName})가 예기치 않게 종료되었습니다.`));
2470
+ return;
2471
+ }
2472
+ settleExitFailure(new Error(`Connector/${plan.connectorName} (connection=${plan.connectionName}) 실패: ${cause}`));
2473
+ };
2474
+ child.on('message', onMessage);
2475
+ child.on('error', onError);
2476
+ child.on('exit', onExit);
2477
+ const terminate = async () => {
2478
+ if (terminatePromise) {
2479
+ return terminatePromise;
2480
+ }
2481
+ terminatePromise = (async () => {
2482
+ expectedTermination = true;
2483
+ const shutdownMessage = {
2484
+ type: 'connector_shutdown',
2485
+ };
2486
+ try {
2487
+ sendConnectorChildCommand(child, shutdownMessage);
2488
+ }
2489
+ catch {
2490
+ // IPC 채널이 이미 닫힌 경우는 무시한다.
2491
+ }
2492
+ if (child.pid && child.pid > 0) {
2493
+ try {
2494
+ child.kill('SIGTERM');
2495
+ }
2496
+ catch {
2497
+ // 이미 종료된 경우 무시한다.
2498
+ }
2499
+ }
2500
+ await Promise.race([
2501
+ exitPromise.catch(() => {
2502
+ // 종료 과정의 에러는 종료 루틴에서 무시한다.
2503
+ }),
2504
+ new Promise((resolve) => {
2505
+ setTimeout(resolve, CONNECTOR_CHILD_SHUTDOWN_TIMEOUT_MS);
2506
+ }),
2507
+ ]);
2508
+ if (!exitSettled && child.pid && child.pid > 0) {
2509
+ try {
2510
+ child.kill('SIGKILL');
2511
+ }
2512
+ catch {
2513
+ // 이미 종료된 경우 무시한다.
2514
+ }
2515
+ await exitPromise.catch(() => {
2516
+ // 종료 과정의 에러는 종료 루틴에서 무시한다.
2517
+ });
2518
+ }
2519
+ })();
2520
+ return terminatePromise;
2521
+ };
2522
+ const startMessage = {
2523
+ type: 'connector_start',
2524
+ connectorEntryPath: plan.connectorEntryPath,
2525
+ connectionName: plan.connectionName,
2526
+ connectorName: plan.connectorName,
2527
+ config: plan.config,
2528
+ secrets: plan.secrets,
2529
+ };
2530
+ try {
2531
+ sendConnectorChildCommand(child, startMessage);
2532
+ }
2533
+ catch (error) {
2534
+ await terminate();
2535
+ throw new Error(`Connector/${plan.connectorName} 시작 요청을 전달하지 못했습니다: ${unknownToErrorMessage(error)}`);
2536
+ }
2537
+ await startupPromise.catch(async (error) => {
2538
+ await terminate();
2539
+ throw error;
2540
+ });
2541
+ return {
2542
+ connectionName: plan.connectionName,
2543
+ connectorName: plan.connectorName,
2544
+ wait: exitPromise,
2545
+ terminate,
2546
+ };
2547
+ }
2548
+ async function startConnectors(plan, runtime) {
2549
+ const running = [];
2550
+ try {
2551
+ for (const connectorPlan of plan.connectors) {
2552
+ const started = await startConnector(connectorPlan, plan, runtime);
2553
+ running.push(started);
2554
+ }
2555
+ }
2556
+ catch (error) {
2557
+ await stopConnectors(running);
2558
+ throw error;
2559
+ }
2560
+ return running;
2561
+ }
2562
+ function monitorConnectors(connectors) {
2563
+ if (connectors.length === 0) {
2564
+ return new Promise(() => {
2565
+ // no-op: 연결된 connector가 없으면 shutdown signal을 기다린다.
2566
+ });
2567
+ }
2568
+ return Promise.race(connectors.map((connector) => connector.wait));
2569
+ }
2570
+ async function stopConnectors(connectors) {
2571
+ await Promise.all(connectors.map(async (connector) => {
2572
+ try {
2573
+ await connector.terminate();
2574
+ }
2575
+ catch (error) {
2576
+ console.warn(`[goondan-runtime] Connector/${connector.connectorName} (connection=${connector.connectionName}) 종료 실패: ${unknownToErrorMessage(error)}`);
2577
+ }
2578
+ }));
2579
+ }
2580
+ function waitForShutdownSignal() {
2581
+ return new Promise((resolve) => {
2582
+ const keepAlive = setInterval(() => {
2583
+ // keep orchestrator event loop alive while waiting for shutdown signal
2584
+ }, 60_000);
2585
+ const shutdown = () => {
2586
+ clearInterval(keepAlive);
2587
+ process.off('SIGINT', shutdown);
2588
+ process.off('SIGTERM', shutdown);
2589
+ resolve();
2590
+ };
2591
+ process.once('SIGINT', shutdown);
2592
+ process.once('SIGTERM', shutdown);
2593
+ });
2594
+ }
2595
+ function startWatchMode(runtime, plan) {
2596
+ const watchers = [];
2597
+ let restartInProgress = false;
2598
+ const triggerRestart = (reason) => {
2599
+ if (restartInProgress) {
2600
+ return;
2601
+ }
2602
+ restartInProgress = true;
2603
+ void requestRuntimeRestart(runtime, reason).finally(() => {
2604
+ restartInProgress = false;
2605
+ });
2606
+ };
2607
+ const uniqueTargets = [...new Set(plan.watchTargets)].sort((left, right) => left.localeCompare(right));
2608
+ for (const target of uniqueTargets) {
2609
+ try {
2610
+ const watcher = watchFs(target, (eventType, fileName) => {
2611
+ const changedPath = typeof fileName === 'string' && fileName.length > 0
2612
+ ? path.resolve(path.dirname(target), fileName)
2613
+ : target;
2614
+ console.info(`[goondan-runtime] watch change detected type=${eventType} path=${changedPath}`);
2615
+ triggerRestart(`watch:${changedPath}`);
2616
+ });
2617
+ watcher.on('error', (error) => {
2618
+ console.warn(`[goondan-runtime] watch error path=${target}: ${unknownToErrorMessage(error)}`);
2619
+ });
2620
+ watchers.push(watcher);
2621
+ }
2622
+ catch (error) {
2623
+ console.warn(`[goondan-runtime] watch registration skipped path=${target}: ${unknownToErrorMessage(error)}`);
2624
+ }
2625
+ }
2626
+ if (watchers.length > 0) {
2627
+ console.info(`[goondan-runtime] watch mode enabled targets=${watchers.length}`);
2628
+ }
2629
+ else {
2630
+ console.warn('[goondan-runtime] watch mode enabled but no files are currently watchable.');
2631
+ }
2632
+ return () => {
2633
+ watchers.forEach((watcher) => {
2634
+ watcher.close();
2635
+ });
2636
+ };
2637
+ }
2638
+ function summarizeConnectorPlans(connectors) {
2639
+ if (connectors.length === 0) {
2640
+ return 'none';
2641
+ }
2642
+ return connectors.map((plan) => `${plan.connectionName}:${plan.connectorName}`).join(', ');
2643
+ }
2644
+ function summarizeAgentPlans(agents) {
2645
+ const names = [...agents.keys()];
2646
+ if (names.length === 0) {
2647
+ return 'none';
2648
+ }
2649
+ return names.join(', ');
2650
+ }
2651
+ async function preloadAgentExtensions(runtime, plan, instanceKey) {
2652
+ for (const [agentName, agentPlan] of plan.agents.entries()) {
2653
+ if (agentPlan.extensionResources.length === 0) {
2654
+ continue;
2655
+ }
2656
+ await getOrCreateAgentExtensionEnvironment({
2657
+ runtime,
2658
+ plan: agentPlan,
2659
+ agentName,
2660
+ instanceKey,
2661
+ logger: console,
2662
+ });
2663
+ }
2664
+ }
2665
+ async function runLifecycle(runningConnectors) {
2666
+ const shutdownWait = waitForShutdownSignal();
2667
+ const connectorWait = monitorConnectors(runningConnectors);
2668
+ await Promise.race([shutdownWait, connectorWait]);
2669
+ }
2670
+ function unknownToErrorMessage(error) {
2671
+ if (error instanceof Error) {
2672
+ return error.message;
2673
+ }
2674
+ return String(error);
2675
+ }
2676
+ async function preflight(args) {
2677
+ const exists = await existsFile(args.bundlePath);
2678
+ if (!exists) {
2679
+ throw new Error(`Bundle 파일을 찾을 수 없습니다: ${args.bundlePath}`);
2680
+ }
2681
+ return await buildRunnerPlan(args);
2682
+ }
2683
+ async function main() {
2684
+ const args = parseRunnerArguments(process.argv.slice(2));
2685
+ const plan = await preflight(args);
2686
+ const runtime = buildRuntimeEngine(args, plan);
2687
+ await preloadAgentExtensions(runtime, plan, args.instanceKey);
2688
+ const stopWatching = args.watch ? startWatchMode(runtime, plan) : () => {
2689
+ // no-op
2690
+ };
2691
+ const runningConnectors = await startConnectors(plan, runtime);
2692
+ console.info(`[goondan-runtime] started instanceKey=${args.instanceKey} pid=${process.pid} swarm=${plan.selectedSwarm.name} connectors=${runningConnectors.length}`);
2693
+ console.info(`[goondan-runtime] active connectors: ${summarizeConnectorPlans(plan.connectors)}`);
2694
+ console.info(`[goondan-runtime] active agents: ${summarizeAgentPlans(plan.agents)}`);
2695
+ const readyMessage = {
2696
+ type: 'ready',
2697
+ instanceKey: args.instanceKey,
2698
+ pid: process.pid,
2699
+ };
2700
+ sendMessage(readyMessage);
2701
+ try {
2702
+ await runLifecycle(runningConnectors);
2703
+ }
2704
+ finally {
2705
+ stopWatching();
2706
+ await stopConnectors(runningConnectors);
2707
+ }
2708
+ if (runtime.restartRequestedReason !== undefined) {
2709
+ const runnerModulePath = process.argv[1];
2710
+ if (typeof runnerModulePath !== 'string' || runnerModulePath.trim().length === 0) {
2711
+ throw new Error('runtime-runner 모듈 경로를 확인할 수 없습니다.');
2712
+ }
2713
+ const reason = runtime.restartRequestedReason;
2714
+ const replacementPid = await spawnReplacementRunner({
2715
+ runnerModulePath,
2716
+ runnerArgs: process.argv.slice(2),
2717
+ stateRoot: runtime.runnerArgs.stateRoot,
2718
+ instanceKey: runtime.runnerArgs.instanceKey,
2719
+ bundlePath: runtime.runnerArgs.bundlePath,
2720
+ watch: runtime.runnerArgs.watch,
2721
+ swarmName: runtime.runnerArgs.swarmName,
2722
+ env: process.env,
2723
+ });
2724
+ console.info(`[goondan-runtime] replacement orchestrator started pid=${replacementPid} reason=${reason}`);
2725
+ }
2726
+ process.exit(0);
2727
+ }
2728
+ void main().catch((error) => {
2729
+ const message = unknownToErrorMessage(error);
2730
+ const startErrorMessage = {
2731
+ type: 'start_error',
2732
+ message,
2733
+ };
2734
+ sendMessage(startErrorMessage);
2735
+ console.error(`[goondan-runtime] startup failure: ${message}`);
2736
+ process.exit(1);
2737
+ });
2738
+ //# sourceMappingURL=runtime-runner.js.map