triflux 3.1.0-dev.2 → 3.1.0-dev.3
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.
- package/hub/bridge.mjs +224 -0
- package/hub/router.mjs +1 -1
- package/hub/server.mjs +81 -0
- package/hub/store.mjs +1 -1
- package/hub/tools.mjs +1 -1
- package/package.json +1 -1
- package/scripts/tfx-route.sh +61 -1
package/hub/bridge.mjs
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hub/bridge.mjs — tfx-route.sh ↔ tfx-hub 브릿지 CLI
|
|
3
|
+
//
|
|
4
|
+
// tfx-route.sh에서 CLI 에이전트 실행 전후로 호출하여
|
|
5
|
+
// Hub에 자동 등록/결과 발행/컨텍스트 수신/해제를 수행한다.
|
|
6
|
+
//
|
|
7
|
+
// 사용법:
|
|
8
|
+
// node bridge.mjs register --agent <id> --cli <type> --timeout <sec> [--topics t1,t2]
|
|
9
|
+
// node bridge.mjs result --agent <id> --file <path> [--topic task.result] [--trace <id>]
|
|
10
|
+
// node bridge.mjs context --agent <id> [--topics t1,t2] [--max 10] [--out <path>]
|
|
11
|
+
// node bridge.mjs deregister --agent <id>
|
|
12
|
+
// node bridge.mjs ping
|
|
13
|
+
//
|
|
14
|
+
// Hub 미실행 시 모든 커맨드는 조용히 실패 (exit 0).
|
|
15
|
+
// tfx-route.sh 흐름을 절대 차단하지 않는다.
|
|
16
|
+
|
|
17
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
|
|
21
|
+
const HUB_PID_FILE = join(homedir(), '.claude', 'cache', 'tfx-hub', 'hub.pid');
|
|
22
|
+
|
|
23
|
+
// ── Hub URL 해석 ──
|
|
24
|
+
|
|
25
|
+
function getHubUrl() {
|
|
26
|
+
// 환경변수 우선
|
|
27
|
+
if (process.env.TFX_HUB_URL) return process.env.TFX_HUB_URL.replace(/\/mcp$/, '');
|
|
28
|
+
|
|
29
|
+
// PID 파일에서 읽기
|
|
30
|
+
if (existsSync(HUB_PID_FILE)) {
|
|
31
|
+
try {
|
|
32
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
|
|
33
|
+
return `http://${info.host || '127.0.0.1'}:${info.port || 27888}`;
|
|
34
|
+
} catch { /* 무시 */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 기본값
|
|
38
|
+
const port = process.env.TFX_HUB_PORT || '27888';
|
|
39
|
+
return `http://127.0.0.1:${port}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── HTTP 요청 ──
|
|
43
|
+
|
|
44
|
+
async function post(path, body, timeoutMs = 5000) {
|
|
45
|
+
const url = `${getHubUrl()}${path}`;
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
});
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
return await res.json();
|
|
58
|
+
} catch {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
return null; // Hub 미실행 — 조용히 실패
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── 인자 파싱 ──
|
|
65
|
+
|
|
66
|
+
function parseArgs(argv) {
|
|
67
|
+
const args = {};
|
|
68
|
+
for (let i = 0; i < argv.length; i++) {
|
|
69
|
+
if (argv[i].startsWith('--')) {
|
|
70
|
+
const key = argv[i].slice(2);
|
|
71
|
+
const next = argv[i + 1];
|
|
72
|
+
if (!next || next.startsWith('--')) {
|
|
73
|
+
args[key] = true;
|
|
74
|
+
} else {
|
|
75
|
+
args[key] = next;
|
|
76
|
+
i++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return args;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── 커맨드 ──
|
|
84
|
+
|
|
85
|
+
async function cmdRegister(args) {
|
|
86
|
+
const agentId = args.agent;
|
|
87
|
+
const cli = args.cli || 'other';
|
|
88
|
+
const timeoutSec = parseInt(args.timeout || '600', 10);
|
|
89
|
+
const topics = args.topics ? args.topics.split(',') : [];
|
|
90
|
+
const capabilities = args.capabilities ? args.capabilities.split(',') : ['code'];
|
|
91
|
+
|
|
92
|
+
const result = await post('/bridge/register', {
|
|
93
|
+
agent_id: agentId,
|
|
94
|
+
cli,
|
|
95
|
+
timeout_sec: timeoutSec,
|
|
96
|
+
topics,
|
|
97
|
+
capabilities,
|
|
98
|
+
metadata: {
|
|
99
|
+
pid: process.ppid, // 부모 프로세스 (tfx-route.sh)
|
|
100
|
+
registered_at: Date.now(),
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (result?.ok) {
|
|
105
|
+
// 에이전트 ID를 stdout으로 출력 (tfx-route.sh에서 캡처)
|
|
106
|
+
console.log(JSON.stringify({ ok: true, agent_id: agentId, lease_expires_ms: result.data?.lease_expires_ms }));
|
|
107
|
+
} else {
|
|
108
|
+
// Hub 미실행 — 조용히 패스
|
|
109
|
+
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function cmdResult(args) {
|
|
114
|
+
const agentId = args.agent;
|
|
115
|
+
const filePath = args.file;
|
|
116
|
+
const topic = args.topic || 'task.result';
|
|
117
|
+
const traceId = args.trace || undefined;
|
|
118
|
+
const correlationId = args.correlation || undefined;
|
|
119
|
+
const exitCode = parseInt(args['exit-code'] || '0', 10);
|
|
120
|
+
|
|
121
|
+
// 결과 파일 읽기 (최대 48KB — Hub 메시지 크기 제한)
|
|
122
|
+
let output = '';
|
|
123
|
+
if (filePath && existsSync(filePath)) {
|
|
124
|
+
output = readFileSync(filePath, 'utf8').slice(0, 49152);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = await post('/bridge/result', {
|
|
128
|
+
agent_id: agentId,
|
|
129
|
+
topic,
|
|
130
|
+
payload: {
|
|
131
|
+
agent_id: agentId,
|
|
132
|
+
exit_code: exitCode,
|
|
133
|
+
output_length: output.length,
|
|
134
|
+
output_preview: output.slice(0, 4096), // 미리보기 4KB
|
|
135
|
+
output_full: output, // 전체 (최대 48KB)
|
|
136
|
+
completed_at: Date.now(),
|
|
137
|
+
},
|
|
138
|
+
trace_id: traceId,
|
|
139
|
+
correlation_id: correlationId,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (result?.ok) {
|
|
143
|
+
console.log(JSON.stringify({ ok: true, message_id: result.data?.message_id }));
|
|
144
|
+
} else {
|
|
145
|
+
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function cmdContext(args) {
|
|
150
|
+
const agentId = args.agent;
|
|
151
|
+
const topics = args.topics ? args.topics.split(',') : undefined;
|
|
152
|
+
const maxMessages = parseInt(args.max || '10', 10);
|
|
153
|
+
const outPath = args.out;
|
|
154
|
+
|
|
155
|
+
const result = await post('/bridge/context', {
|
|
156
|
+
agent_id: agentId,
|
|
157
|
+
topics,
|
|
158
|
+
max_messages: maxMessages,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (result?.ok && result.data?.messages?.length) {
|
|
162
|
+
// 컨텍스트 조합
|
|
163
|
+
const parts = result.data.messages.map((m, i) => {
|
|
164
|
+
const from = m.from_agent || 'unknown';
|
|
165
|
+
const topic = m.topic || 'unknown';
|
|
166
|
+
const payload = typeof m.payload === 'string' ? m.payload : JSON.stringify(m.payload, null, 2);
|
|
167
|
+
return `=== Context ${i + 1}: ${from} (${topic}) ===\n${payload}`;
|
|
168
|
+
});
|
|
169
|
+
const combined = parts.join('\n\n');
|
|
170
|
+
|
|
171
|
+
if (outPath) {
|
|
172
|
+
writeFileSync(outPath, combined, 'utf8');
|
|
173
|
+
console.log(JSON.stringify({ ok: true, count: result.data.messages.length, file: outPath }));
|
|
174
|
+
} else {
|
|
175
|
+
console.log(combined);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
if (outPath) {
|
|
179
|
+
console.log(JSON.stringify({ ok: true, count: 0 }));
|
|
180
|
+
}
|
|
181
|
+
// 메시지 없으면 빈 출력
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function cmdDeregister(args) {
|
|
186
|
+
const agentId = args.agent;
|
|
187
|
+
const result = await post('/bridge/deregister', { agent_id: agentId });
|
|
188
|
+
|
|
189
|
+
if (result?.ok) {
|
|
190
|
+
console.log(JSON.stringify({ ok: true, agent_id: agentId, status: 'offline' }));
|
|
191
|
+
} else {
|
|
192
|
+
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function cmdPing() {
|
|
197
|
+
try {
|
|
198
|
+
const url = `${getHubUrl()}/status`;
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
201
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
const data = await res.json();
|
|
204
|
+
console.log(JSON.stringify({ ok: true, hub: data.hub?.state, sessions: data.sessions }));
|
|
205
|
+
} catch {
|
|
206
|
+
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── 메인 ──
|
|
211
|
+
|
|
212
|
+
const cmd = process.argv[2];
|
|
213
|
+
const args = parseArgs(process.argv.slice(3));
|
|
214
|
+
|
|
215
|
+
switch (cmd) {
|
|
216
|
+
case 'register': await cmdRegister(args); break;
|
|
217
|
+
case 'result': await cmdResult(args); break;
|
|
218
|
+
case 'context': await cmdContext(args); break;
|
|
219
|
+
case 'deregister': await cmdDeregister(args); break;
|
|
220
|
+
case 'ping': await cmdPing(); break;
|
|
221
|
+
default:
|
|
222
|
+
console.error('사용법: bridge.mjs <register|result|context|deregister|ping> [--옵션]');
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
package/hub/router.mjs
CHANGED
package/hub/server.mjs
CHANGED
|
@@ -121,6 +121,87 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1' } = {}
|
|
|
121
121
|
}));
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// /bridge/* — 경량 REST 엔드포인트 (tfx-route.sh 브릿지용)
|
|
125
|
+
if (req.url.startsWith('/bridge')) {
|
|
126
|
+
res.setHeader('Content-Type', 'application/json');
|
|
127
|
+
|
|
128
|
+
if (req.method !== 'POST' && req.method !== 'DELETE') {
|
|
129
|
+
res.writeHead(405);
|
|
130
|
+
return res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed' }));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const body = (req.method === 'POST') ? await parseBody(req) : {};
|
|
135
|
+
const path = req.url.replace(/\?.*/, '');
|
|
136
|
+
|
|
137
|
+
// POST /bridge/register — 에이전트 등록 (프로세스 수명 기반)
|
|
138
|
+
if (path === '/bridge/register' && req.method === 'POST') {
|
|
139
|
+
const { agent_id, cli, timeout_sec = 600, topics = [], capabilities = [], metadata = {} } = body;
|
|
140
|
+
if (!agent_id || !cli) {
|
|
141
|
+
res.writeHead(400);
|
|
142
|
+
return res.end(JSON.stringify({ ok: false, error: 'agent_id, cli 필수' }));
|
|
143
|
+
}
|
|
144
|
+
// heartbeat = 프로세스 타임아웃 + 여유 120초
|
|
145
|
+
const heartbeat_ttl_ms = (timeout_sec + 120) * 1000;
|
|
146
|
+
const data = store.registerAgent({ agent_id, cli, capabilities, topics, heartbeat_ttl_ms, metadata });
|
|
147
|
+
res.writeHead(200);
|
|
148
|
+
return res.end(JSON.stringify({ ok: true, data }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// POST /bridge/result — 결과 발행
|
|
152
|
+
if (path === '/bridge/result' && req.method === 'POST') {
|
|
153
|
+
const { agent_id, topic = 'task.result', payload = {}, trace_id, correlation_id } = body;
|
|
154
|
+
if (!agent_id) {
|
|
155
|
+
res.writeHead(400);
|
|
156
|
+
return res.end(JSON.stringify({ ok: false, error: 'agent_id 필수' }));
|
|
157
|
+
}
|
|
158
|
+
const result = router.handlePublish({
|
|
159
|
+
from: agent_id, to: 'topic:' + topic, topic, payload,
|
|
160
|
+
priority: 5, ttl_ms: 3600000, trace_id, correlation_id,
|
|
161
|
+
});
|
|
162
|
+
res.writeHead(200);
|
|
163
|
+
return res.end(JSON.stringify(result));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// POST /bridge/context — 선행 컨텍스트 폴링
|
|
167
|
+
if (path === '/bridge/context' && req.method === 'POST') {
|
|
168
|
+
const { agent_id, topics, max_messages = 10 } = body;
|
|
169
|
+
if (!agent_id) {
|
|
170
|
+
res.writeHead(400);
|
|
171
|
+
return res.end(JSON.stringify({ ok: false, error: 'agent_id 필수' }));
|
|
172
|
+
}
|
|
173
|
+
const messages = store.pollForAgent(agent_id, {
|
|
174
|
+
max_messages,
|
|
175
|
+
include_topics: topics,
|
|
176
|
+
auto_ack: true,
|
|
177
|
+
});
|
|
178
|
+
res.writeHead(200);
|
|
179
|
+
return res.end(JSON.stringify({ ok: true, data: { messages, count: messages.length } }));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// POST /bridge/deregister — 에이전트 해제
|
|
183
|
+
if (path === '/bridge/deregister' && req.method === 'POST') {
|
|
184
|
+
const { agent_id } = body;
|
|
185
|
+
if (!agent_id) {
|
|
186
|
+
res.writeHead(400);
|
|
187
|
+
return res.end(JSON.stringify({ ok: false, error: 'agent_id 필수' }));
|
|
188
|
+
}
|
|
189
|
+
store.db.prepare("UPDATE agents SET status='offline' WHERE agent_id=?").run(agent_id);
|
|
190
|
+
res.writeHead(200);
|
|
191
|
+
return res.end(JSON.stringify({ ok: true, data: { agent_id, status: 'offline' } }));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
res.writeHead(404);
|
|
195
|
+
return res.end(JSON.stringify({ ok: false, error: 'Unknown bridge endpoint' }));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
if (!res.headersSent) {
|
|
198
|
+
res.writeHead(500);
|
|
199
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
124
205
|
// /mcp — MCP Streamable HTTP 엔드포인트
|
|
125
206
|
if (req.url !== '/mcp') {
|
|
126
207
|
res.writeHead(404);
|
package/hub/store.mjs
CHANGED
|
@@ -84,7 +84,7 @@ export function createStore(dbPath) {
|
|
|
84
84
|
allAgents: db.prepare('SELECT * FROM agents'),
|
|
85
85
|
agentsByTopic: db.prepare("SELECT a.* FROM agents a, json_each(a.topics_json) t WHERE t.value=? AND a.status != 'offline'"),
|
|
86
86
|
markStale: db.prepare("UPDATE agents SET status='stale' WHERE status='online' AND lease_expires_ms < ?"),
|
|
87
|
-
markOffline: db.prepare("UPDATE agents SET status='offline' WHERE status='stale' AND lease_expires_ms < ? -
|
|
87
|
+
markOffline: db.prepare("UPDATE agents SET status='offline' WHERE status='stale' AND lease_expires_ms < ? - 300000"),
|
|
88
88
|
|
|
89
89
|
// 메시지
|
|
90
90
|
insertMsg: db.prepare(`
|
package/hub/tools.mjs
CHANGED
|
@@ -38,7 +38,7 @@ export function createTools(store, router, hitl) {
|
|
|
38
38
|
capabilities: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 64 },
|
|
39
39
|
topics: { type: 'array', items: { type: 'string' }, maxItems: 64 },
|
|
40
40
|
metadata: { type: 'object' },
|
|
41
|
-
heartbeat_ttl_ms: { type: 'integer', minimum:
|
|
41
|
+
heartbeat_ttl_ms: { type: 'integer', minimum: 10000, maximum: 7200000 },
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
handler: wrap('REGISTER_FAILED', (args) => {
|
package/package.json
CHANGED
package/scripts/tfx-route.sh
CHANGED
|
@@ -38,6 +38,38 @@ STDERR_LOG="/tmp/tfx-route-${AGENT_TYPE}-${TIMESTAMP}-stderr.log"
|
|
|
38
38
|
STDOUT_LOG="/tmp/tfx-route-${AGENT_TYPE}-${TIMESTAMP}-stdout.log"
|
|
39
39
|
TFX_TMP="${TMPDIR:-/tmp}"
|
|
40
40
|
|
|
41
|
+
# ── Hub 브릿지 (선택적 — Hub 미실행 시 무시) ──
|
|
42
|
+
# 패키지 내 브릿지 탐색 (npm global / git local 모두 대응)
|
|
43
|
+
find_bridge() {
|
|
44
|
+
# 1. 환경변수 지정
|
|
45
|
+
[[ -n "${TFX_BRIDGE:-}" && -f "$TFX_BRIDGE" ]] && echo "$TFX_BRIDGE" && return
|
|
46
|
+
# 2. 같은 패키지 내
|
|
47
|
+
local script_dir
|
|
48
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
49
|
+
local pkg_bridge="${script_dir}/../hub/bridge.mjs"
|
|
50
|
+
[[ -f "$pkg_bridge" ]] && echo "$pkg_bridge" && return
|
|
51
|
+
# 3. 설치된 triflux 패키지
|
|
52
|
+
local npm_bridge
|
|
53
|
+
npm_bridge="$(npm root -g 2>/dev/null)/triflux/hub/bridge.mjs"
|
|
54
|
+
[[ -f "$npm_bridge" ]] && echo "$npm_bridge" && return
|
|
55
|
+
echo ""
|
|
56
|
+
}
|
|
57
|
+
BRIDGE_BIN="$(find_bridge)"
|
|
58
|
+
HUB_ENABLED="false"
|
|
59
|
+
if [[ -n "$BRIDGE_BIN" ]]; then
|
|
60
|
+
# Hub 핑 (3초 타임아웃, 실패 시 무시)
|
|
61
|
+
HUB_PING=$(node "$BRIDGE_BIN" ping 2>/dev/null || echo '{"ok":false}')
|
|
62
|
+
if echo "$HUB_PING" | grep -q '"ok":true'; then
|
|
63
|
+
HUB_ENABLED="true"
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Hub 브릿지 래퍼 (Hub 꺼져있으면 아무것도 안 함)
|
|
68
|
+
hub_bridge() {
|
|
69
|
+
[[ "$HUB_ENABLED" != "true" ]] && return 0
|
|
70
|
+
node "$BRIDGE_BIN" "$@" 2>/dev/null || true
|
|
71
|
+
}
|
|
72
|
+
|
|
41
73
|
# fallback 시 원래 에이전트 정보 보존
|
|
42
74
|
ORIGINAL_AGENT=""
|
|
43
75
|
ORIGINAL_CLI_ARGS=""
|
|
@@ -353,11 +385,31 @@ ${ctx_content}
|
|
|
353
385
|
|
|
354
386
|
# 메타정보 (stderr)
|
|
355
387
|
echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
|
|
356
|
-
echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE" >&2
|
|
388
|
+
echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE hub=$HUB_ENABLED" >&2
|
|
357
389
|
|
|
358
390
|
# Per-process 에이전트 등록
|
|
359
391
|
register_agent
|
|
360
392
|
|
|
393
|
+
# Hub 브릿지: 에이전트 등록 (프로세스 수명 기반 lease)
|
|
394
|
+
local hub_agent_id="${AGENT_TYPE}-$$"
|
|
395
|
+
local hub_topics="${AGENT_TYPE},task.result"
|
|
396
|
+
hub_bridge register \
|
|
397
|
+
--agent "$hub_agent_id" \
|
|
398
|
+
--cli "$CLI_TYPE" \
|
|
399
|
+
--timeout "$TIMEOUT_SEC" \
|
|
400
|
+
--topics "$hub_topics" \
|
|
401
|
+
--capabilities "code,${AGENT_TYPE}"
|
|
402
|
+
|
|
403
|
+
# Hub 브릿지: 선행 컨텍스트 폴링 (DAG 의존 태스크용)
|
|
404
|
+
if [[ "$HUB_ENABLED" == "true" && -z "$CONTEXT_FILE" ]]; then
|
|
405
|
+
local hub_ctx_file="${TFX_TMP}/tfx-hub-ctx-${hub_agent_id}.md"
|
|
406
|
+
hub_bridge context --agent "$hub_agent_id" --topics "$hub_topics" --out "$hub_ctx_file"
|
|
407
|
+
if [[ -s "$hub_ctx_file" ]]; then
|
|
408
|
+
CONTEXT_FILE="$hub_ctx_file"
|
|
409
|
+
echo "[tfx-route] hub: 선행 컨텍스트 수신 ($(wc -c < "$hub_ctx_file") bytes)" >&2
|
|
410
|
+
fi
|
|
411
|
+
fi
|
|
412
|
+
|
|
361
413
|
# CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
|
|
362
414
|
local exit_code=0
|
|
363
415
|
local start_time
|
|
@@ -414,6 +466,14 @@ ${ctx_content}
|
|
|
414
466
|
end_time=$(date +%s)
|
|
415
467
|
local elapsed=$((end_time - start_time))
|
|
416
468
|
|
|
469
|
+
# Hub 브릿지: 결과 발행 + 에이전트 해제
|
|
470
|
+
hub_bridge result \
|
|
471
|
+
--agent "$hub_agent_id" \
|
|
472
|
+
--file "$STDOUT_LOG" \
|
|
473
|
+
--topic "task.result" \
|
|
474
|
+
--exit-code "$exit_code"
|
|
475
|
+
hub_bridge deregister --agent "$hub_agent_id"
|
|
476
|
+
|
|
417
477
|
# ── 후처리: 단일 node 프로세스로 위임 ──
|
|
418
478
|
# 토큰 추출, 출력 필터링, 로그, 토큰 누적, AIMD, 이슈 추적, 결과 출력 전부 처리
|
|
419
479
|
local post_script="${HOME}/.claude/scripts/tfx-route-post.mjs"
|