triflux 10.3.1 → 10.3.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +22 -22
- package/LICENSE +21 -21
- package/hooks/hook-registry.json +256 -256
- package/hub/adaptive-inject.mjs +1 -1
- package/hub/assign-callbacks.mjs +120 -120
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/hitl.mjs +143 -143
- package/hub/router.mjs +791 -791
- package/hub/session-fingerprint.mjs +1 -1
- package/hub/team/ansi.mjs +44 -28
- package/hub/team/cli/commands/attach.mjs +37 -37
- package/hub/team/cli/commands/debug.mjs +74 -74
- package/hub/team/cli/commands/focus.mjs +53 -53
- package/hub/team/cli/commands/list.mjs +24 -24
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -40
- package/hub/team/cli/commands/start/start-mux.mjs +73 -73
- package/hub/team/cli/commands/start/start-wt.mjs +69 -69
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/render.mjs +30 -30
- package/hub/team/cli/services/attach-fallback.mjs +54 -54
- package/hub/team/cli/services/member-selector.mjs +30 -30
- package/hub/team/cli/services/native-control.mjs +116 -116
- package/hub/team/cli/services/task-model.mjs +30 -30
- package/hub/team/conductor.mjs +2 -2
- package/hub/team/notify.mjs +1 -1
- package/hub/team/orchestrator.mjs +161 -161
- package/hub/team/session.mjs +611 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/tui-lite.mjs +4 -4
- package/hub/team/tui.mjs +16 -12
- package/hub/tray.mjs +368 -368
- package/hub/workers/codex-mcp.mjs +507 -507
- package/hub/workers/factory.mjs +21 -21
- package/hud/constants.mjs +8 -2
- package/hud/providers/codex.mjs +11 -0
- package/hud/providers/gemini.mjs +21 -0
- package/package.json +1 -1
- package/scripts/claudemd-sync.mjs +11 -24
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/hub-ensure.mjs +120 -120
- package/scripts/keyword-detector.mjs +272 -272
- package/scripts/keyword-rules-expander.mjs +521 -521
- package/scripts/lib/mcp-server-catalog.mjs +118 -118
- package/scripts/notion-read.mjs +553 -553
- package/scripts/setup.mjs +23 -0
- package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
- package/scripts/tfx-batch-stats.mjs +96 -96
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- package/skills/tfx-remote-spawn/references/hosts.json +16 -0
package/hub/assign-callbacks.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// hub/assign-callbacks.mjs — assign job 상태 변경용 Named Pipe/Unix socket 브로드캐스터
|
|
2
|
-
|
|
1
|
+
// hub/assign-callbacks.mjs — assign job 상태 변경용 Named Pipe/Unix socket 브로드캐스터
|
|
2
|
+
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
5
5
|
import { IS_WINDOWS, pipePath } from './platform.mjs';
|
|
@@ -7,127 +7,127 @@ import { IS_WINDOWS, pipePath } from './platform.mjs';
|
|
|
7
7
|
export function getAssignCallbackPipePath(sessionId = process.pid) {
|
|
8
8
|
return pipePath('triflux-assign-callback', sessionId);
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
function buildAssignCallbackEvent(event = {}, row = null) {
|
|
12
|
-
const source = row || event || {};
|
|
13
|
-
const updatedAtMs = Number(source.updated_at_ms);
|
|
14
|
-
const createdAtMs = Number(source.created_at_ms);
|
|
15
|
-
const timestampMs = Number.isFinite(updatedAtMs)
|
|
16
|
-
? updatedAtMs
|
|
17
|
-
: (Number.isFinite(createdAtMs) ? createdAtMs : Date.now());
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
event: 'assign_job_status',
|
|
21
|
-
job_id: source.job_id || event.job_id || null,
|
|
22
|
-
supervisor_agent: source.supervisor_agent || null,
|
|
23
|
-
worker_agent: source.worker_agent || null,
|
|
24
|
-
topic: source.topic || null,
|
|
25
|
-
task: source.task || null,
|
|
26
|
-
status: source.status || event.status || null,
|
|
27
|
-
attempt: Number.isFinite(Number(source.attempt)) ? Number(source.attempt) : null,
|
|
28
|
-
retry_count: Number.isFinite(Number(source.retry_count)) ? Number(source.retry_count) : null,
|
|
29
|
-
max_retries: Number.isFinite(Number(source.max_retries)) ? Number(source.max_retries) : null,
|
|
30
|
-
priority: Number.isFinite(Number(source.priority)) ? Number(source.priority) : null,
|
|
31
|
-
ttl_ms: Number.isFinite(Number(source.ttl_ms)) ? Number(source.ttl_ms) : null,
|
|
32
|
-
timeout_ms: Number.isFinite(Number(source.timeout_ms)) ? Number(source.timeout_ms) : null,
|
|
33
|
-
deadline_ms: Number.isFinite(Number(source.deadline_ms)) ? Number(source.deadline_ms) : null,
|
|
34
|
-
trace_id: source.trace_id || null,
|
|
35
|
-
correlation_id: source.correlation_id || null,
|
|
36
|
-
last_message_id: source.last_message_id || null,
|
|
37
|
-
result: Object.hasOwn(source, 'result')
|
|
38
|
-
? source.result
|
|
39
|
-
: (Object.hasOwn(event, 'result') ? event.result : null),
|
|
40
|
-
error: Object.hasOwn(source, 'error')
|
|
41
|
-
? source.error
|
|
42
|
-
: (Object.hasOwn(event, 'error') ? event.error : null),
|
|
43
|
-
created_at_ms: Number.isFinite(createdAtMs) ? createdAtMs : null,
|
|
44
|
-
updated_at_ms: Number.isFinite(updatedAtMs) ? updatedAtMs : null,
|
|
45
|
-
started_at_ms: Number.isFinite(Number(source.started_at_ms)) ? Number(source.started_at_ms) : null,
|
|
46
|
-
completed_at_ms: Number.isFinite(Number(source.completed_at_ms)) ? Number(source.completed_at_ms) : null,
|
|
47
|
-
last_retry_at_ms: Number.isFinite(Number(source.last_retry_at_ms)) ? Number(source.last_retry_at_ms) : null,
|
|
48
|
-
timestamp: new Date(timestampMs).toISOString(),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function createAssignCallbackServer({ store = null, sessionId = process.pid } = {}) {
|
|
53
|
-
const pipePath = getAssignCallbackPipePath(sessionId);
|
|
54
|
-
const clients = new Set();
|
|
55
|
-
let server = null;
|
|
56
|
-
let detachStoreListener = null;
|
|
57
|
-
|
|
58
|
-
function removeSocket(socket) {
|
|
59
|
-
if (!socket) return;
|
|
60
|
-
clients.delete(socket);
|
|
61
|
-
try { socket.destroy(); } catch {}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function broadcast(event) {
|
|
65
|
-
const frame = `${JSON.stringify(event)}\n`;
|
|
66
|
-
for (const socket of Array.from(clients)) {
|
|
67
|
-
if (!socket.writable || socket.destroyed) {
|
|
68
|
-
removeSocket(socket);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
socket.write(frame);
|
|
73
|
-
} catch {
|
|
74
|
-
removeSocket(socket);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
path: pipePath,
|
|
81
|
-
getStatus() {
|
|
82
|
-
return {
|
|
83
|
-
path: pipePath,
|
|
84
|
-
clients: clients.size,
|
|
85
|
-
};
|
|
86
|
-
},
|
|
87
|
-
async start() {
|
|
88
|
-
if (server) return { path: pipePath };
|
|
10
|
+
|
|
11
|
+
function buildAssignCallbackEvent(event = {}, row = null) {
|
|
12
|
+
const source = row || event || {};
|
|
13
|
+
const updatedAtMs = Number(source.updated_at_ms);
|
|
14
|
+
const createdAtMs = Number(source.created_at_ms);
|
|
15
|
+
const timestampMs = Number.isFinite(updatedAtMs)
|
|
16
|
+
? updatedAtMs
|
|
17
|
+
: (Number.isFinite(createdAtMs) ? createdAtMs : Date.now());
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
event: 'assign_job_status',
|
|
21
|
+
job_id: source.job_id || event.job_id || null,
|
|
22
|
+
supervisor_agent: source.supervisor_agent || null,
|
|
23
|
+
worker_agent: source.worker_agent || null,
|
|
24
|
+
topic: source.topic || null,
|
|
25
|
+
task: source.task || null,
|
|
26
|
+
status: source.status || event.status || null,
|
|
27
|
+
attempt: Number.isFinite(Number(source.attempt)) ? Number(source.attempt) : null,
|
|
28
|
+
retry_count: Number.isFinite(Number(source.retry_count)) ? Number(source.retry_count) : null,
|
|
29
|
+
max_retries: Number.isFinite(Number(source.max_retries)) ? Number(source.max_retries) : null,
|
|
30
|
+
priority: Number.isFinite(Number(source.priority)) ? Number(source.priority) : null,
|
|
31
|
+
ttl_ms: Number.isFinite(Number(source.ttl_ms)) ? Number(source.ttl_ms) : null,
|
|
32
|
+
timeout_ms: Number.isFinite(Number(source.timeout_ms)) ? Number(source.timeout_ms) : null,
|
|
33
|
+
deadline_ms: Number.isFinite(Number(source.deadline_ms)) ? Number(source.deadline_ms) : null,
|
|
34
|
+
trace_id: source.trace_id || null,
|
|
35
|
+
correlation_id: source.correlation_id || null,
|
|
36
|
+
last_message_id: source.last_message_id || null,
|
|
37
|
+
result: Object.hasOwn(source, 'result')
|
|
38
|
+
? source.result
|
|
39
|
+
: (Object.hasOwn(event, 'result') ? event.result : null),
|
|
40
|
+
error: Object.hasOwn(source, 'error')
|
|
41
|
+
? source.error
|
|
42
|
+
: (Object.hasOwn(event, 'error') ? event.error : null),
|
|
43
|
+
created_at_ms: Number.isFinite(createdAtMs) ? createdAtMs : null,
|
|
44
|
+
updated_at_ms: Number.isFinite(updatedAtMs) ? updatedAtMs : null,
|
|
45
|
+
started_at_ms: Number.isFinite(Number(source.started_at_ms)) ? Number(source.started_at_ms) : null,
|
|
46
|
+
completed_at_ms: Number.isFinite(Number(source.completed_at_ms)) ? Number(source.completed_at_ms) : null,
|
|
47
|
+
last_retry_at_ms: Number.isFinite(Number(source.last_retry_at_ms)) ? Number(source.last_retry_at_ms) : null,
|
|
48
|
+
timestamp: new Date(timestampMs).toISOString(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createAssignCallbackServer({ store = null, sessionId = process.pid } = {}) {
|
|
53
|
+
const pipePath = getAssignCallbackPipePath(sessionId);
|
|
54
|
+
const clients = new Set();
|
|
55
|
+
let server = null;
|
|
56
|
+
let detachStoreListener = null;
|
|
57
|
+
|
|
58
|
+
function removeSocket(socket) {
|
|
59
|
+
if (!socket) return;
|
|
60
|
+
clients.delete(socket);
|
|
61
|
+
try { socket.destroy(); } catch {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function broadcast(event) {
|
|
65
|
+
const frame = `${JSON.stringify(event)}\n`;
|
|
66
|
+
for (const socket of Array.from(clients)) {
|
|
67
|
+
if (!socket.writable || socket.destroyed) {
|
|
68
|
+
removeSocket(socket);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
socket.write(frame);
|
|
73
|
+
} catch {
|
|
74
|
+
removeSocket(socket);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
path: pipePath,
|
|
81
|
+
getStatus() {
|
|
82
|
+
return {
|
|
83
|
+
path: pipePath,
|
|
84
|
+
clients: clients.size,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
async start() {
|
|
88
|
+
if (server) return { path: pipePath };
|
|
89
89
|
if (!IS_WINDOWS && existsSync(pipePath)) {
|
|
90
90
|
try { unlinkSync(pipePath); } catch {}
|
|
91
91
|
}
|
|
92
|
-
|
|
93
|
-
server = net.createServer((socket) => {
|
|
94
|
-
clients.add(socket);
|
|
95
|
-
socket.setEncoding('utf8');
|
|
96
|
-
socket.on('error', () => removeSocket(socket));
|
|
97
|
-
socket.on('close', () => removeSocket(socket));
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
await new Promise((resolve, reject) => {
|
|
101
|
-
server.once('error', reject);
|
|
102
|
-
server.listen(pipePath, () => {
|
|
103
|
-
server?.off('error', reject);
|
|
104
|
-
resolve();
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
if (store?.onAssignStatusChange && !detachStoreListener) {
|
|
109
|
-
detachStoreListener = store.onAssignStatusChange((event, row) => {
|
|
110
|
-
broadcast(buildAssignCallbackEvent(event, row));
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return { path: pipePath };
|
|
115
|
-
},
|
|
116
|
-
async stop() {
|
|
117
|
-
if (detachStoreListener) {
|
|
118
|
-
try { detachStoreListener(); } catch {}
|
|
119
|
-
detachStoreListener = null;
|
|
120
|
-
}
|
|
121
|
-
if (!server) return;
|
|
122
|
-
for (const socket of Array.from(clients)) {
|
|
123
|
-
removeSocket(socket);
|
|
124
|
-
}
|
|
125
|
-
await new Promise((resolve) => server.close(resolve));
|
|
126
|
-
server = null;
|
|
92
|
+
|
|
93
|
+
server = net.createServer((socket) => {
|
|
94
|
+
clients.add(socket);
|
|
95
|
+
socket.setEncoding('utf8');
|
|
96
|
+
socket.on('error', () => removeSocket(socket));
|
|
97
|
+
socket.on('close', () => removeSocket(socket));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
server.once('error', reject);
|
|
102
|
+
server.listen(pipePath, () => {
|
|
103
|
+
server?.off('error', reject);
|
|
104
|
+
resolve();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (store?.onAssignStatusChange && !detachStoreListener) {
|
|
109
|
+
detachStoreListener = store.onAssignStatusChange((event, row) => {
|
|
110
|
+
broadcast(buildAssignCallbackEvent(event, row));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { path: pipePath };
|
|
115
|
+
},
|
|
116
|
+
async stop() {
|
|
117
|
+
if (detachStoreListener) {
|
|
118
|
+
try { detachStoreListener(); } catch {}
|
|
119
|
+
detachStoreListener = null;
|
|
120
|
+
}
|
|
121
|
+
if (!server) return;
|
|
122
|
+
for (const socket of Array.from(clients)) {
|
|
123
|
+
removeSocket(socket);
|
|
124
|
+
}
|
|
125
|
+
await new Promise((resolve) => server.close(resolve));
|
|
126
|
+
server = null;
|
|
127
127
|
if (!IS_WINDOWS && existsSync(pipePath)) {
|
|
128
128
|
try { unlinkSync(pipePath); } catch {}
|
|
129
129
|
}
|
|
130
|
-
},
|
|
131
|
-
broadcast,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
130
|
+
},
|
|
131
|
+
broadcast,
|
|
132
|
+
};
|
|
133
|
+
}
|
package/hub/delegator/index.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export {
|
|
2
|
-
DELEGATOR_JOB_STATUSES,
|
|
3
|
-
DELEGATOR_MCP_SERVER_INFO,
|
|
4
|
-
DELEGATOR_MODES,
|
|
5
|
-
DELEGATOR_PIPE_ACTIONS,
|
|
6
|
-
DELEGATOR_PROVIDERS,
|
|
7
|
-
DELEGATOR_SCHEMA_URL,
|
|
8
|
-
DELEGATOR_TOOL_NAMES,
|
|
9
|
-
} from './contracts.mjs';
|
|
10
|
-
export { DelegatorService } from './service.mjs';
|
|
11
|
-
export {
|
|
12
|
-
getDelegatorMcpToolDefinitions,
|
|
13
|
-
loadDelegatorSchemaBundle,
|
|
14
|
-
} from './tool-definitions.mjs';
|
|
1
|
+
export {
|
|
2
|
+
DELEGATOR_JOB_STATUSES,
|
|
3
|
+
DELEGATOR_MCP_SERVER_INFO,
|
|
4
|
+
DELEGATOR_MODES,
|
|
5
|
+
DELEGATOR_PIPE_ACTIONS,
|
|
6
|
+
DELEGATOR_PROVIDERS,
|
|
7
|
+
DELEGATOR_SCHEMA_URL,
|
|
8
|
+
DELEGATOR_TOOL_NAMES,
|
|
9
|
+
} from './contracts.mjs';
|
|
10
|
+
export { DelegatorService } from './service.mjs';
|
|
11
|
+
export {
|
|
12
|
+
getDelegatorMcpToolDefinitions,
|
|
13
|
+
loadDelegatorSchemaBundle,
|
|
14
|
+
} from './tool-definitions.mjs';
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
|
|
3
|
-
import { DELEGATOR_SCHEMA_URL } from './contracts.mjs';
|
|
4
|
-
|
|
5
|
-
let schemaBundleCache = null;
|
|
6
|
-
|
|
7
|
-
function deepClone(value) {
|
|
8
|
-
if (value == null) return value;
|
|
9
|
-
return JSON.parse(JSON.stringify(value));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function loadDelegatorSchemaBundle() {
|
|
13
|
-
if (schemaBundleCache) {
|
|
14
|
-
return schemaBundleCache;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
schemaBundleCache = JSON.parse(readFileSync(DELEGATOR_SCHEMA_URL, 'utf8'));
|
|
18
|
-
return schemaBundleCache;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function getDelegatorMcpToolDefinitions() {
|
|
22
|
-
const bundle = loadDelegatorSchemaBundle();
|
|
23
|
-
const defs = bundle.$defs || {};
|
|
24
|
-
const tools = Array.isArray(bundle['x-triflux-mcp-tools'])
|
|
25
|
-
? bundle['x-triflux-mcp-tools']
|
|
26
|
-
: [];
|
|
27
|
-
|
|
28
|
-
return tools.map((tool) => ({
|
|
29
|
-
name: tool.name,
|
|
30
|
-
description: tool.description,
|
|
31
|
-
inputSchema: deepClone(defs[tool.inputSchemaDef]),
|
|
32
|
-
outputSchema: deepClone(defs[tool.outputSchemaDef]),
|
|
33
|
-
pipeAction: tool.pipeAction,
|
|
34
|
-
}));
|
|
35
|
-
}
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { DELEGATOR_SCHEMA_URL } from './contracts.mjs';
|
|
4
|
+
|
|
5
|
+
let schemaBundleCache = null;
|
|
6
|
+
|
|
7
|
+
function deepClone(value) {
|
|
8
|
+
if (value == null) return value;
|
|
9
|
+
return JSON.parse(JSON.stringify(value));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function loadDelegatorSchemaBundle() {
|
|
13
|
+
if (schemaBundleCache) {
|
|
14
|
+
return schemaBundleCache;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
schemaBundleCache = JSON.parse(readFileSync(DELEGATOR_SCHEMA_URL, 'utf8'));
|
|
18
|
+
return schemaBundleCache;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getDelegatorMcpToolDefinitions() {
|
|
22
|
+
const bundle = loadDelegatorSchemaBundle();
|
|
23
|
+
const defs = bundle.$defs || {};
|
|
24
|
+
const tools = Array.isArray(bundle['x-triflux-mcp-tools'])
|
|
25
|
+
? bundle['x-triflux-mcp-tools']
|
|
26
|
+
: [];
|
|
27
|
+
|
|
28
|
+
return tools.map((tool) => ({
|
|
29
|
+
name: tool.name,
|
|
30
|
+
description: tool.description,
|
|
31
|
+
inputSchema: deepClone(defs[tool.inputSchemaDef]),
|
|
32
|
+
outputSchema: deepClone(defs[tool.outputSchemaDef]),
|
|
33
|
+
pipeAction: tool.pipeAction,
|
|
34
|
+
}));
|
|
35
|
+
}
|
package/hub/hitl.mjs
CHANGED
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
// hub/hitl.mjs — Human-in-the-Loop 매니저
|
|
2
|
-
// 사용자 입력 요청/응답, 타임아웃 자동 처리
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* HITL 매니저 생성
|
|
6
|
-
* @param {object} store — createStore() 반환 객체
|
|
7
|
-
* @param {object} router — createRouter() 반환 객체
|
|
8
|
-
*/
|
|
9
|
-
export function createHitlManager(store, router = null) {
|
|
10
|
-
function forwardHumanResponse({ requesterAgent, requestId, action, content, submittedBy, correlationId, traceId, priority }) {
|
|
11
|
-
if (!router?.handlePublish) {
|
|
12
|
-
throw new Error('router.handlePublish is required for HITL forwarding');
|
|
13
|
-
}
|
|
14
|
-
return router.handlePublish({
|
|
15
|
-
from: 'hub:hitl',
|
|
16
|
-
to: requesterAgent,
|
|
17
|
-
topic: 'human.response',
|
|
18
|
-
priority,
|
|
19
|
-
ttl_ms: 300000,
|
|
20
|
-
payload: { request_id: requestId, action, content, submitted_by: submittedBy },
|
|
21
|
-
correlation_id: correlationId,
|
|
22
|
-
trace_id: traceId,
|
|
23
|
-
message_type: 'human_response',
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
/**
|
|
29
|
-
* 사용자에게 입력 요청 생성
|
|
30
|
-
* 터미널에 알림 출력 후 pending 상태로 저장
|
|
31
|
-
*/
|
|
32
|
-
requestHumanInput({
|
|
33
|
-
requester_agent, kind, prompt, requested_schema = {},
|
|
34
|
-
deadline_ms, default_action, channel_preference = 'terminal',
|
|
35
|
-
correlation_id, trace_id,
|
|
36
|
-
}) {
|
|
37
|
-
const result = store.insertHumanRequest({
|
|
38
|
-
requester_agent, kind, prompt, requested_schema,
|
|
39
|
-
deadline_ms, default_action,
|
|
40
|
-
correlation_id, trace_id,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// 터미널 알림 (stderr — stdout은 MCP 용)
|
|
44
|
-
const kindLabel = { captcha: 'CAPTCHA', approval: '승인', credential: '자격증명', choice: '선택', text: '텍스트' };
|
|
45
|
-
process.stderr.write(
|
|
46
|
-
`\n[tfx-hub] 사용자 입력 요청 (${kindLabel[kind] || kind})\n` +
|
|
47
|
-
` 요청자: ${requester_agent}\n` +
|
|
48
|
-
` 내용: ${prompt}\n` +
|
|
49
|
-
` ID: ${result.request_id}\n` +
|
|
50
|
-
` 제한: ${Math.round(deadline_ms / 1000)}초\n\n`,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
return { ok: true, data: result };
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* 사용자 입력 응답 제출
|
|
58
|
-
* 유효성 검증 → 상태 업데이트 → 요청자에게 응답 메시지 전달
|
|
59
|
-
*/
|
|
60
|
-
submitHumanInput({ request_id, action, content = null, submitted_by = 'human' }) {
|
|
61
|
-
// 요청 조회
|
|
62
|
-
const hr = store.getHumanRequest(request_id);
|
|
63
|
-
if (!hr) {
|
|
64
|
-
return { ok: false, error: { code: 'NOT_FOUND', message: `요청 없음: ${request_id}` } };
|
|
65
|
-
}
|
|
66
|
-
if (hr.state !== 'pending') {
|
|
67
|
-
return { ok: false, error: { code: 'ALREADY_HANDLED', message: `이미 처리됨: ${hr.state}` } };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 상태 매핑
|
|
71
|
-
const stateMap = { accept: 'accepted', decline: 'declined', cancel: 'cancelled' };
|
|
72
|
-
const newState = stateMap[action];
|
|
73
|
-
if (!newState) {
|
|
74
|
-
return { ok: false, error: { code: 'INVALID_ACTION', message: `잘못된 action: ${action}` } };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// DB 업데이트
|
|
78
|
-
store.updateHumanRequest(request_id, newState, content);
|
|
79
|
-
|
|
80
|
-
// 요청자에게 응답 메시지 전달
|
|
81
|
-
let forwardedMessageId = null;
|
|
82
|
-
if (action === 'accept' || action === 'decline') {
|
|
83
|
-
const published = forwardHumanResponse({
|
|
84
|
-
requesterAgent: hr.requester_agent,
|
|
85
|
-
requestId: request_id,
|
|
86
|
-
action,
|
|
87
|
-
content,
|
|
88
|
-
submittedBy: submitted_by,
|
|
89
|
-
correlationId: hr.correlation_id,
|
|
90
|
-
traceId: hr.trace_id,
|
|
91
|
-
priority: 7,
|
|
92
|
-
});
|
|
93
|
-
forwardedMessageId = published.data?.message_id || null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
ok: true,
|
|
98
|
-
data: { request_id, new_state: newState, forwarded_message_id: forwardedMessageId },
|
|
99
|
-
};
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* 만료된 요청 자동 처리
|
|
104
|
-
* deadline 초과 시 default_action 적용
|
|
105
|
-
*/
|
|
106
|
-
checkTimeouts() {
|
|
107
|
-
const pending = store.getPendingHumanRequests();
|
|
108
|
-
const now = Date.now();
|
|
109
|
-
const expired = pending.filter(hr => hr.deadline_ms <= now);
|
|
110
|
-
if (!expired.length) return 0;
|
|
111
|
-
|
|
112
|
-
const expireRequests = () => {
|
|
113
|
-
for (const hr of expired) {
|
|
114
|
-
store.updateHumanRequest(hr.request_id, 'timed_out', null);
|
|
115
|
-
if (hr.default_action === 'timeout_continue') {
|
|
116
|
-
forwardHumanResponse({
|
|
117
|
-
requesterAgent: hr.requester_agent,
|
|
118
|
-
requestId: hr.request_id,
|
|
119
|
-
action: 'timeout_continue',
|
|
120
|
-
content: null,
|
|
121
|
-
submittedBy: 'system',
|
|
122
|
-
correlationId: hr.correlation_id,
|
|
123
|
-
traceId: hr.trace_id,
|
|
124
|
-
priority: 5,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return expired.length;
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const processExpired = store.db?.transaction
|
|
132
|
-
? store.db.transaction(expireRequests)
|
|
133
|
-
: expireRequests;
|
|
134
|
-
|
|
135
|
-
return processExpired();
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
/** 대기 중인 요청 목록 */
|
|
139
|
-
getPendingRequests() {
|
|
140
|
-
return store.getPendingHumanRequests();
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
}
|
|
1
|
+
// hub/hitl.mjs — Human-in-the-Loop 매니저
|
|
2
|
+
// 사용자 입력 요청/응답, 타임아웃 자동 처리
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HITL 매니저 생성
|
|
6
|
+
* @param {object} store — createStore() 반환 객체
|
|
7
|
+
* @param {object} router — createRouter() 반환 객체
|
|
8
|
+
*/
|
|
9
|
+
export function createHitlManager(store, router = null) {
|
|
10
|
+
function forwardHumanResponse({ requesterAgent, requestId, action, content, submittedBy, correlationId, traceId, priority }) {
|
|
11
|
+
if (!router?.handlePublish) {
|
|
12
|
+
throw new Error('router.handlePublish is required for HITL forwarding');
|
|
13
|
+
}
|
|
14
|
+
return router.handlePublish({
|
|
15
|
+
from: 'hub:hitl',
|
|
16
|
+
to: requesterAgent,
|
|
17
|
+
topic: 'human.response',
|
|
18
|
+
priority,
|
|
19
|
+
ttl_ms: 300000,
|
|
20
|
+
payload: { request_id: requestId, action, content, submitted_by: submittedBy },
|
|
21
|
+
correlation_id: correlationId,
|
|
22
|
+
trace_id: traceId,
|
|
23
|
+
message_type: 'human_response',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
/**
|
|
29
|
+
* 사용자에게 입력 요청 생성
|
|
30
|
+
* 터미널에 알림 출력 후 pending 상태로 저장
|
|
31
|
+
*/
|
|
32
|
+
requestHumanInput({
|
|
33
|
+
requester_agent, kind, prompt, requested_schema = {},
|
|
34
|
+
deadline_ms, default_action, channel_preference = 'terminal',
|
|
35
|
+
correlation_id, trace_id,
|
|
36
|
+
}) {
|
|
37
|
+
const result = store.insertHumanRequest({
|
|
38
|
+
requester_agent, kind, prompt, requested_schema,
|
|
39
|
+
deadline_ms, default_action,
|
|
40
|
+
correlation_id, trace_id,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 터미널 알림 (stderr — stdout은 MCP 용)
|
|
44
|
+
const kindLabel = { captcha: 'CAPTCHA', approval: '승인', credential: '자격증명', choice: '선택', text: '텍스트' };
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`\n[tfx-hub] 사용자 입력 요청 (${kindLabel[kind] || kind})\n` +
|
|
47
|
+
` 요청자: ${requester_agent}\n` +
|
|
48
|
+
` 내용: ${prompt}\n` +
|
|
49
|
+
` ID: ${result.request_id}\n` +
|
|
50
|
+
` 제한: ${Math.round(deadline_ms / 1000)}초\n\n`,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return { ok: true, data: result };
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 사용자 입력 응답 제출
|
|
58
|
+
* 유효성 검증 → 상태 업데이트 → 요청자에게 응답 메시지 전달
|
|
59
|
+
*/
|
|
60
|
+
submitHumanInput({ request_id, action, content = null, submitted_by = 'human' }) {
|
|
61
|
+
// 요청 조회
|
|
62
|
+
const hr = store.getHumanRequest(request_id);
|
|
63
|
+
if (!hr) {
|
|
64
|
+
return { ok: false, error: { code: 'NOT_FOUND', message: `요청 없음: ${request_id}` } };
|
|
65
|
+
}
|
|
66
|
+
if (hr.state !== 'pending') {
|
|
67
|
+
return { ok: false, error: { code: 'ALREADY_HANDLED', message: `이미 처리됨: ${hr.state}` } };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 상태 매핑
|
|
71
|
+
const stateMap = { accept: 'accepted', decline: 'declined', cancel: 'cancelled' };
|
|
72
|
+
const newState = stateMap[action];
|
|
73
|
+
if (!newState) {
|
|
74
|
+
return { ok: false, error: { code: 'INVALID_ACTION', message: `잘못된 action: ${action}` } };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// DB 업데이트
|
|
78
|
+
store.updateHumanRequest(request_id, newState, content);
|
|
79
|
+
|
|
80
|
+
// 요청자에게 응답 메시지 전달
|
|
81
|
+
let forwardedMessageId = null;
|
|
82
|
+
if (action === 'accept' || action === 'decline') {
|
|
83
|
+
const published = forwardHumanResponse({
|
|
84
|
+
requesterAgent: hr.requester_agent,
|
|
85
|
+
requestId: request_id,
|
|
86
|
+
action,
|
|
87
|
+
content,
|
|
88
|
+
submittedBy: submitted_by,
|
|
89
|
+
correlationId: hr.correlation_id,
|
|
90
|
+
traceId: hr.trace_id,
|
|
91
|
+
priority: 7,
|
|
92
|
+
});
|
|
93
|
+
forwardedMessageId = published.data?.message_id || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
data: { request_id, new_state: newState, forwarded_message_id: forwardedMessageId },
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 만료된 요청 자동 처리
|
|
104
|
+
* deadline 초과 시 default_action 적용
|
|
105
|
+
*/
|
|
106
|
+
checkTimeouts() {
|
|
107
|
+
const pending = store.getPendingHumanRequests();
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const expired = pending.filter(hr => hr.deadline_ms <= now);
|
|
110
|
+
if (!expired.length) return 0;
|
|
111
|
+
|
|
112
|
+
const expireRequests = () => {
|
|
113
|
+
for (const hr of expired) {
|
|
114
|
+
store.updateHumanRequest(hr.request_id, 'timed_out', null);
|
|
115
|
+
if (hr.default_action === 'timeout_continue') {
|
|
116
|
+
forwardHumanResponse({
|
|
117
|
+
requesterAgent: hr.requester_agent,
|
|
118
|
+
requestId: hr.request_id,
|
|
119
|
+
action: 'timeout_continue',
|
|
120
|
+
content: null,
|
|
121
|
+
submittedBy: 'system',
|
|
122
|
+
correlationId: hr.correlation_id,
|
|
123
|
+
traceId: hr.trace_id,
|
|
124
|
+
priority: 5,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return expired.length;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const processExpired = store.db?.transaction
|
|
132
|
+
? store.db.transaction(expireRequests)
|
|
133
|
+
: expireRequests;
|
|
134
|
+
|
|
135
|
+
return processExpired();
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/** 대기 중인 요청 목록 */
|
|
139
|
+
getPendingRequests() {
|
|
140
|
+
return store.getPendingHumanRequests();
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|